feat: enhance new-patient AI chat flow with full scheduling and eligibility
- Add 3-message intro (self-intro → empathetic ack → new/existing question) via single TwiML response to guarantee delivery order - Detect reschedule intent from first message; look up existing appointment date - New patient flow: ask insurance type → MassHealth consent → member ID + DOB → Selenium eligibility check - Post-eligibility: active → ask appointment date/time with office-hours validation; inactive → ask other insurance or collect contact info - Date/time collection mirrors reschedule flow: check office day open, ask time, validate against office hours - Auto-create appointment in schedule for known patients on confirmation; use first available staff member - Add openPhoneReply toggle (Settings → AI Chat) to respond to any number at any time - Add 5-minute inactivity timeout: reset conversation to initial stage and clear pending state - Normalize MassHealth DOB to zero-padded MM/DD/YYYY before Selenium submission - Expand isExistingPatient classifier to recognize "old patient", "old", "previous", "prior" - Existing patient confirmation message now acknowledges patient type before asking about insurance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,11 +8,17 @@ export type ConversationStage =
|
|||||||
| "new_patient_greeted"
|
| "new_patient_greeted"
|
||||||
| "asked_new_or_existing"
|
| "asked_new_or_existing"
|
||||||
| "asked_new_patient_insurance"
|
| "asked_new_patient_insurance"
|
||||||
|
| "asked_new_or_reschedule"
|
||||||
|
| "asked_insurance_type"
|
||||||
|
| "asked_masshealth_check_consent"
|
||||||
| "asked_existing_insurance"
|
| "asked_existing_insurance"
|
||||||
| "asked_appointment_time"
|
| "asked_appointment_time"
|
||||||
| "awaiting_masshealth_info"
|
| "awaiting_masshealth_info"
|
||||||
| "asked_appointment_preference"
|
| "asked_appointment_preference"
|
||||||
| "asked_self_pay"
|
| "asked_self_pay"
|
||||||
|
| "asked_other_insurance_after_inactive"
|
||||||
|
| "collecting_contact_info"
|
||||||
|
| "asked_new_appt_time_for_date"
|
||||||
| "asked_reschedule_confirm"
|
| "asked_reschedule_confirm"
|
||||||
| "asked_reschedule_preference"
|
| "asked_reschedule_preference"
|
||||||
| "asked_reschedule_asap"
|
| "asked_reschedule_asap"
|
||||||
|
|||||||
@@ -43,19 +43,19 @@ function isNewPatient(text: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isExistingPatient(text: string): boolean {
|
function isExistingPatient(text: string): boolean {
|
||||||
return /existing|been there|have been|already|before|i have been|returning|came before|i was there/i.test(text);
|
return /existing|old patient|old\b|been there|have been|already|before|i have been|returning|came before|i was there|previous|prior|past patient/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasMassHealth(text: string): boolean {
|
function hasMassHealth(text: string): boolean {
|
||||||
return /masshealth|mass health|medicaid|masscare/i.test(text);
|
return /masshealth|mass health|medicaid|masscare/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasOtherInsurance(text: string): boolean {
|
function hasInsurance(text: string): boolean {
|
||||||
return /blue cross|delta dental|cigna|aetna|united|metlife|guardian|humana|tufts|harvard pilgrim|bmchp|yes|i have|my insurance|i do/i.test(text);
|
return /yes|i have|my insurance|i do|have insurance|insured/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasNoInsurance(text: string): boolean {
|
function hasNoInsurance(text: string): boolean {
|
||||||
return /no insurance|uninsured|self.pay|self pay|i don't|don't have|no i don't|i have no/i.test(text);
|
return /no insurance|uninsured|self.pay|self pay|i don't|don't have|no i don't|i have no|no,? i|not insured/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameInsurance(text: string): boolean {
|
function sameInsurance(text: string): boolean {
|
||||||
@@ -66,6 +66,14 @@ function changedInsurance(text: string): boolean {
|
|||||||
return /no|changed|different|new insurance|switched|lost|expired/i.test(text);
|
return /no|changed|different|new insurance|switched|lost|expired/i.test(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function saysYes(text: string): boolean {
|
||||||
|
return /\byes\b|yeah|yep|sure|ok\b|okay|of course|absolutely|please|si\b|sí|oui|wi\b|نعم|好的/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saysNo(text: string): boolean {
|
||||||
|
return /\bno\b|nope|nah|not really|i don't|don't|no i|لا|非|no,/i.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
// ── LLM reply helper ──────────────────────────────────────────────────────────
|
// ── LLM reply helper ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function llmReply(
|
async function llmReply(
|
||||||
@@ -88,7 +96,6 @@ async function llmReply(
|
|||||||
|
|
||||||
// ── Graph nodes ───────────────────────────────────────────────────────────────
|
// ── Graph nodes ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Classify intent based on current stage
|
|
||||||
function classifyNode(state: GraphStateType) {
|
function classifyNode(state: GraphStateType) {
|
||||||
const text = state.message.toLowerCase();
|
const text = state.message.toLowerCase();
|
||||||
const stage = state.stage as ConversationStage;
|
const stage = state.stage as ConversationStage;
|
||||||
@@ -97,22 +104,43 @@ function classifyNode(state: GraphStateType) {
|
|||||||
|
|
||||||
if (stage === "new_patient_greeted") {
|
if (stage === "new_patient_greeted") {
|
||||||
intent = wantsAppointment(text) ? "wants_appointment" : "other";
|
intent = wantsAppointment(text) ? "wants_appointment" : "other";
|
||||||
|
|
||||||
} else if (stage === "asked_new_or_existing") {
|
} else if (stage === "asked_new_or_existing") {
|
||||||
if (isNewPatient(text)) intent = "new_patient";
|
if (isNewPatient(text)) intent = "new_patient";
|
||||||
else if (isExistingPatient(text)) intent = "existing_patient";
|
else if (isExistingPatient(text)) intent = "existing_patient";
|
||||||
|
|
||||||
} else if (stage === "asked_new_patient_insurance") {
|
} else if (stage === "asked_new_patient_insurance") {
|
||||||
if (hasMassHealth(text)) intent = "masshealth";
|
if (hasMassHealth(text)) intent = "masshealth";
|
||||||
else if (hasNoInsurance(text)) intent = "no_insurance";
|
else if (hasNoInsurance(text)) intent = "no_insurance";
|
||||||
else if (hasOtherInsurance(text)) intent = "other_insurance";
|
else if (hasInsurance(text)) intent = "has_insurance";
|
||||||
|
|
||||||
|
} else if (stage === "asked_insurance_type") {
|
||||||
|
if (hasMassHealth(text)) intent = "masshealth";
|
||||||
|
else intent = "other_insurance";
|
||||||
|
|
||||||
|
} else if (stage === "asked_masshealth_check_consent") {
|
||||||
|
if (saysYes(text)) intent = "yes";
|
||||||
|
else intent = "no";
|
||||||
|
|
||||||
} else if (stage === "asked_existing_insurance") {
|
} else if (stage === "asked_existing_insurance") {
|
||||||
if (sameInsurance(text)) intent = "same_insurance";
|
if (sameInsurance(text)) intent = "same_insurance";
|
||||||
else if (changedInsurance(text)) intent = "changed_insurance";
|
else if (changedInsurance(text)) intent = "changed_insurance";
|
||||||
|
|
||||||
} else if (stage === "asked_appointment_time") {
|
} else if (stage === "asked_appointment_time") {
|
||||||
intent = "appointment_time";
|
intent = "appointment_time";
|
||||||
|
|
||||||
} else if (stage === "asked_appointment_preference") {
|
} else if (stage === "asked_appointment_preference") {
|
||||||
intent = "appointment_preference_reply";
|
intent = "appointment_preference_reply";
|
||||||
|
|
||||||
} else if (stage === "asked_self_pay") {
|
} else if (stage === "asked_self_pay") {
|
||||||
intent = "self_pay_reply";
|
intent = "self_pay_reply";
|
||||||
|
|
||||||
|
} else if (stage === "asked_other_insurance_after_inactive") {
|
||||||
|
if (saysYes(text) && !saysNo(text)) intent = "yes";
|
||||||
|
else if (saysNo(text)) intent = "no";
|
||||||
|
|
||||||
|
} else if (stage === "collecting_contact_info") {
|
||||||
|
intent = "contact_info_received";
|
||||||
}
|
}
|
||||||
|
|
||||||
return { intent };
|
return { intent };
|
||||||
@@ -122,14 +150,40 @@ function routeNode(state: GraphStateType): string {
|
|||||||
const stage = state.stage as ConversationStage;
|
const stage = state.stage as ConversationStage;
|
||||||
const intent = state.intent;
|
const intent = state.intent;
|
||||||
|
|
||||||
if (stage === "new_patient_greeted") return intent === "wants_appointment" ? "ask_new_or_existing" : "transfer";
|
if (stage === "new_patient_greeted")
|
||||||
if (stage === "asked_new_or_existing") return intent === "new_patient" ? "ask_new_patient_insurance" : intent === "existing_patient" ? "ask_existing_insurance" : "transfer";
|
return intent === "wants_appointment" ? "ask_new_or_existing" : "transfer";
|
||||||
if (stage === "asked_new_patient_insurance") return intent === "masshealth" ? "ask_masshealth_info" : intent === "no_insurance" ? "ask_appointment_time" : "transfer";
|
|
||||||
if (stage === "asked_existing_insurance") return intent === "same_insurance" ? "ask_appointment_time" : "transfer";
|
if (stage === "asked_new_or_existing")
|
||||||
|
return intent === "new_patient" ? "ask_new_patient_insurance"
|
||||||
|
: intent === "existing_patient" ? "ask_existing_insurance"
|
||||||
|
: "transfer";
|
||||||
|
|
||||||
|
if (stage === "asked_new_patient_insurance")
|
||||||
|
return intent === "masshealth" ? "ask_masshealth_check_consent"
|
||||||
|
: intent === "no_insurance" ? "ask_appointment_time"
|
||||||
|
: intent === "has_insurance" ? "ask_insurance_type"
|
||||||
|
: "transfer";
|
||||||
|
|
||||||
|
if (stage === "asked_insurance_type")
|
||||||
|
return intent === "masshealth" ? "ask_masshealth_check_consent" : "ask_appointment_time";
|
||||||
|
|
||||||
|
if (stage === "asked_masshealth_check_consent")
|
||||||
|
return intent === "yes" ? "ask_masshealth_info" : "ask_appointment_time";
|
||||||
|
|
||||||
|
if (stage === "asked_existing_insurance")
|
||||||
|
return intent === "same_insurance" ? "ask_appointment_time" : "transfer";
|
||||||
|
|
||||||
if (stage === "asked_appointment_time") return "acknowledge_appointment_time";
|
if (stage === "asked_appointment_time") return "acknowledge_appointment_time";
|
||||||
if (stage === "asked_appointment_preference") return "handle_appointment_preference";
|
if (stage === "asked_appointment_preference") return "handle_appointment_preference";
|
||||||
if (stage === "asked_self_pay") return "handle_self_pay";
|
if (stage === "asked_self_pay") return "handle_self_pay";
|
||||||
|
|
||||||
|
if (stage === "asked_other_insurance_after_inactive")
|
||||||
|
return intent === "yes" ? "transfer"
|
||||||
|
: intent === "no" ? "ask_contact_info"
|
||||||
|
: "transfer";
|
||||||
|
|
||||||
|
if (stage === "collecting_contact_info") return "acknowledge_contact_info";
|
||||||
|
|
||||||
return "transfer";
|
return "transfer";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,30 +241,56 @@ async function askNewPatientInsuranceNode(state: GraphStateType, config: any) {
|
|||||||
return { reply, nextStage: "asked_new_patient_insurance" };
|
return { reply, nextStage: "asked_new_patient_insurance" };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askExistingInsuranceNode(state: GraphStateType, config: any) {
|
async function askInsuranceTypeNode(state: GraphStateType, config: any) {
|
||||||
const lang = state.language || "English";
|
const lang = state.language || "English";
|
||||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
const fallbacks: Record<string, string> = {
|
const fallbacks: Record<string, string> = {
|
||||||
English: "Do you still have the same insurance?",
|
English: "What kind of insurance do you have?",
|
||||||
Spanish: "¿Sigue teniendo el mismo seguro?",
|
Spanish: "¿Qué tipo de seguro tiene?",
|
||||||
Portuguese: "Você ainda tem o mesmo plano?",
|
Portuguese: "Que tipo de plano de saúde você tem?",
|
||||||
Mandarin: "您还有相同的保险吗?",
|
Mandarin: "您有什么类型的保险?",
|
||||||
Cantonese: "您仍然有相同的保險嗎?",
|
Cantonese: "您有什麼類型的保險?",
|
||||||
Arabic: "هل لا تزال تمتلك نفس التأمين؟",
|
Arabic: "ما نوع التأمين الذي لديك؟",
|
||||||
"Haitian Creole": "Èske ou toujou gen menm asirans lan?",
|
"Haitian Creole": "Ki kalite asirans ou genyen?",
|
||||||
};
|
};
|
||||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||||
|
|
||||||
const reply = apiKey
|
const reply = apiKey
|
||||||
? await llmReply(
|
? await llmReply(
|
||||||
`You are a friendly dental office AI assistant. Ask the existing patient in ${lang} if they still have the same dental insurance on file. One sentence, no formatting.`,
|
`You are a friendly dental office AI assistant. The patient confirmed they have insurance. Ask them in ${lang} what kind of insurance they have. One sentence, no formatting.`,
|
||||||
`Existing patient confirmed. Ask about insurance.`,
|
`Patient has insurance. Ask what kind.`,
|
||||||
fallback, apiKey
|
fallback, apiKey
|
||||||
)
|
)
|
||||||
: fallback;
|
: fallback;
|
||||||
|
|
||||||
return { reply, nextStage: "asked_existing_insurance" };
|
return { reply, nextStage: "asked_insurance_type" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askMassHealthCheckConsentNode(state: GraphStateType, config: any) {
|
||||||
|
const lang = state.language || "English";
|
||||||
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
|
const fallbacks: Record<string, string> = {
|
||||||
|
English: "Do you want to check your MassHealth insurance now?",
|
||||||
|
Spanish: "¿Desea verificar su seguro MassHealth ahora?",
|
||||||
|
Portuguese: "Você gostaria de verificar sua cobertura MassHealth agora?",
|
||||||
|
Mandarin: "您想现在查询您的MassHealth保险吗?",
|
||||||
|
Cantonese: "您想現在查詢您的MassHealth保險嗎?",
|
||||||
|
Arabic: "هل تريد التحقق من تأمين MassHealth الخاص بك الآن؟",
|
||||||
|
"Haitian Creole": "Èske ou vle verifye asirans MassHealth ou kounye a?",
|
||||||
|
};
|
||||||
|
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||||
|
|
||||||
|
const reply = apiKey
|
||||||
|
? await llmReply(
|
||||||
|
`You are a friendly dental office AI assistant. The patient has MassHealth insurance. Ask them in ${lang} if they would like to check their MassHealth coverage right now. One sentence, no formatting.`,
|
||||||
|
`Patient has MassHealth. Ask if they want to check it now.`,
|
||||||
|
fallback, apiKey
|
||||||
|
)
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
return { reply, nextStage: "asked_masshealth_check_consent" };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
||||||
@@ -218,20 +298,20 @@ async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
|||||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
const fallbacks: Record<string, string> = {
|
const fallbacks: Record<string, string> = {
|
||||||
English: "I can check your MassHealth coverage! Please text me your Member ID and date of birth.",
|
English: "Please send me your MassHealth Member ID and date of birth so I can check your coverage.",
|
||||||
Spanish: "¡Puedo verificar su cobertura de MassHealth! Por favor envíeme su número de miembro y fecha de nacimiento.",
|
Spanish: "Por favor envíeme su número de miembro de MassHealth y su fecha de nacimiento para verificar su cobertura.",
|
||||||
Portuguese: "Posso verificar sua cobertura MassHealth! Por favor envie seu número de membro e data de nascimento.",
|
Portuguese: "Por favor envie seu número de membro MassHealth e data de nascimento para verificar sua cobertura.",
|
||||||
Mandarin: "我可以查看您的MassHealth保险!请发送您的会员ID和出生日期。",
|
Mandarin: "请发送您的MassHealth会员ID和出生日期,以便我查询您的保险。",
|
||||||
Cantonese: "我可以查核您的MassHealth保險!請傳送您的會員ID和出生日期。",
|
Cantonese: "請傳送您的MassHealth會員ID和出生日期,以便我查詢您的保險。",
|
||||||
Arabic: "يمكنني التحقق من تغطية MassHealth الخاصة بك! من فضلك أرسل لي رقم العضوية وتاريخ الميلاد.",
|
Arabic: "من فضلك أرسل لي رقم عضوية MassHealth وتاريخ ميلادك حتى أتحقق من تغطيتك.",
|
||||||
"Haitian Creole": "Mwen ka verifye asirans MassHealth ou! Tanpri voye ID manm ou ak dat nesans ou.",
|
"Haitian Creole": "Tanpri voye ID manm MassHealth ou ak dat nesans ou pou mwen ka verifye kouvèti ou.",
|
||||||
};
|
};
|
||||||
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||||
|
|
||||||
const reply = apiKey
|
const reply = apiKey
|
||||||
? await llmReply(
|
? await llmReply(
|
||||||
`You are a friendly dental office AI assistant. The patient has MassHealth. Tell them in ${lang} that you can check their coverage and ask them to send their Member ID and date of birth. 1-2 sentences, no formatting.`,
|
`You are a friendly dental office AI assistant. The patient wants to check their MassHealth coverage. Ask them in ${lang} to provide their MassHealth Member ID and date of birth. 1-2 sentences, no formatting.`,
|
||||||
`Patient has MassHealth. Ask for member ID and DOB.`,
|
`Patient agreed to MassHealth check. Ask for member ID and DOB.`,
|
||||||
fallback, apiKey
|
fallback, apiKey
|
||||||
)
|
)
|
||||||
: fallback;
|
: fallback;
|
||||||
@@ -239,16 +319,42 @@ async function askMassHealthInfoNode(state: GraphStateType, config: any) {
|
|||||||
return { reply, nextStage: "awaiting_masshealth_info" };
|
return { reply, nextStage: "awaiting_masshealth_info" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function askExistingInsuranceNode(state: GraphStateType, config: any) {
|
||||||
|
const lang = state.language || "English";
|
||||||
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
|
const fallbacks: Record<string, string> = {
|
||||||
|
English: "Got it — you are an existing patient! Do you still have the same insurance on file?",
|
||||||
|
Spanish: "¡Entendido — usted es un paciente existente! ¿Sigue teniendo el mismo seguro registrado?",
|
||||||
|
Portuguese: "Entendido — você é um paciente existente! Você ainda tem o mesmo plano registrado?",
|
||||||
|
Mandarin: "好的——您是现有患者!您还有相同的保险记录吗?",
|
||||||
|
Cantonese: "好的——您是現有病人!您仍然有相同的保險記錄嗎?",
|
||||||
|
Arabic: "حسناً — أنت مريض حالي! هل لا تزال تمتلك نفس التأمين المسجل لدينا؟",
|
||||||
|
"Haitian Creole": "Konprann — ou se yon pasyan egzistan! Èske ou toujou gen menm asirans nou gen anrejistreman?",
|
||||||
|
};
|
||||||
|
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||||
|
|
||||||
|
const reply = apiKey
|
||||||
|
? await llmReply(
|
||||||
|
`You are a friendly dental office AI assistant. The patient confirmed they are an existing patient. In ${lang}, acknowledge that they are an existing patient and then ask if they still have the same dental insurance on file. 1-2 sentences, no formatting.`,
|
||||||
|
`Existing patient confirmed. Acknowledge and ask about insurance.`,
|
||||||
|
fallback, apiKey
|
||||||
|
)
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
return { reply, nextStage: "asked_existing_insurance" };
|
||||||
|
}
|
||||||
|
|
||||||
async function askAppointmentTimeNode(state: GraphStateType, config: any) {
|
async function askAppointmentTimeNode(state: GraphStateType, config: any) {
|
||||||
const lang = state.language || "English";
|
const lang = state.language || "English";
|
||||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
const fallbacks: Record<string, string> = {
|
const fallbacks: Record<string, string> = {
|
||||||
English: "When would you like to make an appointment?",
|
English: "When would you like to come in for your appointment?",
|
||||||
Spanish: "¿Cuándo le gustaría hacer una cita?",
|
Spanish: "¿Cuándo le gustaría venir para su cita?",
|
||||||
Portuguese: "Quando você gostaria de agendar uma consulta?",
|
Portuguese: "Quando você gostaria de agendar sua consulta?",
|
||||||
Mandarin: "您想什么时候预约?",
|
Mandarin: "您想什么时候来预约?",
|
||||||
Cantonese: "您想幾時預約?",
|
Cantonese: "您想幾時來預約?",
|
||||||
Arabic: "متى تريد تحديد موعد؟",
|
Arabic: "متى تريد تحديد موعد؟",
|
||||||
"Haitian Creole": "Ki lè ou ta renmen fè yon randevou?",
|
"Haitian Creole": "Ki lè ou ta renmen fè yon randevou?",
|
||||||
};
|
};
|
||||||
@@ -256,7 +362,7 @@ async function askAppointmentTimeNode(state: GraphStateType, config: any) {
|
|||||||
|
|
||||||
const reply = apiKey
|
const reply = apiKey
|
||||||
? await llmReply(
|
? await llmReply(
|
||||||
`You are a friendly dental office AI assistant. Ask the patient in ${lang} when they would like to schedule their appointment. One sentence, no formatting.`,
|
`You are a friendly dental office AI assistant. Ask the patient in ${lang} what date and time they would prefer for their appointment. One sentence, no formatting.`,
|
||||||
`Ask when to schedule.`,
|
`Ask when to schedule.`,
|
||||||
fallback, apiKey
|
fallback, apiKey
|
||||||
)
|
)
|
||||||
@@ -291,8 +397,6 @@ async function acknowledgeAppointmentTimeNode(state: GraphStateType, config: any
|
|||||||
return { reply, nextStage: "done" };
|
return { reply, nextStage: "done" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Post-Selenium: appointment preference (MassHealth ACTIVE) ─────────────────
|
|
||||||
|
|
||||||
async function handleAppointmentPreferenceNode(state: GraphStateType, config: any) {
|
async function handleAppointmentPreferenceNode(state: GraphStateType, config: any) {
|
||||||
const lang = state.language || "English";
|
const lang = state.language || "English";
|
||||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
@@ -319,14 +423,11 @@ async function handleAppointmentPreferenceNode(state: GraphStateType, config: an
|
|||||||
return { reply, nextStage: "asked_appointment_time" };
|
return { reply, nextStage: "asked_appointment_time" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Post-Selenium: self-pay offer (MassHealth INACTIVE) ───────────────────────
|
|
||||||
|
|
||||||
async function handleSelfPayNode(state: GraphStateType, config: any) {
|
async function handleSelfPayNode(state: GraphStateType, config: any) {
|
||||||
const lang = state.language || "English";
|
const lang = state.language || "English";
|
||||||
const apiKey: string | undefined = config?.configurable?.apiKey;
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
const text = state.message.toLowerCase();
|
const text = state.message.toLowerCase();
|
||||||
|
|
||||||
// Classify yes/no for self-pay
|
|
||||||
const acceptsSelfPay = /yes|sure|ok|okay|yep|yeah|sí|si|claro|sim|confirmado|好的|نعم|wi|oke/i.test(text);
|
const acceptsSelfPay = /yes|sure|ok|okay|yep|yeah|sí|si|claro|sim|confirmado|好的|نعم|wi|oke/i.test(text);
|
||||||
const declinesSelfPay = /no|nope|can't|won't|not interested|no puedo|não|لا|pa ka/i.test(text);
|
const declinesSelfPay = /no|nope|can't|won't|not interested|no puedo|não|لا|pa ka/i.test(text);
|
||||||
|
|
||||||
@@ -372,10 +473,61 @@ async function handleSelfPayNode(state: GraphStateType, config: any) {
|
|||||||
return { reply, nextStage: "asked_appointment_time" };
|
return { reply, nextStage: "asked_appointment_time" };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ambiguous — transfer to staff
|
|
||||||
return { reply: transferMsg(lang), nextStage: "done" };
|
return { reply: transferMsg(lang), nextStage: "done" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function askContactInfoNode(state: GraphStateType, config: any) {
|
||||||
|
const lang = state.language || "English";
|
||||||
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
|
const fallbacks: Record<string, string> = {
|
||||||
|
English: "Please leave your name and phone number. Our receptionist will contact you as soon as possible.",
|
||||||
|
Spanish: "Por favor déjenos su nombre y número de teléfono. Nuestra recepcionista se comunicará con usted lo antes posible.",
|
||||||
|
Portuguese: "Por favor deixe seu nome e número de telefone. Nossa recepcionista entrará em contato o mais breve possível.",
|
||||||
|
Mandarin: "请留下您的姓名和电话号码,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "請留下您的姓名和電話號碼,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "يرجى ترك اسمك ورقم هاتفك. ستتواصل معك موظفة الاستقبال في أقرب وقت ممكن.",
|
||||||
|
"Haitian Creole": "Tanpri kite non ou ak nimewo telefòn ou. Resepsyonis nou an pral kontakte ou pi vit posib.",
|
||||||
|
};
|
||||||
|
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||||
|
|
||||||
|
const reply = apiKey
|
||||||
|
? await llmReply(
|
||||||
|
`You are a friendly dental office AI assistant. The patient's MassHealth is inactive and they have no other insurance. In ${lang}, politely ask them to leave their name and phone number so the receptionist can contact them. 1-2 sentences, no formatting.`,
|
||||||
|
`MassHealth inactive, no other insurance. Ask for name and phone.`,
|
||||||
|
fallback, apiKey
|
||||||
|
)
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
return { reply, nextStage: "collecting_contact_info" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acknowledgeContactInfoNode(state: GraphStateType, config: any) {
|
||||||
|
const lang = state.language || "English";
|
||||||
|
const apiKey: string | undefined = config?.configurable?.apiKey;
|
||||||
|
|
||||||
|
const fallbacks: Record<string, string> = {
|
||||||
|
English: "Thank you! Our receptionist will reach out to you shortly.",
|
||||||
|
Spanish: "¡Gracias! Nuestra recepcionista se pondrá en contacto con usted en breve.",
|
||||||
|
Portuguese: "Obrigado! Nossa recepcionista entrará em contato com você em breve.",
|
||||||
|
Mandarin: "谢谢!我们的前台将很快与您联系。",
|
||||||
|
Cantonese: "多謝!我們的接待員將很快與您聯絡。",
|
||||||
|
Arabic: "شكراً! ستتواصل معك موظفة الاستقبال قريباً.",
|
||||||
|
"Haitian Creole": "Mèsi! Resepsyonis nou an pral kontakte ou byento.",
|
||||||
|
};
|
||||||
|
const fallback = fallbacks[lang] ?? fallbacks["English"]!;
|
||||||
|
|
||||||
|
const reply = apiKey
|
||||||
|
? await llmReply(
|
||||||
|
`You are a friendly dental office AI assistant. The patient has left their contact information. Thank them in ${lang} and let them know the receptionist will reach out soon. 1-2 sentences, no formatting.`,
|
||||||
|
`Patient provided contact info. Acknowledge.`,
|
||||||
|
fallback, apiKey
|
||||||
|
)
|
||||||
|
: fallback;
|
||||||
|
|
||||||
|
return { reply, nextStage: "done" };
|
||||||
|
}
|
||||||
|
|
||||||
function transferNode(state: GraphStateType) {
|
function transferNode(state: GraphStateType) {
|
||||||
const lang = state.language || "English";
|
const lang = state.language || "English";
|
||||||
return { reply: state.generalFallback || transferMsg(lang), nextStage: "done" };
|
return { reply: state.generalFallback || transferMsg(lang), nextStage: "done" };
|
||||||
@@ -387,33 +539,45 @@ const graph = new StateGraph(GraphState)
|
|||||||
.addNode("classify", classifyNode)
|
.addNode("classify", classifyNode)
|
||||||
.addNode("ask_new_or_existing", askNewOrExistingNode)
|
.addNode("ask_new_or_existing", askNewOrExistingNode)
|
||||||
.addNode("ask_new_patient_insurance", askNewPatientInsuranceNode)
|
.addNode("ask_new_patient_insurance", askNewPatientInsuranceNode)
|
||||||
.addNode("ask_existing_insurance", askExistingInsuranceNode)
|
.addNode("ask_insurance_type", askInsuranceTypeNode)
|
||||||
|
.addNode("ask_masshealth_check_consent", askMassHealthCheckConsentNode)
|
||||||
.addNode("ask_masshealth_info", askMassHealthInfoNode)
|
.addNode("ask_masshealth_info", askMassHealthInfoNode)
|
||||||
|
.addNode("ask_existing_insurance", askExistingInsuranceNode)
|
||||||
.addNode("ask_appointment_time", askAppointmentTimeNode)
|
.addNode("ask_appointment_time", askAppointmentTimeNode)
|
||||||
.addNode("acknowledge_appointment_time", acknowledgeAppointmentTimeNode)
|
.addNode("acknowledge_appointment_time", acknowledgeAppointmentTimeNode)
|
||||||
.addNode("handle_appointment_preference",handleAppointmentPreferenceNode)
|
.addNode("handle_appointment_preference",handleAppointmentPreferenceNode)
|
||||||
.addNode("handle_self_pay", handleSelfPayNode)
|
.addNode("handle_self_pay", handleSelfPayNode)
|
||||||
|
.addNode("ask_contact_info", askContactInfoNode)
|
||||||
|
.addNode("acknowledge_contact_info", acknowledgeContactInfoNode)
|
||||||
.addNode("transfer", transferNode)
|
.addNode("transfer", transferNode)
|
||||||
.addEdge(START, "classify")
|
.addEdge(START, "classify")
|
||||||
.addConditionalEdges("classify", routeNode, {
|
.addConditionalEdges("classify", routeNode, {
|
||||||
ask_new_or_existing: "ask_new_or_existing",
|
ask_new_or_existing: "ask_new_or_existing",
|
||||||
ask_new_patient_insurance: "ask_new_patient_insurance",
|
ask_new_patient_insurance: "ask_new_patient_insurance",
|
||||||
ask_existing_insurance: "ask_existing_insurance",
|
ask_insurance_type: "ask_insurance_type",
|
||||||
|
ask_masshealth_check_consent: "ask_masshealth_check_consent",
|
||||||
ask_masshealth_info: "ask_masshealth_info",
|
ask_masshealth_info: "ask_masshealth_info",
|
||||||
|
ask_existing_insurance: "ask_existing_insurance",
|
||||||
ask_appointment_time: "ask_appointment_time",
|
ask_appointment_time: "ask_appointment_time",
|
||||||
acknowledge_appointment_time: "acknowledge_appointment_time",
|
acknowledge_appointment_time: "acknowledge_appointment_time",
|
||||||
handle_appointment_preference: "handle_appointment_preference",
|
handle_appointment_preference: "handle_appointment_preference",
|
||||||
handle_self_pay: "handle_self_pay",
|
handle_self_pay: "handle_self_pay",
|
||||||
|
ask_contact_info: "ask_contact_info",
|
||||||
|
acknowledge_contact_info: "acknowledge_contact_info",
|
||||||
transfer: "transfer",
|
transfer: "transfer",
|
||||||
})
|
})
|
||||||
.addEdge("ask_new_or_existing", END)
|
.addEdge("ask_new_or_existing", END)
|
||||||
.addEdge("ask_new_patient_insurance", END)
|
.addEdge("ask_new_patient_insurance", END)
|
||||||
.addEdge("ask_existing_insurance", END)
|
.addEdge("ask_insurance_type", END)
|
||||||
|
.addEdge("ask_masshealth_check_consent", END)
|
||||||
.addEdge("ask_masshealth_info", END)
|
.addEdge("ask_masshealth_info", END)
|
||||||
|
.addEdge("ask_existing_insurance", END)
|
||||||
.addEdge("ask_appointment_time", END)
|
.addEdge("ask_appointment_time", END)
|
||||||
.addEdge("acknowledge_appointment_time", END)
|
.addEdge("acknowledge_appointment_time", END)
|
||||||
.addEdge("handle_appointment_preference", END)
|
.addEdge("handle_appointment_preference", END)
|
||||||
.addEdge("handle_self_pay", END)
|
.addEdge("handle_self_pay", END)
|
||||||
|
.addEdge("ask_contact_info", END)
|
||||||
|
.addEdge("acknowledge_contact_info", END)
|
||||||
.addEdge("transfer", END)
|
.addEdge("transfer", END)
|
||||||
.compile();
|
.compile();
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function getNextWeekDateObjects() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Format "HH:MM" (24-h) → "H:MM am/pm" */
|
/** Format "HH:MM" (24-h) → "H:MM am/pm" */
|
||||||
function timeLabel(hhmm: string): string {
|
export function timeLabel(hhmm: string): string {
|
||||||
const [h, m] = hhmm.split(":").map(Number);
|
const [h, m] = hhmm.split(":").map(Number);
|
||||||
const h12 = (h! % 12) || 12;
|
const h12 = (h! % 12) || 12;
|
||||||
const ampm = (h! >= 12) ? "pm" : "am";
|
const ampm = (h! >= 12) ? "pm" : "am";
|
||||||
@@ -80,7 +80,7 @@ function messageHasTime(msg: string): boolean {
|
|||||||
* Parse only a date (no time) from a message like "5/18" or "next Monday".
|
* Parse only a date (no time) from a message like "5/18" or "next Monday".
|
||||||
* Returns a UTC-midnight Date and a date-only display label, or null.
|
* Returns a UTC-midnight Date and a date-only display label, or null.
|
||||||
*/
|
*/
|
||||||
async function parseDateOnlyFromMessage(
|
export async function parseDateOnlyFromMessage(
|
||||||
message: string,
|
message: string,
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
): Promise<{ date: Date; dateLabel: string } | null> {
|
): Promise<{ date: Date; dateLabel: string } | null> {
|
||||||
@@ -249,7 +249,7 @@ Rules:
|
|||||||
* Returns true if the given date+time is within the office's configured hours.
|
* Returns true if the given date+time is within the office's configured hours.
|
||||||
* Falls back to true (unrestricted) if no hours are configured.
|
* Falls back to true (unrestricted) if no hours are configured.
|
||||||
*/
|
*/
|
||||||
async function isWithinOfficeHours(
|
export async function isWithinOfficeHours(
|
||||||
date: Date,
|
date: Date,
|
||||||
time: string,
|
time: string,
|
||||||
userId: number,
|
userId: number,
|
||||||
@@ -285,7 +285,7 @@ async function isWithinOfficeHours(
|
|||||||
* Returns whether the office is open at all on a given date (day-level check).
|
* Returns whether the office is open at all on a given date (day-level check).
|
||||||
* Also returns the display day name (e.g. "Sunday") for use in error messages.
|
* Also returns the display day name (e.g. "Sunday") for use in error messages.
|
||||||
*/
|
*/
|
||||||
async function isOfficeDayOpen(
|
export async function isOfficeDayOpen(
|
||||||
date: Date,
|
date: Date,
|
||||||
userId: number,
|
userId: number,
|
||||||
): Promise<{ open: boolean; displayDay: string }> {
|
): Promise<{ open: boolean; displayDay: string }> {
|
||||||
@@ -319,7 +319,7 @@ async function isOfficeDayOpen(
|
|||||||
* e.g. "9:00 am – 12:00 pm and 1:00 pm – 5:00 pm".
|
* e.g. "9:00 am – 12:00 pm and 1:00 pm – 5:00 pm".
|
||||||
* Returns null if no hours are configured or the day is closed.
|
* Returns null if no hours are configured or the day is closed.
|
||||||
*/
|
*/
|
||||||
async function getOfficeHoursDisplay(date: Date, userId: number): Promise<string | null> {
|
export async function getOfficeHoursDisplay(date: Date, userId: number): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
const record = await storage.getOfficeHours(userId);
|
const record = await storage.getOfficeHours(userId);
|
||||||
if (!record?.data) return null;
|
if (!record?.data) return null;
|
||||||
@@ -387,7 +387,7 @@ async function isSlotAvailable(
|
|||||||
|
|
||||||
// ── Time parsing (legacy) ─────────────────────────────────────────────────────
|
// ── Time parsing (legacy) ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function parseTime(message: string, apiKey: string): Promise<string | null> {
|
export async function parseTime(message: string, apiKey: string): Promise<string | null> {
|
||||||
const t = message.toLowerCase();
|
const t = message.toLowerCase();
|
||||||
if (/\bmorning\b|mañana|manhã|上午|صباح|maten/i.test(t)) return "09:00";
|
if (/\bmorning\b|mañana|manhã|上午|صباح|maten/i.test(t)) return "09:00";
|
||||||
if (/\bafternoon\b|tarde|après-midi|下午|مساء|aprèmidi/i.test(t)) return "13:00";
|
if (/\bafternoon\b|tarde|après-midi|下午|مساء|aprèmidi/i.test(t)) return "13:00";
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ router.get("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const settings = await storage.getAiSettings(userId);
|
const settings = await storage.getAiSettings(userId);
|
||||||
if (!settings) return res.status(200).json(null);
|
if (!settings) return res.status(200).json(null);
|
||||||
|
|
||||||
return res.status(200).json({ id: settings.id, apiKey: settings.apiKey });
|
return res.status(200).json({
|
||||||
|
id: settings.id,
|
||||||
|
apiKey: settings.apiKey,
|
||||||
|
openPhoneReply: settings.openPhoneReply ?? false,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res.status(500).json({ error: "Failed to fetch AI settings", details: String(err) });
|
return res.status(500).json({ error: "Failed to fetch AI settings", details: String(err) });
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,37 @@ router.put("/settings", async (req: Request, res: Response): Promise<any> => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/ai/advanced-settings
|
||||||
|
router.get("/advanced-settings", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const openPhoneReply = await storage.getOpenPhoneReply(userId);
|
||||||
|
return res.status(200).json({ openPhoneReply });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to fetch advanced settings", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/ai/advanced-settings
|
||||||
|
router.put("/advanced-settings", async (req: Request, res: Response): Promise<any> => {
|
||||||
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||||
|
|
||||||
|
const { openPhoneReply } = req.body;
|
||||||
|
if (typeof openPhoneReply !== "boolean") {
|
||||||
|
return res.status(400).json({ message: "openPhoneReply must be a boolean" });
|
||||||
|
}
|
||||||
|
|
||||||
|
await storage.setOpenPhoneReply(userId, openPhoneReply);
|
||||||
|
return res.status(200).json({ openPhoneReply });
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({ error: "Failed to save advanced settings", details: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// GET /api/ai/chat-templates
|
// GET /api/ai/chat-templates
|
||||||
router.get("/chat-templates", async (req: Request, res: Response): Promise<any> => {
|
router.get("/chat-templates", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,12 +4,21 @@ import { storage } from "../storage";
|
|||||||
import { prisma as db } from "@repo/db/client";
|
import { prisma as db } from "@repo/db/client";
|
||||||
import { runReminderGraph } from "../ai/reminder-graph";
|
import { runReminderGraph } from "../ai/reminder-graph";
|
||||||
import { runNewPatientStep } from "../ai/new-patient-graph";
|
import { runNewPatientStep } from "../ai/new-patient-graph";
|
||||||
import { runRescheduleStep } from "../ai/reschedule-graph";
|
import {
|
||||||
|
runRescheduleStep,
|
||||||
|
parseDateOnlyFromMessage,
|
||||||
|
parseTime,
|
||||||
|
isOfficeDayOpen,
|
||||||
|
isWithinOfficeHours,
|
||||||
|
getOfficeHoursDisplay,
|
||||||
|
timeLabel,
|
||||||
|
} from "../ai/reschedule-graph";
|
||||||
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
|
||||||
import { runEligibilityProcessor } from "../queue/processors/eligibilityProcessor";
|
import { runEligibilityProcessor } from "../queue/processors/eligibilityProcessor";
|
||||||
import {
|
import {
|
||||||
getHandoff, getAfterHoursHandoff,
|
getHandoff, getAfterHoursHandoff,
|
||||||
getStage, setStage,
|
getStage, setStage,
|
||||||
|
setPendingReschedule, getPendingReschedule, clearPendingReschedule,
|
||||||
type ConversationStage,
|
type ConversationStage,
|
||||||
} from "../ai/aiHandoffStore";
|
} from "../ai/aiHandoffStore";
|
||||||
|
|
||||||
@@ -23,6 +32,12 @@ function escapeXml(text: string): string {
|
|||||||
.replace(/"/g, """).replace(/'/g, "'");
|
.replace(/"/g, """).replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Send multiple SMS messages in guaranteed order via a single TwiML response. */
|
||||||
|
function twimlMessages(...texts: string[]): string {
|
||||||
|
const body = texts.map(t => `<Message>${escapeXml(t)}</Message>`).join("");
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?><Response>${body}</Response>`;
|
||||||
|
}
|
||||||
|
|
||||||
function twimlReply(text: string): string {
|
function twimlReply(text: string): string {
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?><Response><Message>${escapeXml(text)}</Message></Response>`;
|
return `<?xml version="1.0" encoding="UTF-8"?><Response><Message>${escapeXml(text)}</Message></Response>`;
|
||||||
}
|
}
|
||||||
@@ -81,6 +96,17 @@ async function saveOutbound(patientId: number, body: string): Promise<void> {
|
|||||||
* Extract MassHealth Member ID and date of birth from a free-text SMS.
|
* Extract MassHealth Member ID and date of birth from a free-text SMS.
|
||||||
* Tries regex first, falls back to LLM extraction.
|
* Tries regex first, falls back to LLM extraction.
|
||||||
*/
|
*/
|
||||||
|
/** Normalize a DOB string to zero-padded MM/DD/YYYY required by MassHealth. */
|
||||||
|
function normalizeDob(raw: string): string {
|
||||||
|
const parts = raw.split(/[\/\-\.]/);
|
||||||
|
if (parts.length !== 3) return raw;
|
||||||
|
const [m, d, y] = parts;
|
||||||
|
const mm = String(parseInt(m!, 10)).padStart(2, "0");
|
||||||
|
const dd = String(parseInt(d!, 10)).padStart(2, "0");
|
||||||
|
const yyyy = y!.length === 2 ? `20${y}` : y!;
|
||||||
|
return `${mm}/${dd}/${yyyy}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function parseMassHealthInfo(
|
async function parseMassHealthInfo(
|
||||||
message: string,
|
message: string,
|
||||||
apiKey: string
|
apiKey: string
|
||||||
@@ -92,7 +118,7 @@ async function parseMassHealthInfo(
|
|||||||
if (idMatch && dobMatch) {
|
if (idMatch && dobMatch) {
|
||||||
const [, m, d, y] = dobMatch;
|
const [, m, d, y] = dobMatch;
|
||||||
const year = y!.length === 2 ? `20${y}` : y;
|
const year = y!.length === 2 ? `20${y}` : y;
|
||||||
return { memberId: idMatch[1]!, dob: `${m}/${d}/${year}` };
|
return { memberId: idMatch[1]!, dob: normalizeDob(`${m}/${d}/${year}`) };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back to LLM structured extraction
|
// Fall back to LLM structured extraction
|
||||||
@@ -109,7 +135,8 @@ async function parseMassHealthInfo(
|
|||||||
]);
|
]);
|
||||||
const raw = String(res.content).replace(/```json|```/g, "").trim();
|
const raw = String(res.content).replace(/```json|```/g, "").trim();
|
||||||
const json = JSON.parse(raw);
|
const json = JSON.parse(raw);
|
||||||
return { memberId: json.memberId ?? null, dob: json.dob ?? null };
|
const dob = json.dob ? normalizeDob(String(json.dob)) : null;
|
||||||
|
return { memberId: json.memberId ?? null, dob };
|
||||||
} catch {
|
} catch {
|
||||||
return { memberId: null, dob: null };
|
return { memberId: null, dob: null };
|
||||||
}
|
}
|
||||||
@@ -156,31 +183,29 @@ async function runMassHealthCheckAndNotify(
|
|||||||
const lang = patient.preferredLanguage || "English";
|
const lang = patient.preferredLanguage || "English";
|
||||||
const active = updated?.status === "ACTIVE";
|
const active = updated?.status === "ACTIVE";
|
||||||
|
|
||||||
// ── ACTIVE: existing patient → simple scheduling; new patient → preference ─
|
// ── ACTIVE ────────────────────────────────────────────────────────────────
|
||||||
const activeMessagesExisting: Record<string, string> = {
|
const activeMessages: Record<string, string> = {
|
||||||
English: "Great news! Your MassHealth coverage is active. When would you like to come in for your appointment?",
|
English: "Great news! Your MassHealth coverage is active. We can schedule an appointment for you! What date and time would you prefer?",
|
||||||
Spanish: "¡Buenas noticias! Su cobertura de MassHealth está activa. ¿Cuándo le gustaría venir para su cita?",
|
Spanish: "¡Buenas noticias! Su cobertura de MassHealth está activa. ¡Podemos programar una cita para usted! ¿Qué fecha y hora prefiere?",
|
||||||
Portuguese: "Ótimas notícias! Sua cobertura MassHealth está ativa. Quando gostaria de vir para sua consulta?",
|
Portuguese: "Ótimas notícias! Sua cobertura MassHealth está ativa. Podemos agendar uma consulta para você! Qual data e horário prefere?",
|
||||||
Mandarin: "好消息!您的MassHealth保险有效。您想什么时候来预约?",
|
Mandarin: "好消息!您的MassHealth保险有效。我们可以为您安排预约!您希望什么日期和时间?",
|
||||||
Cantonese: "好消息!您的MassHealth保險有效。您想幾時來預約?",
|
Cantonese: "好消息!您的MassHealth保險有效。我們可以為您安排預約!您希望什麼日期和時間?",
|
||||||
Arabic: "أخبار رائعة! تغطيتك من MassHealth نشطة. متى تودّ الحضور لموعدك؟",
|
Arabic: "أخبار رائعة! تغطيتك من MassHealth نشطة. يمكننا تحديد موعد لك! ما التاريخ والوقت المفضل لديك؟",
|
||||||
"Haitian Creole": "Bon nouvèl! Asirans MassHealth ou aktif. Ki lè ou ta renmen vini pou randevou ou?",
|
"Haitian Creole": "Bon nouvèl! Asirans MassHealth ou aktif. Nou ka planifye yon randevou pou ou! Ki dat ak lè ou prefere?",
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeMessagesNew: Record<string, string> = {
|
// ── INACTIVE: new patient → ask other insurance; existing → ask self-pay ──
|
||||||
English: "Great news! Your MassHealth coverage is active. When would you like to come in? Are you looking for a routine check-up and teeth cleaning, or do you have a tooth problem or pain?",
|
const inactiveMessagesNew: Record<string, string> = {
|
||||||
Spanish: "¡Buenas noticias! Su cobertura de MassHealth está activa. ¿Cuándo le gustaría venir? ¿Busca una revisión rutinaria y limpieza dental, o tiene algún problema dental o dolor?",
|
English: "Unfortunately, your MassHealth coverage appears to be inactive. Do you have any other insurance?",
|
||||||
Portuguese: "Ótimas notícias! Sua cobertura MassHealth está ativa. Quando gostaria de vir? Você busca uma consulta de rotina e limpeza, ou tem algum problema dentário ou dor?",
|
Spanish: "Lamentablemente, su cobertura de MassHealth parece estar inactiva. ¿Tiene algún otro seguro?",
|
||||||
Mandarin: "好消息!您的MassHealth保险有效。您想什么时候来?您是想做常规检查和洗牙,还是您有牙齿问题或疼痛?",
|
Portuguese: "Infelizmente, sua cobertura MassHealth parece estar inativa. Você tem algum outro plano de saúde?",
|
||||||
Cantonese: "好消息!您的MassHealth保險有效。您想幾時來?您是想做例行檢查和洗牙,還是您有牙齒問題或疼痛?",
|
Mandarin: "很遗憾,您的MassHealth保险似乎无效。您还有其他保险吗?",
|
||||||
Arabic: "أخبار رائعة! تغطيتك من MassHealth نشطة. متى تودّ الحضور؟ هل تبحث عن فحص روتيني وتنظيف أسنان، أم أن لديك مشكلة في الأسنان أو ألماً؟",
|
Cantonese: "很遺憾,您的MassHealth保險似乎無效。您還有其他保險嗎?",
|
||||||
"Haitian Creole": "Bon nouvèl! Asirans MassHealth ou aktif. Ki lè ou ta renmen vini? Èske ou ap chèche yon egzamen woutin ak netwayaj dan, oswa ou gen pwoblèm dan oswa doulè?",
|
Arabic: "للأسف، تغطيتك من MassHealth تبدو غير نشطة. هل لديك أي تأمين آخر؟",
|
||||||
|
"Haitian Creole": "Malerezman, kouvèti MassHealth ou parèt inaktif. Èske ou gen yon lòt asirans?",
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeMessages = isExistingPatient ? activeMessagesExisting : activeMessagesNew;
|
const inactiveMessagesExisting: Record<string, string> = {
|
||||||
|
|
||||||
// ── INACTIVE: offer self-pay examination ────────────────────────────
|
|
||||||
const inactiveMessages: Record<string, string> = {
|
|
||||||
English: "We checked your MassHealth coverage. Unfortunately the plan appears inactive or could not be verified. Would you still like to schedule an examination appointment as a self-pay patient?",
|
English: "We checked your MassHealth coverage. Unfortunately the plan appears inactive or could not be verified. Would you still like to schedule an examination appointment as a self-pay patient?",
|
||||||
Spanish: "Verificamos su cobertura de MassHealth. Lamentablemente el plan aparece inactivo o no pudo ser verificado. ¿Le gustaría programar una cita de examen como paciente de pago particular?",
|
Spanish: "Verificamos su cobertura de MassHealth. Lamentablemente el plan aparece inactivo o no pudo ser verificado. ¿Le gustaría programar una cita de examen como paciente de pago particular?",
|
||||||
Portuguese: "Verificamos sua cobertura MassHealth. Infelizmente o plano parece inativo ou não pôde ser verificado. Gostaria de agendar uma consulta de exame como paciente particular?",
|
Portuguese: "Verificamos sua cobertura MassHealth. Infelizmente o plano parece inativo ou não pôde ser verificado. Gostaria de agendar uma consulta de exame como paciente particular?",
|
||||||
@@ -191,12 +216,16 @@ async function runMassHealthCheckAndNotify(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resultText = active
|
const resultText = active
|
||||||
? (activeMessages[lang] ?? activeMessages["English"]!)
|
? (activeMessages[lang] ?? activeMessages["English"]!)
|
||||||
: (inactiveMessages[lang] ?? inactiveMessages["English"]!);
|
: isExistingPatient
|
||||||
|
? (inactiveMessagesExisting[lang] ?? inactiveMessagesExisting["English"]!)
|
||||||
|
: (inactiveMessagesNew[lang] ?? inactiveMessagesNew["English"]!);
|
||||||
|
|
||||||
const nextStage: ConversationStage = active
|
const nextStage: ConversationStage = active
|
||||||
? (isExistingPatient ? "asked_appointment_time" : "asked_appointment_preference")
|
? "asked_appointment_time"
|
||||||
: "asked_self_pay";
|
: isExistingPatient
|
||||||
|
? "asked_self_pay"
|
||||||
|
: "asked_other_insurance_after_inactive";
|
||||||
|
|
||||||
// Send follow-up question via Twilio
|
// Send follow-up question via Twilio
|
||||||
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
|
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
|
||||||
@@ -215,11 +244,64 @@ async function runMassHealthCheckAndNotify(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Empathetic one-liner (instant keyword-based, no API latency) ─────────────
|
||||||
|
|
||||||
|
function getEmpatheticAck(message: string, language: string): string {
|
||||||
|
const hasPain = /pain|hurt|ache|toothache|emergency|urgent|asap|bleeding|broke|crack|fell out|swollen|infection|abscess/i.test(message);
|
||||||
|
|
||||||
|
const painMsgs: Record<string, string> = {
|
||||||
|
English: "Sorry to hear that! We are here to help you.",
|
||||||
|
Spanish: "¡Lo sentimos! Estamos aquí para ayudarle.",
|
||||||
|
Portuguese: "Lamentamos isso! Estamos aqui para ajudá-lo.",
|
||||||
|
Mandarin: "很遗憾听到这个消息!我们在这里帮助您。",
|
||||||
|
Cantonese: "很遺憾聽到這個消息!我們在這裡幫助您。",
|
||||||
|
Arabic: "آسف لسماع ذلك! نحن هنا لمساعدتك.",
|
||||||
|
"Haitian Creole": "Nou regrèt tande sa! Nou la pou ede ou.",
|
||||||
|
};
|
||||||
|
const normalMsgs: Record<string, string> = {
|
||||||
|
English: "No problem!",
|
||||||
|
Spanish: "¡Sin problema!",
|
||||||
|
Portuguese: "Sem problema!",
|
||||||
|
Mandarin: "没问题!",
|
||||||
|
Cantonese: "沒問題!",
|
||||||
|
Arabic: "لا مشكلة!",
|
||||||
|
"Haitian Creole": "Pa gen pwoblèm!",
|
||||||
|
};
|
||||||
|
|
||||||
|
return hasPain
|
||||||
|
? (painMsgs[language] ?? painMsgs["English"]!)
|
||||||
|
: (normalMsgs[language] ?? normalMsgs["English"]!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── "New appointment or reschedule?" prompt (multilingual) ────────────────────
|
||||||
|
|
||||||
|
const NEW_OR_RESCHEDULE_Q: Record<string, string> = {
|
||||||
|
English: "Just to confirm — would you like to make a new appointment, or would you like to reschedule your current appointment?",
|
||||||
|
Spanish: "Solo para confirmar — ¿desea hacer una nueva cita o reprogramar su cita actual?",
|
||||||
|
Portuguese: "Só para confirmar — você gostaria de marcar uma nova consulta ou reagendar sua consulta atual?",
|
||||||
|
Mandarin: "请问您是想预约新的就诊,还是想更改现有预约?",
|
||||||
|
Cantonese: "請問您是想預約新的就診,還是想更改現有預約?",
|
||||||
|
Arabic: "فقط للتأكيد — هل تريد تحديد موعد جديد أم تعديل موعدك الحالي؟",
|
||||||
|
"Haitian Creole": "Jis pou konfime — èske ou ta renmen pran yon nouvo randevou, oswa èske ou ta renmen reprogramè randevou aktyèl ou?",
|
||||||
|
};
|
||||||
|
|
||||||
// ── POST /api/twilio/webhook/sms ──────────────────────────────────────────────
|
// ── POST /api/twilio/webhook/sms ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
const CONVO_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
||||||
|
|
||||||
|
// In-memory conversation state for unknown phone numbers (no patient record yet).
|
||||||
|
// Keyed by normalized "From" number.
|
||||||
|
const unknownPhoneConvos = new Map<string, {
|
||||||
|
userId: number;
|
||||||
|
stage: ConversationStage;
|
||||||
|
language: string;
|
||||||
|
lastActivityAt: Date;
|
||||||
|
pendingApptDate?: { date: Date; dateLabel: string };
|
||||||
|
}>();
|
||||||
|
|
||||||
router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> => {
|
router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
const { From, Body, MessageSid } = req.body;
|
const { From, To, Body, MessageSid } = req.body;
|
||||||
|
|
||||||
const normalizedFrom = (From || "").replace(/\D/g, "");
|
const normalizedFrom = (From || "").replace(/\D/g, "");
|
||||||
const allPatients = await db.patient.findMany({
|
const allPatients = await db.patient.findMany({
|
||||||
@@ -230,6 +312,262 @@ router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> =>
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!patient) {
|
if (!patient) {
|
||||||
|
// Unknown number — look up office by the Twilio "To" number
|
||||||
|
const normalizedTo = (To || "").replace(/\D/g, "");
|
||||||
|
const twilioRow = await db.twilioSettings.findFirst({
|
||||||
|
where: { phoneNumber: { contains: normalizedTo.slice(-10) } },
|
||||||
|
select: { userId: true },
|
||||||
|
});
|
||||||
|
if (!twilioRow) {
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = twilioRow.userId;
|
||||||
|
const openPhoneReply = await storage.getOpenPhoneReply(userId);
|
||||||
|
if (!openPhoneReply) {
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch required context for this office
|
||||||
|
const aiSettings = await storage.getAiSettings(userId);
|
||||||
|
if (!aiSettings?.apiKey) {
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
const chatTemplates = await storage.getAiChatTemplates(userId);
|
||||||
|
const officeContact = await storage.getOfficeContact(userId);
|
||||||
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
const convo = unknownPhoneConvos.get(normalizedFrom);
|
||||||
|
let stage = convo?.stage ?? "initial";
|
||||||
|
const language = convo?.language ?? "English";
|
||||||
|
|
||||||
|
// Reset conversation if idle for more than 5 minutes
|
||||||
|
if (
|
||||||
|
stage !== "initial" && stage !== "done" &&
|
||||||
|
convo?.lastActivityAt &&
|
||||||
|
Date.now() - convo.lastActivityAt.getTime() > CONVO_TIMEOUT_MS
|
||||||
|
) {
|
||||||
|
stage = "initial";
|
||||||
|
unknownPhoneConvos.set(normalizedFrom, { userId, stage: "initial", language, lastActivityAt: new Date() });
|
||||||
|
}
|
||||||
|
|
||||||
|
const replyUnknown = (
|
||||||
|
text: string,
|
||||||
|
nextStage: ConversationStage,
|
||||||
|
pendingApptDate?: { date: Date; dateLabel: string },
|
||||||
|
) => {
|
||||||
|
unknownPhoneConvos.set(normalizedFrom, {
|
||||||
|
userId, stage: nextStage, language,
|
||||||
|
lastActivityAt: new Date(),
|
||||||
|
...(pendingApptDate ? { pendingApptDate } : { pendingApptDate: convo?.pendingApptDate }),
|
||||||
|
});
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(twimlReply(text));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stage === "initial" || stage === "done") {
|
||||||
|
// MSG 1: AI intro
|
||||||
|
const rawGreeting = chatTemplates.newPatientGreeting ||
|
||||||
|
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
|
||||||
|
const introText = applyOfficeName(rawGreeting, officeName);
|
||||||
|
|
||||||
|
// MSG 2: Empathetic acknowledgment
|
||||||
|
const empatheticText = getEmpatheticAck(Body, language);
|
||||||
|
|
||||||
|
// MSG 3: Detect intent from first message
|
||||||
|
const isReschedule = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
||||||
|
let msg3Unk: string;
|
||||||
|
let nextStage3Unk: ConversationStage;
|
||||||
|
|
||||||
|
if (isReschedule) {
|
||||||
|
const notFoundMsgs: Record<string, string> = {
|
||||||
|
English: "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
|
||||||
|
Spanish: "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
|
||||||
|
Portuguese: "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
|
||||||
|
Mandarin: "我找不到与您号码关联的预约。请留下您的姓名和联系电话,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
|
||||||
|
"Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
|
||||||
|
};
|
||||||
|
msg3Unk = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
|
||||||
|
nextStage3Unk = "collecting_contact_info";
|
||||||
|
} else {
|
||||||
|
const newOrExistingMsgs: Record<string, string> = {
|
||||||
|
English: "Can you please tell me if you are a new or existing patient?",
|
||||||
|
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
||||||
|
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
||||||
|
Mandarin: "请问您是新患者还是现有患者?",
|
||||||
|
Cantonese: "請問您是新病人還是現有病人?",
|
||||||
|
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
||||||
|
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
||||||
|
};
|
||||||
|
msg3Unk = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
|
||||||
|
nextStage3Unk = "asked_new_or_existing";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update store then send all 3 in one TwiML response (guaranteed order)
|
||||||
|
unknownPhoneConvos.set(normalizedFrom, {
|
||||||
|
userId, stage: nextStage3Unk, language, lastActivityAt: new Date(),
|
||||||
|
});
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(twimlMessages(introText, empatheticText, msg3Unk));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown: asked_new_or_reschedule (fallback for in-progress conversations) ──
|
||||||
|
if (stage === "asked_new_or_reschedule") {
|
||||||
|
const isReschedule = /reschedule|rescheduler|change|modify|move|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
||||||
|
if (isReschedule) {
|
||||||
|
const notFoundMsgs: Record<string, string> = {
|
||||||
|
English: "I couldn't find an appointment associated with your number. Please leave your name and a good callback number and our receptionist will assist you as soon as possible.",
|
||||||
|
Spanish: "No encontré una cita asociada a su número. Por favor deje su nombre y un número de contacto y nuestra recepcionista le atenderá lo antes posible.",
|
||||||
|
Portuguese: "Não encontrei uma consulta associada ao seu número. Por favor deixe seu nome e um número de contato e nossa recepcionista lhe atenderá o mais breve possível.",
|
||||||
|
Mandarin: "我找不到与您号码关联的预约。请留下您的姓名和联系电话,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "我找不到與您號碼關聯的預約。請留下您的姓名和聯絡電話,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "لم أجد موعداً مرتبطاً برقمك. يرجى ترك اسمك ورقم هاتف للتواصل وسيساعدك موظف الاستقبال في أقرب وقت.",
|
||||||
|
"Haitian Creole": "Mwen pa jwenn yon randevou ki asosye ak nimewo ou. Tanpri kite non ou ak yon nimewo pou rele epi resepsyonis nou an pral ede ou pi vit posib.",
|
||||||
|
};
|
||||||
|
return replyUnknown(notFoundMsgs[language] ?? notFoundMsgs["English"]!, "collecting_contact_info");
|
||||||
|
}
|
||||||
|
const newOrExistingMsgs: Record<string, string> = {
|
||||||
|
English: "Can you please tell me if you are a new or existing patient?",
|
||||||
|
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
||||||
|
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
||||||
|
Mandarin: "请问您是新患者还是现有患者?",
|
||||||
|
Cantonese: "請問您是新病人還是現有病人?",
|
||||||
|
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
||||||
|
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
||||||
|
};
|
||||||
|
return replyUnknown(newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!, "asked_new_or_existing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown: asked_appointment_time → parse date, check office hours ──
|
||||||
|
if (stage === "asked_appointment_time") {
|
||||||
|
const parsedDate = await parseDateOnlyFromMessage(Body, aiSettings.apiKey);
|
||||||
|
if (!parsedDate) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: "I didn't catch that. What day would you prefer? For example: 'May 28', 'next Monday', or '5/28'.",
|
||||||
|
Spanish: "No entendí. ¿Qué día prefiere? Por ejemplo: '28 de mayo', 'próximo lunes' o '28/5'.",
|
||||||
|
Portuguese: "Não entendi. Que dia você prefere? Por exemplo: '28 de maio', 'próxima segunda' ou '28/5'.",
|
||||||
|
Mandarin: "我没听清。您希望哪天?例如:'5月28日'、'下周一'或'5/28'。",
|
||||||
|
Cantonese: "我沒聽清。您希望哪天?例如:'5月28日'、'下週一'或'5/28'。",
|
||||||
|
Arabic: "لم أفهم. ما اليوم الذي تفضله؟ مثلاً: '28 مايو' أو '5/28'.",
|
||||||
|
"Haitian Creole": "Mwen pa konprann. Ki jou ou prefere? Pa egzanp: '28 me', 'Lendi pwochèn', oswa '5/28'.",
|
||||||
|
};
|
||||||
|
return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
||||||
|
}
|
||||||
|
const { date, dateLabel } = parsedDate;
|
||||||
|
const dayCheck = await isOfficeDayOpen(date, userId);
|
||||||
|
if (!dayCheck.open) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: `Our office is closed on ${dateLabel} (${dayCheck.displayDay}). Can you please choose another day?`,
|
||||||
|
Spanish: `Nuestra oficina está cerrada el ${dateLabel} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
|
||||||
|
Portuguese: `Nosso consultório está fechado em ${dateLabel} (${dayCheck.displayDay}). Pode escolher outro dia?`,
|
||||||
|
Mandarin: `我们诊所在 ${dateLabel}(${dayCheck.displayDay})不开放。请您选择另一天好吗?`,
|
||||||
|
Cantonese: `我們診所在 ${dateLabel}(${dayCheck.displayDay})不開放。請您選擇另一天好嗎?`,
|
||||||
|
Arabic: `مكتبنا مغلق في ${dateLabel} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
|
||||||
|
"Haitian Creole": `Biwo nou fèmen nan ${dateLabel} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
|
||||||
|
};
|
||||||
|
return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
||||||
|
}
|
||||||
|
const askTimeMsgs: Record<string, string> = {
|
||||||
|
English: `What time do you prefer on ${dateLabel}?`,
|
||||||
|
Spanish: `¿A qué hora prefiere el ${dateLabel}?`,
|
||||||
|
Portuguese: `Que horário você prefere em ${dateLabel}?`,
|
||||||
|
Mandarin: `您希望在 ${dateLabel} 几点?`,
|
||||||
|
Cantonese: `您希望在 ${dateLabel} 幾點?`,
|
||||||
|
Arabic: `ما الوقت الذي تفضله في ${dateLabel}؟`,
|
||||||
|
"Haitian Creole": `Ki lè ou prefere nan ${dateLabel}?`,
|
||||||
|
};
|
||||||
|
return replyUnknown(
|
||||||
|
askTimeMsgs[language] ?? askTimeMsgs["English"]!,
|
||||||
|
"asked_new_appt_time_for_date",
|
||||||
|
{ date, dateLabel },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unknown: asked_new_appt_time_for_date → parse time, check hours ───
|
||||||
|
if (stage === "asked_new_appt_time_for_date") {
|
||||||
|
const pendingApptDate = convo?.pendingApptDate;
|
||||||
|
if (!pendingApptDate) {
|
||||||
|
return replyUnknown(
|
||||||
|
"I lost track of the date. What day would you prefer?",
|
||||||
|
"asked_appointment_time",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const startTime = await parseTime(Body, aiSettings.apiKey);
|
||||||
|
if (!startTime) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: `I didn't catch the time. What time do you prefer on ${pendingApptDate.dateLabel}? For example: '10am' or '2pm'.`,
|
||||||
|
Spanish: `No entendí la hora. ¿Qué hora prefiere el ${pendingApptDate.dateLabel}? Por ejemplo: '10am' o '2pm'.`,
|
||||||
|
Portuguese: `Não entendi o horário. Que hora você prefere em ${pendingApptDate.dateLabel}? Por exemplo: '10h' ou '14h'.`,
|
||||||
|
Mandarin: `我没听清时间。您在 ${pendingApptDate.dateLabel} 几点?例如:上午10点或下午2点。`,
|
||||||
|
Cantonese: `我沒聽清時間。您在 ${pendingApptDate.dateLabel} 幾點?例如:上午10點或下午2點。`,
|
||||||
|
Arabic: `لم أفهم الوقت. ما الوقت الذي تفضله في ${pendingApptDate.dateLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
|
||||||
|
"Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pendingApptDate.dateLabel}? Pa egzanp: 10am oswa 2pm.`,
|
||||||
|
};
|
||||||
|
return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
const withinHours = await isWithinOfficeHours(pendingApptDate.date, startTime, userId);
|
||||||
|
if (!withinHours) {
|
||||||
|
const hoursDisplay = await getOfficeHoursDisplay(pendingApptDate.date, userId);
|
||||||
|
const timeLbl = timeLabel(startTime);
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: hoursDisplay
|
||||||
|
? `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
|
||||||
|
: `Our office is not available at ${timeLbl} on ${pendingApptDate.dateLabel}. What other time do you prefer?`,
|
||||||
|
Spanish: hoursDisplay
|
||||||
|
? `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
|
||||||
|
: `Nuestra oficina no está disponible a las ${timeLbl} el ${pendingApptDate.dateLabel}. ¿Qué otro horario prefiere?`,
|
||||||
|
Portuguese: hoursDisplay
|
||||||
|
? `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
|
||||||
|
: `Nosso consultório não está disponível às ${timeLbl} em ${pendingApptDate.dateLabel}. Que outro horário você prefere?`,
|
||||||
|
Mandarin: hoursDisplay
|
||||||
|
? `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间?`
|
||||||
|
: `我们诊所在 ${pendingApptDate.dateLabel} ${timeLbl} 不开放。您希望改什么时间?`,
|
||||||
|
Cantonese: hoursDisplay
|
||||||
|
? `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間?`
|
||||||
|
: `我們診所在 ${pendingApptDate.dateLabel} ${timeLbl} 不開放。您希望改什麼時間?`,
|
||||||
|
Arabic: hoursDisplay
|
||||||
|
? `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
|
||||||
|
: `مكتبنا غير متاح في ${timeLbl} يوم ${pendingApptDate.dateLabel}. ما وقت آخر تفضله؟`,
|
||||||
|
"Haitian Creole": hoursDisplay
|
||||||
|
? `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
|
||||||
|
: `Biwo nou pa disponib a ${timeLbl} nan ${pendingApptDate.dateLabel}. Ki lòt lè ou prefere?`,
|
||||||
|
};
|
||||||
|
return replyUnknown(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
const apptLabel = `${pendingApptDate.dateLabel} at ${timeLabel(startTime)}`;
|
||||||
|
const confirmMsgs: Record<string, string> = {
|
||||||
|
English: `Thank you! Your preferred appointment is ${apptLabel}. Our receptionist will confirm the details with you shortly.`,
|
||||||
|
Spanish: `¡Gracias! Su cita preferida es el ${apptLabel}. Nuestra recepcionista confirmará los detalles con usted en breve.`,
|
||||||
|
Portuguese: `Obrigado! Sua consulta preferida é em ${apptLabel}. Nossa recepcionista confirmará os detalhes em breve.`,
|
||||||
|
Mandarin: `谢谢!您的预约时间为 ${apptLabel}。我们的前台将很快与您确认详情。`,
|
||||||
|
Cantonese: `多謝!您的預約時間為 ${apptLabel}。我們的接待員將很快與您確認詳情。`,
|
||||||
|
Arabic: `شكراً! موعدك المفضل هو ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيد التفاصيل.`,
|
||||||
|
"Haitian Creole": `Mèsi! Randevou ou a se ${apptLabel}. Resepsyonis nou an pral konfime detay yo ak ou byento.`,
|
||||||
|
};
|
||||||
|
return replyUnknown(confirmMsgs[language] ?? confirmMsgs["English"]!, "done");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multi-step new patient stages for unknown numbers
|
||||||
|
const unknownNewPatientStages: ConversationStage[] = [
|
||||||
|
"new_patient_greeted", "asked_new_or_existing",
|
||||||
|
"asked_new_patient_insurance", "asked_insurance_type",
|
||||||
|
"asked_masshealth_check_consent", "asked_existing_insurance",
|
||||||
|
"asked_appointment_preference",
|
||||||
|
"asked_self_pay", "asked_other_insurance_after_inactive",
|
||||||
|
"collecting_contact_info",
|
||||||
|
];
|
||||||
|
if (unknownNewPatientStages.includes(stage)) {
|
||||||
|
const { reply: aiReply, nextStage } = await runNewPatientStep(
|
||||||
|
Body, stage, language, aiSettings.apiKey, chatTemplates.generalFallback
|
||||||
|
);
|
||||||
|
return replyUnknown(aiReply, nextStage);
|
||||||
|
}
|
||||||
|
|
||||||
res.set("Content-Type", "text/xml");
|
res.set("Content-Type", "text/xml");
|
||||||
return res.send(empty());
|
return res.send(empty());
|
||||||
}
|
}
|
||||||
@@ -252,9 +590,22 @@ router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> =>
|
|||||||
return res.send(empty());
|
return res.send(empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
const language = patient.preferredLanguage || "English";
|
const language = patient.preferredLanguage || "English";
|
||||||
const stage = await getStage(patient.userId, patient.id);
|
let stage = await getStage(patient.userId, patient.id);
|
||||||
const chatTemplates = await storage.getAiChatTemplates(patient.userId);
|
const chatTemplates = await storage.getAiChatTemplates(patient.userId);
|
||||||
|
|
||||||
|
// Reset conversation if idle for more than 5 minutes
|
||||||
|
if (stage !== "initial" && stage !== "done") {
|
||||||
|
const convRow = await db.patientConversation.findUnique({
|
||||||
|
where: { patientId: patient.id },
|
||||||
|
select: { updatedAt: true },
|
||||||
|
});
|
||||||
|
if (convRow?.updatedAt && Date.now() - convRow.updatedAt.getTime() > CONVO_TIMEOUT_MS) {
|
||||||
|
clearPendingReschedule(patient.userId, patient.id);
|
||||||
|
await setStage(patient.userId, patient.id, "initial");
|
||||||
|
stage = "initial";
|
||||||
|
}
|
||||||
|
}
|
||||||
const officeContact = await storage.getOfficeContact(patient.userId);
|
const officeContact = await storage.getOfficeContact(patient.userId);
|
||||||
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
const officeName = (officeContact as any)?.officeName?.trim() || "";
|
||||||
|
|
||||||
@@ -454,12 +805,200 @@ router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> =>
|
|||||||
// Not MassHealth or said NO — fall through to normal graph handling
|
// Not MassHealth or said NO — fall through to normal graph handling
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Stage: asked_new_or_reschedule ────────────────────────────────────
|
||||||
|
if (stage === "asked_new_or_reschedule") {
|
||||||
|
const isReschedule = /reschedule|rescheduler|change|modify|move|different|reprogramar|reagendar|cambiar|mudar|\b2\b/i.test(Body);
|
||||||
|
const isNew = /new.*appoint|make.*appoint|book.*appoint|new patient|first.*time|nueva cita|nova consulta|\b1\b/i.test(Body);
|
||||||
|
|
||||||
|
if (isReschedule) {
|
||||||
|
const apptDatetime = await getAppointmentDatetime(patient.id);
|
||||||
|
if (apptDatetime) {
|
||||||
|
const foundMsgs: Record<string, string> = {
|
||||||
|
English: `Ok. Just to confirm, your current appointment is on ${apptDatetime}. When would you like to reschedule?`,
|
||||||
|
Spanish: `De acuerdo. Solo para confirmar, su cita actual es el ${apptDatetime}. ¿Cuándo le gustaría reprogramarla?`,
|
||||||
|
Portuguese: `Ok. Só para confirmar, sua consulta atual é em ${apptDatetime}. Quando você gostaria de reagendar?`,
|
||||||
|
Mandarin: `好的。请确认一下,您当前的预约是 ${apptDatetime}。您想重新安排到什么时候?`,
|
||||||
|
Cantonese: `好的。請確認一下,您目前的預約是 ${apptDatetime}。您想重新安排到什麼時候?`,
|
||||||
|
Arabic: `حسناً. فقط للتأكيد، موعدك الحالي في ${apptDatetime}. متى تريد إعادة الجدولة؟`,
|
||||||
|
"Haitian Creole": `Oke. Jis pou konfime, randevou aktyèl ou a se ${apptDatetime}. Ki lè ou ta renmen reprogramè?`,
|
||||||
|
};
|
||||||
|
return reply(foundMsgs[language] ?? foundMsgs["English"]!, "asked_reschedule_datetime");
|
||||||
|
} else {
|
||||||
|
const notFoundMsgs: Record<string, string> = {
|
||||||
|
English: "Ok. I could not find your current appointment. Please tell me when you'd like to reschedule and our receptionist will contact you as soon as possible.",
|
||||||
|
Spanish: "De acuerdo. No pude encontrar su cita actual. Por favor díganos cuándo le gustaría reprogramar y nuestra recepcionista le contactará lo antes posible.",
|
||||||
|
Portuguese: "Ok. Não consegui encontrar sua consulta atual. Por favor diga-nos quando você gostaria de reagendar e nossa recepcionista entrará em contato o mais breve possível.",
|
||||||
|
Mandarin: "好的。我找不到您当前的预约。请告诉我您希望重新安排的时间,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "好的。我找不到您目前的預約。請告訴我您希望重新安排的時間,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "حسناً. لم أجد موعدك الحالي. يرجى إخبارنا بالوقت الذي تريد إعادة الجدولة إليه وسيتصل بك موظف الاستقبال في أقرب وقت ممكن.",
|
||||||
|
"Haitian Creole": "Oke. Mwen pa t jwenn randevou aktyèl ou. Tanpri di nou ki lè ou ta renmen reprogramè epi resepsyonis nou an pral kontakte ou pi vit posib.",
|
||||||
|
};
|
||||||
|
return reply(notFoundMsgs[language] ?? notFoundMsgs["English"]!, "done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "new appointment" or ambiguous → ask new/existing patient
|
||||||
|
const newApptMsgs: Record<string, string> = {
|
||||||
|
English: "No problem! To get started, are you an existing patient or a new patient?",
|
||||||
|
Spanish: "¡Sin problema! Para comenzar, ¿es usted un paciente existente o un paciente nuevo?",
|
||||||
|
Portuguese: "Sem problema! Para começar, você é um paciente existente ou um novo paciente?",
|
||||||
|
Mandarin: "没问题!请问您是现有患者还是新患者?",
|
||||||
|
Cantonese: "沒問題!請問您是現有病人還是新病人?",
|
||||||
|
Arabic: "لا مشكلة! للبدء، هل أنت مريض حالي أم مريض جديد؟",
|
||||||
|
"Haitian Creole": "Pa gen pwoblèm! Pou kòmanse, èske ou se yon pasyan egzistan oswa yon nouvo pasyan?",
|
||||||
|
};
|
||||||
|
return reply(newApptMsgs[language] ?? newApptMsgs["English"]!, "asked_new_or_existing");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stage: asked_appointment_time → parse date, check office hours ───
|
||||||
|
if (stage === "asked_appointment_time") {
|
||||||
|
const parsedDate = await parseDateOnlyFromMessage(Body, aiSettings.apiKey);
|
||||||
|
if (!parsedDate) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: "I didn't catch that. What day would you prefer? For example: 'May 28', 'next Monday', or '5/28'.",
|
||||||
|
Spanish: "No entendí. ¿Qué día prefiere? Por ejemplo: '28 de mayo', 'próximo lunes' o '28/5'.",
|
||||||
|
Portuguese: "Não entendi. Que dia você prefere? Por exemplo: '28 de maio', 'próxima segunda' ou '28/5'.",
|
||||||
|
Mandarin: "我没听清。您希望哪天?例如:'5月28日'、'下周一'或'5/28'。",
|
||||||
|
Cantonese: "我沒聽清。您希望哪天?例如:'5月28日'、'下週一'或'5/28'。",
|
||||||
|
Arabic: "لم أفهم. ما اليوم الذي تفضله؟ مثلاً: '28 مايو' أو '5/28'.",
|
||||||
|
"Haitian Creole": "Mwen pa konprann. Ki jou ou prefere? Pa egzanp: '28 me', 'Lendi pwochèn', oswa '5/28'.",
|
||||||
|
};
|
||||||
|
return reply(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
||||||
|
}
|
||||||
|
const { date, dateLabel } = parsedDate;
|
||||||
|
const dayCheck = await isOfficeDayOpen(date, patient.userId);
|
||||||
|
if (!dayCheck.open) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: `Our office is closed on ${dateLabel} (${dayCheck.displayDay}). Can you please choose another day?`,
|
||||||
|
Spanish: `Nuestra oficina está cerrada el ${dateLabel} (${dayCheck.displayDay}). ¿Puede elegir otro día?`,
|
||||||
|
Portuguese: `Nosso consultório está fechado em ${dateLabel} (${dayCheck.displayDay}). Pode escolher outro dia?`,
|
||||||
|
Mandarin: `我们诊所在 ${dateLabel}(${dayCheck.displayDay})不开放。请您选择另一天好吗?`,
|
||||||
|
Cantonese: `我們診所在 ${dateLabel}(${dayCheck.displayDay})不開放。請您選擇另一天好嗎?`,
|
||||||
|
Arabic: `مكتبنا مغلق في ${dateLabel} (${dayCheck.displayDay}). هل يمكنك اختيار يوم آخر؟`,
|
||||||
|
"Haitian Creole": `Biwo nou fèmen nan ${dateLabel} (${dayCheck.displayDay}). Tanpri chwazi yon lòt jou?`,
|
||||||
|
};
|
||||||
|
return reply(msgs[language] ?? msgs["English"]!, "asked_appointment_time");
|
||||||
|
}
|
||||||
|
// Day is open — save pending date and ask for preferred time
|
||||||
|
setPendingReschedule(patient.userId, patient.id, { newDate: date, dayLabel: dateLabel });
|
||||||
|
const askTimeMsgs: Record<string, string> = {
|
||||||
|
English: `What time do you prefer on ${dateLabel}?`,
|
||||||
|
Spanish: `¿A qué hora prefiere el ${dateLabel}?`,
|
||||||
|
Portuguese: `Que horário você prefere em ${dateLabel}?`,
|
||||||
|
Mandarin: `您希望在 ${dateLabel} 几点?`,
|
||||||
|
Cantonese: `您希望在 ${dateLabel} 幾點?`,
|
||||||
|
Arabic: `ما الوقت الذي تفضله في ${dateLabel}؟`,
|
||||||
|
"Haitian Creole": `Ki lè ou prefere nan ${dateLabel}?`,
|
||||||
|
};
|
||||||
|
return reply(askTimeMsgs[language] ?? askTimeMsgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Stage: asked_new_appt_time_for_date → parse time, check hours ────
|
||||||
|
if (stage === "asked_new_appt_time_for_date") {
|
||||||
|
const pending = getPendingReschedule(patient.userId, patient.id);
|
||||||
|
if (!pending) {
|
||||||
|
return reply("I lost track of the date. What day would you prefer?", "asked_appointment_time");
|
||||||
|
}
|
||||||
|
const startTime = await parseTime(Body, aiSettings.apiKey);
|
||||||
|
if (!startTime) {
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: `I didn't catch the time. What time do you prefer on ${pending.dayLabel}? For example: '10am' or '2pm'.`,
|
||||||
|
Spanish: `No entendí la hora. ¿Qué hora prefiere el ${pending.dayLabel}? Por ejemplo: '10am' o '2pm'.`,
|
||||||
|
Portuguese: `Não entendi o horário. Que hora você prefere em ${pending.dayLabel}? Por exemplo: '10h' ou '14h'.`,
|
||||||
|
Mandarin: `我没听清时间。您在 ${pending.dayLabel} 几点?例如:上午10点或下午2点。`,
|
||||||
|
Cantonese: `我沒聽清時間。您在 ${pending.dayLabel} 幾點?例如:上午10點或下午2點。`,
|
||||||
|
Arabic: `لم أفهم الوقت. ما الوقت الذي تفضله في ${pending.dayLabel}؟ مثلاً: 10 صباحاً أو 2 مساءً.`,
|
||||||
|
"Haitian Creole": `Mwen pa konprann lè a. Ki lè ou prefere nan ${pending.dayLabel}? Pa egzanp: 10am oswa 2pm.`,
|
||||||
|
};
|
||||||
|
return reply(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
const withinHours = await isWithinOfficeHours(pending.newDate, startTime, patient.userId);
|
||||||
|
if (!withinHours) {
|
||||||
|
const hoursDisplay = await getOfficeHoursDisplay(pending.newDate, patient.userId);
|
||||||
|
const timeLbl = timeLabel(startTime);
|
||||||
|
const msgs: Record<string, string> = {
|
||||||
|
English: hoursDisplay
|
||||||
|
? `Our office is not available at ${timeLbl} on ${pending.dayLabel}. Our hours are ${hoursDisplay}. What other time do you prefer?`
|
||||||
|
: `Our office is not available at ${timeLbl} on ${pending.dayLabel}. What other time do you prefer?`,
|
||||||
|
Spanish: hoursDisplay
|
||||||
|
? `Nuestra oficina no está disponible a las ${timeLbl} el ${pending.dayLabel}. Nuestro horario es ${hoursDisplay}. ¿Qué otro horario prefiere?`
|
||||||
|
: `Nuestra oficina no está disponible a las ${timeLbl} el ${pending.dayLabel}. ¿Qué otro horario prefiere?`,
|
||||||
|
Portuguese: hoursDisplay
|
||||||
|
? `Nosso consultório não está disponível às ${timeLbl} em ${pending.dayLabel}. Nosso horário é ${hoursDisplay}. Que outro horário você prefere?`
|
||||||
|
: `Nosso consultório não está disponível às ${timeLbl} em ${pending.dayLabel}. Que outro horário você prefere?`,
|
||||||
|
Mandarin: hoursDisplay
|
||||||
|
? `我们诊所在 ${pending.dayLabel} ${timeLbl} 不开放。工作时间是 ${hoursDisplay}。您希望改什么时间?`
|
||||||
|
: `我们诊所在 ${pending.dayLabel} ${timeLbl} 不开放。您希望改什么时间?`,
|
||||||
|
Cantonese: hoursDisplay
|
||||||
|
? `我們診所在 ${pending.dayLabel} ${timeLbl} 不開放。工作時間是 ${hoursDisplay}。您希望改什麼時間?`
|
||||||
|
: `我們診所在 ${pending.dayLabel} ${timeLbl} 不開放。您希望改什麼時間?`,
|
||||||
|
Arabic: hoursDisplay
|
||||||
|
? `مكتبنا غير متاح في ${timeLbl} يوم ${pending.dayLabel}. ساعات العمل: ${hoursDisplay}. ما وقت آخر تفضله؟`
|
||||||
|
: `مكتبنا غير متاح في ${timeLbl} يوم ${pending.dayLabel}. ما وقت آخر تفضله؟`,
|
||||||
|
"Haitian Creole": hoursDisplay
|
||||||
|
? `Biwo nou pa disponib a ${timeLbl} nan ${pending.dayLabel}. Orè nou se ${hoursDisplay}. Ki lòt lè ou prefere?`
|
||||||
|
: `Biwo nou pa disponib a ${timeLbl} nan ${pending.dayLabel}. Ki lòt lè ou prefere?`,
|
||||||
|
};
|
||||||
|
return reply(msgs[language] ?? msgs["English"]!, "asked_new_appt_time_for_date");
|
||||||
|
}
|
||||||
|
clearPendingReschedule(patient.userId, patient.id);
|
||||||
|
const apptLabel = `${pending.dayLabel} at ${timeLabel(startTime)}`;
|
||||||
|
|
||||||
|
// Calculate end time (60-minute default slot)
|
||||||
|
const [nh, nm] = startTime.split(":").map(Number);
|
||||||
|
const endTotal = nh! * 60 + nm! + 60;
|
||||||
|
const endTime = `${String(Math.floor(endTotal / 60)).padStart(2, "0")}:${String(endTotal % 60).padStart(2, "0")}`;
|
||||||
|
|
||||||
|
// Look up patient name + find first staff for this office
|
||||||
|
const [patientRecord, firstStaff] = await Promise.all([
|
||||||
|
db.patient.findUnique({
|
||||||
|
where: { id: patient.id },
|
||||||
|
select: { firstName: true, lastName: true },
|
||||||
|
}),
|
||||||
|
db.staff.findFirst({
|
||||||
|
where: { userId: patient.userId },
|
||||||
|
orderBy: { id: "asc" },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Create the appointment if a staff member exists
|
||||||
|
if (firstStaff) {
|
||||||
|
try {
|
||||||
|
await storage.createAppointment({
|
||||||
|
patientId: patient.id,
|
||||||
|
userId: patient.userId,
|
||||||
|
staffId: firstStaff.id,
|
||||||
|
title: `AI Scheduled - ${(patientRecord?.firstName ?? "") + " " + (patientRecord?.lastName ?? "")}`.trim(),
|
||||||
|
date: pending.newDate,
|
||||||
|
startTime,
|
||||||
|
endTime,
|
||||||
|
type: "checkup",
|
||||||
|
status: "scheduled",
|
||||||
|
movedByAi: true,
|
||||||
|
} as any);
|
||||||
|
} catch { /* silent — message still sent, staff can create manually */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmMsgs: Record<string, string> = {
|
||||||
|
English: `Thank you! Your preferred appointment at ${apptLabel} was scheduled. Our receptionist will confirm it with you shortly.`,
|
||||||
|
Spanish: `¡Gracias! Su cita preferida el ${apptLabel} fue programada. Nuestra recepcionista lo confirmará con usted en breve.`,
|
||||||
|
Portuguese: `Obrigado! Sua consulta preferida em ${apptLabel} foi agendada. Nossa recepcionista confirmará com você em breve.`,
|
||||||
|
Mandarin: `谢谢!您在 ${apptLabel} 的预约已安排。我们的前台将很快与您确认。`,
|
||||||
|
Cantonese: `多謝!您在 ${apptLabel} 的預約已安排。我們的接待員將很快與您確認。`,
|
||||||
|
Arabic: `شكراً! تم جدولة موعدك في ${apptLabel}. ستتواصل معك موظفة الاستقبال قريباً لتأكيده.`,
|
||||||
|
"Haitian Creole": `Mèsi! Randevou ou a nan ${apptLabel} pwograme. Resepsyonis nou an pral konfime li ak ou byento.`,
|
||||||
|
};
|
||||||
|
return reply(confirmMsgs[language] ?? confirmMsgs["English"]!, "done");
|
||||||
|
}
|
||||||
|
|
||||||
// ── Stage: new_patient_greeted + multi-step new patient stages ────────
|
// ── Stage: new_patient_greeted + multi-step new patient stages ────────
|
||||||
const newPatientStages: ConversationStage[] = [
|
const newPatientStages: ConversationStage[] = [
|
||||||
"new_patient_greeted", "asked_new_or_existing",
|
"new_patient_greeted", "asked_new_or_existing",
|
||||||
"asked_new_patient_insurance", "asked_existing_insurance",
|
"asked_new_patient_insurance", "asked_insurance_type",
|
||||||
"asked_appointment_time",
|
"asked_masshealth_check_consent", "asked_existing_insurance",
|
||||||
"asked_appointment_preference", "asked_self_pay",
|
"asked_appointment_preference",
|
||||||
|
"asked_self_pay", "asked_other_insurance_after_inactive",
|
||||||
|
"collecting_contact_info",
|
||||||
];
|
];
|
||||||
if (newPatientStages.includes(stage)) {
|
if (newPatientStages.includes(stage)) {
|
||||||
const { reply: aiReply, nextStage } = await runNewPatientStep(
|
const { reply: aiReply, nextStage } = await runNewPatientStep(
|
||||||
@@ -518,17 +1057,78 @@ router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Stage: initial (no active conversation) ───────────────────────────
|
// ── Stage: initial / done (patient texts in fresh) ───────────────────
|
||||||
// Check after-hours: if enabled and currently outside office hours → start new-patient flow
|
|
||||||
if (stage === "initial" || stage === "done") {
|
if (stage === "initial" || stage === "done") {
|
||||||
|
const openPhoneReply = await storage.getOpenPhoneReply(patient.userId);
|
||||||
const afterHoursEnabled = await getAfterHoursHandoff(patient.userId);
|
const afterHoursEnabled = await getAfterHoursHandoff(patient.userId);
|
||||||
const outsideHours = await isAfterHours(patient.userId);
|
const outsideHours = await isAfterHours(patient.userId);
|
||||||
|
|
||||||
if (afterHoursEnabled && outsideHours) {
|
if (openPhoneReply || (afterHoursEnabled && outsideHours)) {
|
||||||
|
// MSG 1: AI self-introduction
|
||||||
const rawGreeting = chatTemplates.newPatientGreeting ||
|
const rawGreeting = chatTemplates.newPatientGreeting ||
|
||||||
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7. How can I help you today?`;
|
`Hi! My name is Lisa, the dedicated AI assistant at {officeName}. I can help you schedule an appointment, check your insurance, and answer general questions 24/7.`;
|
||||||
|
const introText = applyOfficeName(rawGreeting, officeName);
|
||||||
|
|
||||||
return reply(applyOfficeName(rawGreeting, officeName), "new_patient_greeted");
|
// MSG 2: Empathetic acknowledgment of the patient's message
|
||||||
|
const empatheticText = getEmpatheticAck(Body, language);
|
||||||
|
|
||||||
|
// MSG 3: Detect reschedule vs new appointment from initial text
|
||||||
|
const isRescheduleIntent = /reschedule|rescheduler|change.*appoint|modify.*appoint|move.*appoint|reprogramar|reagendar|cambiar|mudar/i.test(Body);
|
||||||
|
|
||||||
|
let msg3: string;
|
||||||
|
let nextStage3: ConversationStage;
|
||||||
|
|
||||||
|
if (isRescheduleIntent) {
|
||||||
|
const apptDatetime = await getAppointmentDatetime(patient.id);
|
||||||
|
if (apptDatetime) {
|
||||||
|
const foundMsgs: Record<string, string> = {
|
||||||
|
English: `Ok. Just to confirm, your current appointment is on ${apptDatetime}. When would you like to reschedule?`,
|
||||||
|
Spanish: `De acuerdo. Solo para confirmar, su cita actual es el ${apptDatetime}. ¿Cuándo le gustaría reprogramarla?`,
|
||||||
|
Portuguese: `Ok. Só para confirmar, sua consulta atual é em ${apptDatetime}. Quando você gostaria de reagendar?`,
|
||||||
|
Mandarin: `好的。请确认一下,您当前的预约是 ${apptDatetime}。您想重新安排到什么时候?`,
|
||||||
|
Cantonese: `好的。請確認一下,您目前的預約是 ${apptDatetime}。您想重新安排到什麼時候?`,
|
||||||
|
Arabic: `حسناً. فقط للتأكيد، موعدك الحالي في ${apptDatetime}. متى تريد إعادة الجدولة؟`,
|
||||||
|
"Haitian Creole": `Oke. Jis pou konfime, randevou aktyèl ou a se ${apptDatetime}. Ki lè ou ta renmen reprogramè?`,
|
||||||
|
};
|
||||||
|
msg3 = foundMsgs[language] ?? foundMsgs["English"]!;
|
||||||
|
nextStage3 = "asked_reschedule_datetime";
|
||||||
|
} else {
|
||||||
|
const notFoundMsgs: Record<string, string> = {
|
||||||
|
English: "Ok. I could not find your current appointment. Please tell me when you'd like to reschedule and our receptionist will contact you as soon as possible.",
|
||||||
|
Spanish: "De acuerdo. No pude encontrar su cita actual. Por favor díganos cuándo le gustaría reprogramar y nuestra recepcionista le contactará lo antes posible.",
|
||||||
|
Portuguese: "Ok. Não consegui encontrar sua consulta atual. Por favor, diga-nos quando você gostaria de reagendar e nossa recepcionista entrará em contato o mais breve possível.",
|
||||||
|
Mandarin: "好的。我找不到您当前的预约。请告诉我您希望重新安排的时间,我们的前台将尽快与您联系。",
|
||||||
|
Cantonese: "好的。我找不到您目前的預約。請告訴我您希望重新安排的時間,我們的接待員將盡快與您聯絡。",
|
||||||
|
Arabic: "حسناً. لم أجد موعدك الحالي. يرجى إخبارنا بالوقت الذي تريد إعادة الجدولة إليه وسيتصل بك موظف الاستقبال في أقرب وقت ممكن.",
|
||||||
|
"Haitian Creole": "Oke. Mwen pa t jwenn randevou aktyèl ou. Tanpri di nou ki lè ou ta renmen reprogramè epi resepsyonis nou an pral kontakte ou pi vit posib.",
|
||||||
|
};
|
||||||
|
msg3 = notFoundMsgs[language] ?? notFoundMsgs["English"]!;
|
||||||
|
nextStage3 = "done";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// New appointment (or any other intent) → ask new or existing patient
|
||||||
|
const newOrExistingMsgs: Record<string, string> = {
|
||||||
|
English: "Can you please tell me if you are a new or existing patient?",
|
||||||
|
Spanish: "¿Puede decirme si es usted un paciente nuevo o existente?",
|
||||||
|
Portuguese: "Pode me dizer se você é um paciente novo ou existente?",
|
||||||
|
Mandarin: "请问您是新患者还是现有患者?",
|
||||||
|
Cantonese: "請問您是新病人還是現有病人?",
|
||||||
|
Arabic: "هل يمكنك إخباري إذا كنت مريضاً جديداً أم حالياً؟",
|
||||||
|
"Haitian Creole": "Èske ou ka di mwen si ou se yon nouvo pasyan oswa yon pasyan egzistan?",
|
||||||
|
};
|
||||||
|
msg3 = newOrExistingMsgs[language] ?? newOrExistingMsgs["English"]!;
|
||||||
|
nextStage3 = "asked_new_or_existing";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save all 3 outbound messages and set stage
|
||||||
|
await saveOutbound(patient.id, introText);
|
||||||
|
await saveOutbound(patient.id, empatheticText);
|
||||||
|
await saveOutbound(patient.id, msg3);
|
||||||
|
await setStage(patient.userId, patient.id, nextStage3);
|
||||||
|
|
||||||
|
// Send all 3 in one TwiML response — Twilio guarantees delivery order
|
||||||
|
res.set("Content-Type", "text/xml");
|
||||||
|
return res.send(twimlMessages(introText, empatheticText, msg3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,17 @@ export const aiSettingsStorage = {
|
|||||||
create: { userId, apiKey },
|
create: { userId, apiKey },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getOpenPhoneReply(userId: number): Promise<boolean> {
|
||||||
|
const row = await db.aiSettings.findUnique({ where: { userId } });
|
||||||
|
return row?.openPhoneReply ?? false;
|
||||||
|
},
|
||||||
|
|
||||||
|
async setOpenPhoneReply(userId: number, enabled: boolean): Promise<void> {
|
||||||
|
await db.aiSettings.upsert({
|
||||||
|
where: { userId },
|
||||||
|
update: { openPhoneReply: enabled },
|
||||||
|
create: { userId, apiKey: "", openPhoneReply: enabled },
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { useQuery, useMutation } from "@tanstack/react-query";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import { apiRequest, queryClient } from "@/lib/queryClient";
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus } from "lucide-react";
|
import { Bot, CalendarCheck, UserPlus, MessageCircle, Info, GitFork, MessageSquare, Trash2, Plus, Zap } from "lucide-react";
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
@@ -546,6 +547,39 @@ export function AiChatSettingsCard() {
|
|||||||
const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback);
|
const [generalFallback, setGeneralFallback] = useState(DEFAULTS.generalFallback);
|
||||||
const initialized = useRef(false);
|
const initialized = useRef(false);
|
||||||
|
|
||||||
|
const [openPhoneReply, setOpenPhoneReply] = useState(false);
|
||||||
|
|
||||||
|
const { data: advancedSettings } = useQuery<{ openPhoneReply: boolean }>({
|
||||||
|
queryKey: ["/api/ai/advanced-settings"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const res = await apiRequest("GET", "/api/ai/advanced-settings");
|
||||||
|
if (!res.ok) return { openPhoneReply: false };
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
staleTime: Infinity,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (advancedSettings) setOpenPhoneReply(advancedSettings.openPhoneReply ?? false);
|
||||||
|
}, [advancedSettings]);
|
||||||
|
|
||||||
|
const openPhoneReplyMutation = useMutation({
|
||||||
|
mutationFn: async (enabled: boolean) => {
|
||||||
|
const res = await apiRequest("PUT", "/api/ai/advanced-settings", { openPhoneReply: enabled });
|
||||||
|
if (!res.ok) throw new Error("Failed to save");
|
||||||
|
return res.json();
|
||||||
|
},
|
||||||
|
onSuccess: (data) => {
|
||||||
|
setOpenPhoneReply(data.openPhoneReply);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["/api/ai/advanced-settings"] });
|
||||||
|
toast({ title: data.openPhoneReply ? "Open reply enabled" : "Open reply disabled" });
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({ title: "Error", description: "Failed to save setting.", variant: "destructive" });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// ── SMS template list ──────────────────────────────────────────
|
// ── SMS template list ──────────────────────────────────────────
|
||||||
const [smsTemplates, setSmsTemplates] = useState<SmsTemplate[]>([]);
|
const [smsTemplates, setSmsTemplates] = useState<SmsTemplate[]>([]);
|
||||||
const smsInitialized = useRef(false);
|
const smsInitialized = useRef(false);
|
||||||
@@ -698,6 +732,29 @@ export function AiChatSettingsCard() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
||||||
|
{/* ── Advanced: Open Phone Reply toggle ───────────────────── */}
|
||||||
|
<Card>
|
||||||
|
<CardContent className="py-6">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Zap className="h-5 w-5 text-primary mt-0.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-semibold leading-snug">Respond to any phone number at any time</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
When enabled, the AI will reply to all incoming texts — including unknown numbers not yet in your patient list — 24/7 regardless of office hours. When disabled, the AI only responds during after-hours.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
checked={openPhoneReply}
|
||||||
|
disabled={openPhoneReplyMutation.isPending}
|
||||||
|
onCheckedChange={(val) => openPhoneReplyMutation.mutate(val)}
|
||||||
|
className="flex-shrink-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* ── Section 0: SMS Templates ─────────────────────────────── */}
|
{/* ── Section 0: SMS Templates ─────────────────────────────── */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-6 space-y-5">
|
<CardContent className="py-6 space-y-5">
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -423,7 +423,8 @@ exports.Prisma.AiSettingsScalarFieldEnum = {
|
|||||||
id: 'id',
|
id: 'id',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
apiKey: 'apiKey',
|
apiKey: 'apiKey',
|
||||||
afterHoursEnabled: 'afterHoursEnabled'
|
afterHoursEnabled: 'afterHoursEnabled',
|
||||||
|
openPhoneReply: 'openPhoneReply'
|
||||||
};
|
};
|
||||||
|
|
||||||
exports.Prisma.OfficeHoursScalarFieldEnum = {
|
exports.Prisma.OfficeHoursScalarFieldEnum = {
|
||||||
|
|||||||
37
packages/db/generated/prisma/index.d.ts
vendored
37
packages/db/generated/prisma/index.d.ts
vendored
@@ -34731,6 +34731,7 @@ export namespace Prisma {
|
|||||||
userId: number | null
|
userId: number | null
|
||||||
apiKey: string | null
|
apiKey: string | null
|
||||||
afterHoursEnabled: boolean | null
|
afterHoursEnabled: boolean | null
|
||||||
|
openPhoneReply: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsMaxAggregateOutputType = {
|
export type AiSettingsMaxAggregateOutputType = {
|
||||||
@@ -34738,6 +34739,7 @@ export namespace Prisma {
|
|||||||
userId: number | null
|
userId: number | null
|
||||||
apiKey: string | null
|
apiKey: string | null
|
||||||
afterHoursEnabled: boolean | null
|
afterHoursEnabled: boolean | null
|
||||||
|
openPhoneReply: boolean | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsCountAggregateOutputType = {
|
export type AiSettingsCountAggregateOutputType = {
|
||||||
@@ -34745,6 +34747,7 @@ export namespace Prisma {
|
|||||||
userId: number
|
userId: number
|
||||||
apiKey: number
|
apiKey: number
|
||||||
afterHoursEnabled: number
|
afterHoursEnabled: number
|
||||||
|
openPhoneReply: number
|
||||||
_all: number
|
_all: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34764,6 +34767,7 @@ export namespace Prisma {
|
|||||||
userId?: true
|
userId?: true
|
||||||
apiKey?: true
|
apiKey?: true
|
||||||
afterHoursEnabled?: true
|
afterHoursEnabled?: true
|
||||||
|
openPhoneReply?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsMaxAggregateInputType = {
|
export type AiSettingsMaxAggregateInputType = {
|
||||||
@@ -34771,6 +34775,7 @@ export namespace Prisma {
|
|||||||
userId?: true
|
userId?: true
|
||||||
apiKey?: true
|
apiKey?: true
|
||||||
afterHoursEnabled?: true
|
afterHoursEnabled?: true
|
||||||
|
openPhoneReply?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsCountAggregateInputType = {
|
export type AiSettingsCountAggregateInputType = {
|
||||||
@@ -34778,6 +34783,7 @@ export namespace Prisma {
|
|||||||
userId?: true
|
userId?: true
|
||||||
apiKey?: true
|
apiKey?: true
|
||||||
afterHoursEnabled?: true
|
afterHoursEnabled?: true
|
||||||
|
openPhoneReply?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34872,6 +34878,7 @@ export namespace Prisma {
|
|||||||
userId: number
|
userId: number
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled: boolean
|
afterHoursEnabled: boolean
|
||||||
|
openPhoneReply: boolean
|
||||||
_count: AiSettingsCountAggregateOutputType | null
|
_count: AiSettingsCountAggregateOutputType | null
|
||||||
_avg: AiSettingsAvgAggregateOutputType | null
|
_avg: AiSettingsAvgAggregateOutputType | null
|
||||||
_sum: AiSettingsSumAggregateOutputType | null
|
_sum: AiSettingsSumAggregateOutputType | null
|
||||||
@@ -34898,6 +34905,7 @@ export namespace Prisma {
|
|||||||
userId?: boolean
|
userId?: boolean
|
||||||
apiKey?: boolean
|
apiKey?: boolean
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
user?: boolean | UserDefaultArgs<ExtArgs>
|
user?: boolean | UserDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["aiSettings"]>
|
}, ExtArgs["result"]["aiSettings"]>
|
||||||
|
|
||||||
@@ -34906,6 +34914,7 @@ export namespace Prisma {
|
|||||||
userId?: boolean
|
userId?: boolean
|
||||||
apiKey?: boolean
|
apiKey?: boolean
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
user?: boolean | UserDefaultArgs<ExtArgs>
|
user?: boolean | UserDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["aiSettings"]>
|
}, ExtArgs["result"]["aiSettings"]>
|
||||||
|
|
||||||
@@ -34914,6 +34923,7 @@ export namespace Prisma {
|
|||||||
userId?: boolean
|
userId?: boolean
|
||||||
apiKey?: boolean
|
apiKey?: boolean
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
user?: boolean | UserDefaultArgs<ExtArgs>
|
user?: boolean | UserDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["aiSettings"]>
|
}, ExtArgs["result"]["aiSettings"]>
|
||||||
|
|
||||||
@@ -34922,9 +34932,10 @@ export namespace Prisma {
|
|||||||
userId?: boolean
|
userId?: boolean
|
||||||
apiKey?: boolean
|
apiKey?: boolean
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "userId" | "apiKey" | "afterHoursEnabled", ExtArgs["result"]["aiSettings"]>
|
export type AiSettingsOmit<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = $Extensions.GetOmit<"id" | "userId" | "apiKey" | "afterHoursEnabled" | "openPhoneReply", ExtArgs["result"]["aiSettings"]>
|
||||||
export type AiSettingsInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
export type AiSettingsInclude<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs> = {
|
||||||
user?: boolean | UserDefaultArgs<ExtArgs>
|
user?: boolean | UserDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
@@ -34945,6 +34956,7 @@ export namespace Prisma {
|
|||||||
userId: number
|
userId: number
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled: boolean
|
afterHoursEnabled: boolean
|
||||||
|
openPhoneReply: boolean
|
||||||
}, ExtArgs["result"]["aiSettings"]>
|
}, ExtArgs["result"]["aiSettings"]>
|
||||||
composites: {}
|
composites: {}
|
||||||
}
|
}
|
||||||
@@ -35373,6 +35385,7 @@ export namespace Prisma {
|
|||||||
readonly userId: FieldRef<"AiSettings", 'Int'>
|
readonly userId: FieldRef<"AiSettings", 'Int'>
|
||||||
readonly apiKey: FieldRef<"AiSettings", 'String'>
|
readonly apiKey: FieldRef<"AiSettings", 'String'>
|
||||||
readonly afterHoursEnabled: FieldRef<"AiSettings", 'Boolean'>
|
readonly afterHoursEnabled: FieldRef<"AiSettings", 'Boolean'>
|
||||||
|
readonly openPhoneReply: FieldRef<"AiSettings", 'Boolean'>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -41715,7 +41728,8 @@ export namespace Prisma {
|
|||||||
id: 'id',
|
id: 'id',
|
||||||
userId: 'userId',
|
userId: 'userId',
|
||||||
apiKey: 'apiKey',
|
apiKey: 'apiKey',
|
||||||
afterHoursEnabled: 'afterHoursEnabled'
|
afterHoursEnabled: 'afterHoursEnabled',
|
||||||
|
openPhoneReply: 'openPhoneReply'
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AiSettingsScalarFieldEnum = (typeof AiSettingsScalarFieldEnum)[keyof typeof AiSettingsScalarFieldEnum]
|
export type AiSettingsScalarFieldEnum = (typeof AiSettingsScalarFieldEnum)[keyof typeof AiSettingsScalarFieldEnum]
|
||||||
@@ -44229,6 +44243,7 @@ export namespace Prisma {
|
|||||||
userId?: IntFilter<"AiSettings"> | number
|
userId?: IntFilter<"AiSettings"> | number
|
||||||
apiKey?: StringFilter<"AiSettings"> | string
|
apiKey?: StringFilter<"AiSettings"> | string
|
||||||
afterHoursEnabled?: BoolFilter<"AiSettings"> | boolean
|
afterHoursEnabled?: BoolFilter<"AiSettings"> | boolean
|
||||||
|
openPhoneReply?: BoolFilter<"AiSettings"> | boolean
|
||||||
user?: XOR<UserScalarRelationFilter, UserWhereInput>
|
user?: XOR<UserScalarRelationFilter, UserWhereInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44237,6 +44252,7 @@ export namespace Prisma {
|
|||||||
userId?: SortOrder
|
userId?: SortOrder
|
||||||
apiKey?: SortOrder
|
apiKey?: SortOrder
|
||||||
afterHoursEnabled?: SortOrder
|
afterHoursEnabled?: SortOrder
|
||||||
|
openPhoneReply?: SortOrder
|
||||||
user?: UserOrderByWithRelationInput
|
user?: UserOrderByWithRelationInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44248,6 +44264,7 @@ export namespace Prisma {
|
|||||||
NOT?: AiSettingsWhereInput | AiSettingsWhereInput[]
|
NOT?: AiSettingsWhereInput | AiSettingsWhereInput[]
|
||||||
apiKey?: StringFilter<"AiSettings"> | string
|
apiKey?: StringFilter<"AiSettings"> | string
|
||||||
afterHoursEnabled?: BoolFilter<"AiSettings"> | boolean
|
afterHoursEnabled?: BoolFilter<"AiSettings"> | boolean
|
||||||
|
openPhoneReply?: BoolFilter<"AiSettings"> | boolean
|
||||||
user?: XOR<UserScalarRelationFilter, UserWhereInput>
|
user?: XOR<UserScalarRelationFilter, UserWhereInput>
|
||||||
}, "id" | "userId">
|
}, "id" | "userId">
|
||||||
|
|
||||||
@@ -44256,6 +44273,7 @@ export namespace Prisma {
|
|||||||
userId?: SortOrder
|
userId?: SortOrder
|
||||||
apiKey?: SortOrder
|
apiKey?: SortOrder
|
||||||
afterHoursEnabled?: SortOrder
|
afterHoursEnabled?: SortOrder
|
||||||
|
openPhoneReply?: SortOrder
|
||||||
_count?: AiSettingsCountOrderByAggregateInput
|
_count?: AiSettingsCountOrderByAggregateInput
|
||||||
_avg?: AiSettingsAvgOrderByAggregateInput
|
_avg?: AiSettingsAvgOrderByAggregateInput
|
||||||
_max?: AiSettingsMaxOrderByAggregateInput
|
_max?: AiSettingsMaxOrderByAggregateInput
|
||||||
@@ -44271,6 +44289,7 @@ export namespace Prisma {
|
|||||||
userId?: IntWithAggregatesFilter<"AiSettings"> | number
|
userId?: IntWithAggregatesFilter<"AiSettings"> | number
|
||||||
apiKey?: StringWithAggregatesFilter<"AiSettings"> | string
|
apiKey?: StringWithAggregatesFilter<"AiSettings"> | string
|
||||||
afterHoursEnabled?: BoolWithAggregatesFilter<"AiSettings"> | boolean
|
afterHoursEnabled?: BoolWithAggregatesFilter<"AiSettings"> | boolean
|
||||||
|
openPhoneReply?: BoolWithAggregatesFilter<"AiSettings"> | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OfficeHoursWhereInput = {
|
export type OfficeHoursWhereInput = {
|
||||||
@@ -46744,6 +46763,7 @@ export namespace Prisma {
|
|||||||
export type AiSettingsCreateInput = {
|
export type AiSettingsCreateInput = {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
user: UserCreateNestedOneWithoutAiSettingsInput
|
user: UserCreateNestedOneWithoutAiSettingsInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46752,11 +46772,13 @@ export namespace Prisma {
|
|||||||
userId: number
|
userId: number
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsUpdateInput = {
|
export type AiSettingsUpdateInput = {
|
||||||
apiKey?: StringFieldUpdateOperationsInput | string
|
apiKey?: StringFieldUpdateOperationsInput | string
|
||||||
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
||||||
|
openPhoneReply?: BoolFieldUpdateOperationsInput | boolean
|
||||||
user?: UserUpdateOneRequiredWithoutAiSettingsNestedInput
|
user?: UserUpdateOneRequiredWithoutAiSettingsNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46765,6 +46787,7 @@ export namespace Prisma {
|
|||||||
userId?: IntFieldUpdateOperationsInput | number
|
userId?: IntFieldUpdateOperationsInput | number
|
||||||
apiKey?: StringFieldUpdateOperationsInput | string
|
apiKey?: StringFieldUpdateOperationsInput | string
|
||||||
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
||||||
|
openPhoneReply?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsCreateManyInput = {
|
export type AiSettingsCreateManyInput = {
|
||||||
@@ -46772,11 +46795,13 @@ export namespace Prisma {
|
|||||||
userId: number
|
userId: number
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsUpdateManyMutationInput = {
|
export type AiSettingsUpdateManyMutationInput = {
|
||||||
apiKey?: StringFieldUpdateOperationsInput | string
|
apiKey?: StringFieldUpdateOperationsInput | string
|
||||||
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
||||||
|
openPhoneReply?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsUncheckedUpdateManyInput = {
|
export type AiSettingsUncheckedUpdateManyInput = {
|
||||||
@@ -46784,6 +46809,7 @@ export namespace Prisma {
|
|||||||
userId?: IntFieldUpdateOperationsInput | number
|
userId?: IntFieldUpdateOperationsInput | number
|
||||||
apiKey?: StringFieldUpdateOperationsInput | string
|
apiKey?: StringFieldUpdateOperationsInput | string
|
||||||
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
||||||
|
openPhoneReply?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OfficeHoursCreateInput = {
|
export type OfficeHoursCreateInput = {
|
||||||
@@ -49191,6 +49217,7 @@ export namespace Prisma {
|
|||||||
userId?: SortOrder
|
userId?: SortOrder
|
||||||
apiKey?: SortOrder
|
apiKey?: SortOrder
|
||||||
afterHoursEnabled?: SortOrder
|
afterHoursEnabled?: SortOrder
|
||||||
|
openPhoneReply?: SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsAvgOrderByAggregateInput = {
|
export type AiSettingsAvgOrderByAggregateInput = {
|
||||||
@@ -49203,6 +49230,7 @@ export namespace Prisma {
|
|||||||
userId?: SortOrder
|
userId?: SortOrder
|
||||||
apiKey?: SortOrder
|
apiKey?: SortOrder
|
||||||
afterHoursEnabled?: SortOrder
|
afterHoursEnabled?: SortOrder
|
||||||
|
openPhoneReply?: SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsMinOrderByAggregateInput = {
|
export type AiSettingsMinOrderByAggregateInput = {
|
||||||
@@ -49210,6 +49238,7 @@ export namespace Prisma {
|
|||||||
userId?: SortOrder
|
userId?: SortOrder
|
||||||
apiKey?: SortOrder
|
apiKey?: SortOrder
|
||||||
afterHoursEnabled?: SortOrder
|
afterHoursEnabled?: SortOrder
|
||||||
|
openPhoneReply?: SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsSumOrderByAggregateInput = {
|
export type AiSettingsSumOrderByAggregateInput = {
|
||||||
@@ -53081,12 +53110,14 @@ export namespace Prisma {
|
|||||||
export type AiSettingsCreateWithoutUserInput = {
|
export type AiSettingsCreateWithoutUserInput = {
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsUncheckedCreateWithoutUserInput = {
|
export type AiSettingsUncheckedCreateWithoutUserInput = {
|
||||||
id?: number
|
id?: number
|
||||||
apiKey: string
|
apiKey: string
|
||||||
afterHoursEnabled?: boolean
|
afterHoursEnabled?: boolean
|
||||||
|
openPhoneReply?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsCreateOrConnectWithoutUserInput = {
|
export type AiSettingsCreateOrConnectWithoutUserInput = {
|
||||||
@@ -53659,12 +53690,14 @@ export namespace Prisma {
|
|||||||
export type AiSettingsUpdateWithoutUserInput = {
|
export type AiSettingsUpdateWithoutUserInput = {
|
||||||
apiKey?: StringFieldUpdateOperationsInput | string
|
apiKey?: StringFieldUpdateOperationsInput | string
|
||||||
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
||||||
|
openPhoneReply?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AiSettingsUncheckedUpdateWithoutUserInput = {
|
export type AiSettingsUncheckedUpdateWithoutUserInput = {
|
||||||
id?: IntFieldUpdateOperationsInput | number
|
id?: IntFieldUpdateOperationsInput | number
|
||||||
apiKey?: StringFieldUpdateOperationsInput | string
|
apiKey?: StringFieldUpdateOperationsInput | string
|
||||||
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
afterHoursEnabled?: BoolFieldUpdateOperationsInput | boolean
|
||||||
|
openPhoneReply?: BoolFieldUpdateOperationsInput | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OfficeHoursUpsertWithoutUserInput = {
|
export type OfficeHoursUpsertWithoutUserInput = {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-7ab23aa0435ee62d1543cc6f3f10f1fac4ea741f94efe006ada0cea03090a0c9",
|
"name": "prisma-client-f058d09dfd4869ea5203c66c2370bac837d95f1a5aaea0632641bf080a04b5d8",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@@ -579,6 +579,7 @@ model AiSettings {
|
|||||||
userId Int @unique
|
userId Int @unique
|
||||||
apiKey String
|
apiKey String
|
||||||
afterHoursEnabled Boolean @default(true)
|
afterHoursEnabled Boolean @default(true)
|
||||||
|
openPhoneReply Boolean @default(false)
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|||||||
@@ -580,6 +580,7 @@ model AiSettings {
|
|||||||
userId Int @unique
|
userId Int @unique
|
||||||
apiKey String
|
apiKey String
|
||||||
afterHoursEnabled Boolean @default(true)
|
afterHoursEnabled Boolean @default(true)
|
||||||
|
openPhoneReply Boolean @default(false)
|
||||||
|
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
"generatorVersion": "1.0.0",
|
"generatorVersion": "1.0.0",
|
||||||
"generatedAt": "2026-05-12T02:28:39.220Z",
|
"generatedAt": "2026-05-14T18:13:16.768Z",
|
||||||
"outputPath": "/home/ee/Desktop/DentalManagementMH05/packages/db/shared",
|
"outputPath": "/home/ee/Desktop/DentalManagementMH05/packages/db/shared",
|
||||||
"files": [
|
"files": [
|
||||||
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
"schemas/enums/TransactionIsolationLevel.schema.ts",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
export const AiSettingsScalarFieldEnumSchema = z.enum(['id', 'userId', 'apiKey', 'afterHoursEnabled'])
|
export const AiSettingsScalarFieldEnumSchema = z.enum(['id', 'userId', 'apiKey', 'afterHoursEnabled', 'openPhoneReply'])
|
||||||
|
|
||||||
export type AiSettingsScalarFieldEnum = z.infer<typeof AiSettingsScalarFieldEnumSchema>;
|
export type AiSettingsScalarFieldEnum = z.infer<typeof AiSettingsScalarFieldEnumSchema>;
|
||||||
@@ -14,6 +14,7 @@ export const AiSettingsFindFirstSelectSchema: z.ZodType<Prisma.AiSettingsSelect>
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.boolean().optional()
|
user: z.boolean().optional()
|
||||||
}).strict() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
}).strict() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export const AiSettingsFindFirstSelectZodSchema = z.object({
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.boolean().optional()
|
user: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const AiSettingsFindFirstOrThrowSelectSchema: z.ZodType<Prisma.AiSettings
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.boolean().optional()
|
user: z.boolean().optional()
|
||||||
}).strict() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
}).strict() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export const AiSettingsFindFirstOrThrowSelectZodSchema = z.object({
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.boolean().optional()
|
user: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const AiSettingsFindManySelectSchema: z.ZodType<Prisma.AiSettingsSelect>
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.boolean().optional()
|
user: z.boolean().optional()
|
||||||
}).strict() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
}).strict() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export const AiSettingsFindManySelectZodSchema = z.object({
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.boolean().optional()
|
user: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const makeSchema = () => z.object({
|
|||||||
userId: z.literal(true).optional(),
|
userId: z.literal(true).optional(),
|
||||||
apiKey: z.literal(true).optional(),
|
apiKey: z.literal(true).optional(),
|
||||||
afterHoursEnabled: z.literal(true).optional(),
|
afterHoursEnabled: z.literal(true).optional(),
|
||||||
|
openPhoneReply: z.literal(true).optional(),
|
||||||
_all: z.literal(true).optional()
|
_all: z.literal(true).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsCountAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsCountAggregateInputType> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCountAggregateInputType>;
|
export const AiSettingsCountAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsCountAggregateInputType> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCountAggregateInputType>;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: SortOrderSchema.optional(),
|
id: SortOrderSchema.optional(),
|
||||||
userId: SortOrderSchema.optional(),
|
userId: SortOrderSchema.optional(),
|
||||||
apiKey: SortOrderSchema.optional(),
|
apiKey: SortOrderSchema.optional(),
|
||||||
afterHoursEnabled: SortOrderSchema.optional()
|
afterHoursEnabled: SortOrderSchema.optional(),
|
||||||
|
openPhoneReply: SortOrderSchema.optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsCountOrderByAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsCountOrderByAggregateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCountOrderByAggregateInput>;
|
export const AiSettingsCountOrderByAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsCountOrderByAggregateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCountOrderByAggregateInput>;
|
||||||
export const AiSettingsCountOrderByAggregateInputObjectZodSchema = makeSchema();
|
export const AiSettingsCountOrderByAggregateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { UserCreateNestedOneWithoutAiSettingsInputObjectSchema as UserCreateNest
|
|||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.lazy(() => UserCreateNestedOneWithoutAiSettingsInputObjectSchema)
|
user: z.lazy(() => UserCreateNestedOneWithoutAiSettingsInputObjectSchema)
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsCreateInputObjectSchema: z.ZodType<Prisma.AiSettingsCreateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCreateInput>;
|
export const AiSettingsCreateInputObjectSchema: z.ZodType<Prisma.AiSettingsCreateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCreateInput>;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean().optional()
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsCreateManyInputObjectSchema: z.ZodType<Prisma.AiSettingsCreateManyInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCreateManyInput>;
|
export const AiSettingsCreateManyInputObjectSchema: z.ZodType<Prisma.AiSettingsCreateManyInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCreateManyInput>;
|
||||||
export const AiSettingsCreateManyInputObjectZodSchema = makeSchema();
|
export const AiSettingsCreateManyInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import type { Prisma } from '../../../generated/prisma';
|
|||||||
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean().optional()
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsCreateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsCreateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCreateWithoutUserInput>;
|
export const AiSettingsCreateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsCreateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsCreateWithoutUserInput>;
|
||||||
export const AiSettingsCreateWithoutUserInputObjectZodSchema = makeSchema();
|
export const AiSettingsCreateWithoutUserInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: z.literal(true).optional(),
|
id: z.literal(true).optional(),
|
||||||
userId: z.literal(true).optional(),
|
userId: z.literal(true).optional(),
|
||||||
apiKey: z.literal(true).optional(),
|
apiKey: z.literal(true).optional(),
|
||||||
afterHoursEnabled: z.literal(true).optional()
|
afterHoursEnabled: z.literal(true).optional(),
|
||||||
|
openPhoneReply: z.literal(true).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsMaxAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMaxAggregateInputType> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMaxAggregateInputType>;
|
export const AiSettingsMaxAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMaxAggregateInputType> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMaxAggregateInputType>;
|
||||||
export const AiSettingsMaxAggregateInputObjectZodSchema = makeSchema();
|
export const AiSettingsMaxAggregateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: SortOrderSchema.optional(),
|
id: SortOrderSchema.optional(),
|
||||||
userId: SortOrderSchema.optional(),
|
userId: SortOrderSchema.optional(),
|
||||||
apiKey: SortOrderSchema.optional(),
|
apiKey: SortOrderSchema.optional(),
|
||||||
afterHoursEnabled: SortOrderSchema.optional()
|
afterHoursEnabled: SortOrderSchema.optional(),
|
||||||
|
openPhoneReply: SortOrderSchema.optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsMaxOrderByAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMaxOrderByAggregateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMaxOrderByAggregateInput>;
|
export const AiSettingsMaxOrderByAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMaxOrderByAggregateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMaxOrderByAggregateInput>;
|
||||||
export const AiSettingsMaxOrderByAggregateInputObjectZodSchema = makeSchema();
|
export const AiSettingsMaxOrderByAggregateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: z.literal(true).optional(),
|
id: z.literal(true).optional(),
|
||||||
userId: z.literal(true).optional(),
|
userId: z.literal(true).optional(),
|
||||||
apiKey: z.literal(true).optional(),
|
apiKey: z.literal(true).optional(),
|
||||||
afterHoursEnabled: z.literal(true).optional()
|
afterHoursEnabled: z.literal(true).optional(),
|
||||||
|
openPhoneReply: z.literal(true).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsMinAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMinAggregateInputType> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMinAggregateInputType>;
|
export const AiSettingsMinAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMinAggregateInputType> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMinAggregateInputType>;
|
||||||
export const AiSettingsMinAggregateInputObjectZodSchema = makeSchema();
|
export const AiSettingsMinAggregateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: SortOrderSchema.optional(),
|
id: SortOrderSchema.optional(),
|
||||||
userId: SortOrderSchema.optional(),
|
userId: SortOrderSchema.optional(),
|
||||||
apiKey: SortOrderSchema.optional(),
|
apiKey: SortOrderSchema.optional(),
|
||||||
afterHoursEnabled: SortOrderSchema.optional()
|
afterHoursEnabled: SortOrderSchema.optional(),
|
||||||
|
openPhoneReply: SortOrderSchema.optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsMinOrderByAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMinOrderByAggregateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMinOrderByAggregateInput>;
|
export const AiSettingsMinOrderByAggregateInputObjectSchema: z.ZodType<Prisma.AiSettingsMinOrderByAggregateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsMinOrderByAggregateInput>;
|
||||||
export const AiSettingsMinOrderByAggregateInputObjectZodSchema = makeSchema();
|
export const AiSettingsMinOrderByAggregateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const makeSchema = () => z.object({
|
|||||||
userId: SortOrderSchema.optional(),
|
userId: SortOrderSchema.optional(),
|
||||||
apiKey: SortOrderSchema.optional(),
|
apiKey: SortOrderSchema.optional(),
|
||||||
afterHoursEnabled: SortOrderSchema.optional(),
|
afterHoursEnabled: SortOrderSchema.optional(),
|
||||||
|
openPhoneReply: SortOrderSchema.optional(),
|
||||||
_count: z.lazy(() => AiSettingsCountOrderByAggregateInputObjectSchema).optional(),
|
_count: z.lazy(() => AiSettingsCountOrderByAggregateInputObjectSchema).optional(),
|
||||||
_avg: z.lazy(() => AiSettingsAvgOrderByAggregateInputObjectSchema).optional(),
|
_avg: z.lazy(() => AiSettingsAvgOrderByAggregateInputObjectSchema).optional(),
|
||||||
_max: z.lazy(() => AiSettingsMaxOrderByAggregateInputObjectSchema).optional(),
|
_max: z.lazy(() => AiSettingsMaxOrderByAggregateInputObjectSchema).optional(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const makeSchema = () => z.object({
|
|||||||
userId: SortOrderSchema.optional(),
|
userId: SortOrderSchema.optional(),
|
||||||
apiKey: SortOrderSchema.optional(),
|
apiKey: SortOrderSchema.optional(),
|
||||||
afterHoursEnabled: SortOrderSchema.optional(),
|
afterHoursEnabled: SortOrderSchema.optional(),
|
||||||
|
openPhoneReply: SortOrderSchema.optional(),
|
||||||
user: z.lazy(() => UserOrderByWithRelationInputObjectSchema).optional()
|
user: z.lazy(() => UserOrderByWithRelationInputObjectSchema).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsOrderByWithRelationInputObjectSchema: z.ZodType<Prisma.AiSettingsOrderByWithRelationInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsOrderByWithRelationInput>;
|
export const AiSettingsOrderByWithRelationInputObjectSchema: z.ZodType<Prisma.AiSettingsOrderByWithRelationInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsOrderByWithRelationInput>;
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ const aisettingsscalarwherewithaggregatesinputSchema = z.object({
|
|||||||
id: z.union([z.lazy(() => IntWithAggregatesFilterObjectSchema), z.number().int()]).optional(),
|
id: z.union([z.lazy(() => IntWithAggregatesFilterObjectSchema), z.number().int()]).optional(),
|
||||||
userId: z.union([z.lazy(() => IntWithAggregatesFilterObjectSchema), z.number().int()]).optional(),
|
userId: z.union([z.lazy(() => IntWithAggregatesFilterObjectSchema), z.number().int()]).optional(),
|
||||||
apiKey: z.union([z.lazy(() => StringWithAggregatesFilterObjectSchema), z.string()]).optional(),
|
apiKey: z.union([z.lazy(() => StringWithAggregatesFilterObjectSchema), z.string()]).optional(),
|
||||||
afterHoursEnabled: z.union([z.lazy(() => BoolWithAggregatesFilterObjectSchema), z.boolean()]).optional()
|
afterHoursEnabled: z.union([z.lazy(() => BoolWithAggregatesFilterObjectSchema), z.boolean()]).optional(),
|
||||||
|
openPhoneReply: z.union([z.lazy(() => BoolWithAggregatesFilterObjectSchema), z.boolean()]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsScalarWhereWithAggregatesInputObjectSchema: z.ZodType<Prisma.AiSettingsScalarWhereWithAggregatesInput> = aisettingsscalarwherewithaggregatesinputSchema as unknown as z.ZodType<Prisma.AiSettingsScalarWhereWithAggregatesInput>;
|
export const AiSettingsScalarWhereWithAggregatesInputObjectSchema: z.ZodType<Prisma.AiSettingsScalarWhereWithAggregatesInput> = aisettingsscalarwherewithaggregatesinputSchema as unknown as z.ZodType<Prisma.AiSettingsScalarWhereWithAggregatesInput>;
|
||||||
export const AiSettingsScalarWhereWithAggregatesInputObjectZodSchema = aisettingsscalarwherewithaggregatesinputSchema;
|
export const AiSettingsScalarWhereWithAggregatesInputObjectZodSchema = aisettingsscalarwherewithaggregatesinputSchema;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const makeSchema = () => z.object({
|
|||||||
userId: z.boolean().optional(),
|
userId: z.boolean().optional(),
|
||||||
apiKey: z.boolean().optional(),
|
apiKey: z.boolean().optional(),
|
||||||
afterHoursEnabled: z.boolean().optional(),
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional(),
|
||||||
user: z.union([z.boolean(), z.lazy(() => UserArgsObjectSchema)]).optional()
|
user: z.union([z.boolean(), z.lazy(() => UserArgsObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsSelectObjectSchema: z.ZodType<Prisma.AiSettingsSelect> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
export const AiSettingsSelectObjectSchema: z.ZodType<Prisma.AiSettingsSelect> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsSelect>;
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ const makeSchema = () => z.object({
|
|||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean().optional()
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUncheckedCreateInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedCreateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedCreateInput>;
|
export const AiSettingsUncheckedCreateInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedCreateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedCreateInput>;
|
||||||
export const AiSettingsUncheckedCreateInputObjectZodSchema = makeSchema();
|
export const AiSettingsUncheckedCreateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import type { Prisma } from '../../../generated/prisma';
|
|||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.number().int().optional(),
|
id: z.number().int().optional(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean().optional()
|
afterHoursEnabled: z.boolean().optional(),
|
||||||
|
openPhoneReply: z.boolean().optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUncheckedCreateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedCreateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedCreateWithoutUserInput>;
|
export const AiSettingsUncheckedCreateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedCreateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedCreateWithoutUserInput>;
|
||||||
export const AiSettingsUncheckedCreateWithoutUserInputObjectZodSchema = makeSchema();
|
export const AiSettingsUncheckedCreateWithoutUserInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const makeSchema = () => z.object({
|
|||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
|
openPhoneReply: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUncheckedUpdateInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedUpdateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedUpdateInput>;
|
export const AiSettingsUncheckedUpdateInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedUpdateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedUpdateInput>;
|
||||||
export const AiSettingsUncheckedUpdateInputObjectZodSchema = makeSchema();
|
export const AiSettingsUncheckedUpdateInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const makeSchema = () => z.object({
|
|||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
userId: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
|
openPhoneReply: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUncheckedUpdateManyInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedUpdateManyInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedUpdateManyInput>;
|
export const AiSettingsUncheckedUpdateManyInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedUpdateManyInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedUpdateManyInput>;
|
||||||
export const AiSettingsUncheckedUpdateManyInputObjectZodSchema = makeSchema();
|
export const AiSettingsUncheckedUpdateManyInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import { BoolFieldUpdateOperationsInputObjectSchema as BoolFieldUpdateOperations
|
|||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
id: z.union([z.number().int(), z.lazy(() => IntFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
|
openPhoneReply: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUncheckedUpdateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedUpdateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedUpdateWithoutUserInput>;
|
export const AiSettingsUncheckedUpdateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsUncheckedUpdateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUncheckedUpdateWithoutUserInput>;
|
||||||
export const AiSettingsUncheckedUpdateWithoutUserInputObjectZodSchema = makeSchema();
|
export const AiSettingsUncheckedUpdateWithoutUserInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { UserUpdateOneRequiredWithoutAiSettingsNestedInputObjectSchema as UserUp
|
|||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
|
openPhoneReply: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
user: z.lazy(() => UserUpdateOneRequiredWithoutAiSettingsNestedInputObjectSchema).optional()
|
user: z.lazy(() => UserUpdateOneRequiredWithoutAiSettingsNestedInputObjectSchema).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUpdateInputObjectSchema: z.ZodType<Prisma.AiSettingsUpdateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUpdateInput>;
|
export const AiSettingsUpdateInputObjectSchema: z.ZodType<Prisma.AiSettingsUpdateInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUpdateInput>;
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { BoolFieldUpdateOperationsInputObjectSchema as BoolFieldUpdateOperations
|
|||||||
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
|
openPhoneReply: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUpdateManyMutationInputObjectSchema: z.ZodType<Prisma.AiSettingsUpdateManyMutationInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUpdateManyMutationInput>;
|
export const AiSettingsUpdateManyMutationInputObjectSchema: z.ZodType<Prisma.AiSettingsUpdateManyMutationInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUpdateManyMutationInput>;
|
||||||
export const AiSettingsUpdateManyMutationInputObjectZodSchema = makeSchema();
|
export const AiSettingsUpdateManyMutationInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import { BoolFieldUpdateOperationsInputObjectSchema as BoolFieldUpdateOperations
|
|||||||
|
|
||||||
const makeSchema = () => z.object({
|
const makeSchema = () => z.object({
|
||||||
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
apiKey: z.union([z.string(), z.lazy(() => StringFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
afterHoursEnabled: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional(),
|
||||||
|
openPhoneReply: z.union([z.boolean(), z.lazy(() => BoolFieldUpdateOperationsInputObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsUpdateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsUpdateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUpdateWithoutUserInput>;
|
export const AiSettingsUpdateWithoutUserInputObjectSchema: z.ZodType<Prisma.AiSettingsUpdateWithoutUserInput> = makeSchema() as unknown as z.ZodType<Prisma.AiSettingsUpdateWithoutUserInput>;
|
||||||
export const AiSettingsUpdateWithoutUserInputObjectZodSchema = makeSchema();
|
export const AiSettingsUpdateWithoutUserInputObjectZodSchema = makeSchema();
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const aisettingswhereinputSchema = z.object({
|
|||||||
userId: z.union([z.lazy(() => IntFilterObjectSchema), z.number().int()]).optional(),
|
userId: z.union([z.lazy(() => IntFilterObjectSchema), z.number().int()]).optional(),
|
||||||
apiKey: z.union([z.lazy(() => StringFilterObjectSchema), z.string()]).optional(),
|
apiKey: z.union([z.lazy(() => StringFilterObjectSchema), z.string()]).optional(),
|
||||||
afterHoursEnabled: z.union([z.lazy(() => BoolFilterObjectSchema), z.boolean()]).optional(),
|
afterHoursEnabled: z.union([z.lazy(() => BoolFilterObjectSchema), z.boolean()]).optional(),
|
||||||
|
openPhoneReply: z.union([z.lazy(() => BoolFilterObjectSchema), z.boolean()]).optional(),
|
||||||
user: z.union([z.lazy(() => UserScalarRelationFilterObjectSchema), z.lazy(() => UserWhereInputObjectSchema)]).optional()
|
user: z.union([z.lazy(() => UserScalarRelationFilterObjectSchema), z.lazy(() => UserWhereInputObjectSchema)]).optional()
|
||||||
}).strict();
|
}).strict();
|
||||||
export const AiSettingsWhereInputObjectSchema: z.ZodType<Prisma.AiSettingsWhereInput> = aisettingswhereinputSchema as unknown as z.ZodType<Prisma.AiSettingsWhereInput>;
|
export const AiSettingsWhereInputObjectSchema: z.ZodType<Prisma.AiSettingsWhereInput> = aisettingswhereinputSchema as unknown as z.ZodType<Prisma.AiSettingsWhereInput>;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export const AiSettingsAggregateResultSchema = z.object({ _count: z.object({
|
|||||||
userId: z.number(),
|
userId: z.number(),
|
||||||
apiKey: z.number(),
|
apiKey: z.number(),
|
||||||
afterHoursEnabled: z.number(),
|
afterHoursEnabled: z.number(),
|
||||||
|
openPhoneReply: z.number(),
|
||||||
user: z.number()
|
user: z.number()
|
||||||
}).optional(),
|
}).optional(),
|
||||||
_sum: z.object({
|
_sum: z.object({
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export const AiSettingsCreateResultSchema = z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
});
|
});
|
||||||
@@ -4,5 +4,6 @@ export const AiSettingsDeleteResultSchema = z.nullable(z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}));
|
}));
|
||||||
@@ -4,5 +4,6 @@ export const AiSettingsFindFirstResultSchema = z.nullable(z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}));
|
}));
|
||||||
@@ -5,6 +5,7 @@ export const AiSettingsFindManyResultSchema = z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
})),
|
})),
|
||||||
pagination: z.object({
|
pagination: z.object({
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export const AiSettingsFindUniqueResultSchema = z.nullable(z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}));
|
}));
|
||||||
@@ -4,11 +4,13 @@ export const AiSettingsGroupByResultSchema = z.array(z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
_count: z.object({
|
_count: z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
userId: z.number(),
|
userId: z.number(),
|
||||||
apiKey: z.number(),
|
apiKey: z.number(),
|
||||||
afterHoursEnabled: z.number(),
|
afterHoursEnabled: z.number(),
|
||||||
|
openPhoneReply: z.number(),
|
||||||
user: z.number()
|
user: z.number()
|
||||||
}).optional(),
|
}).optional(),
|
||||||
_sum: z.object({
|
_sum: z.object({
|
||||||
|
|||||||
@@ -4,5 +4,6 @@ export const AiSettingsUpdateResultSchema = z.nullable(z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}));
|
}));
|
||||||
@@ -4,5 +4,6 @@ export const AiSettingsUpsertResultSchema = z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
});
|
});
|
||||||
@@ -5,6 +5,7 @@ export const AiSettingsInputSchema = z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const AiSettingsModelSchema = z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const AiSettingsResultSchema = z.object({
|
|||||||
userId: z.number().int(),
|
userId: z.number().int(),
|
||||||
apiKey: z.string(),
|
apiKey: z.string(),
|
||||||
afterHoursEnabled: z.boolean(),
|
afterHoursEnabled: z.boolean(),
|
||||||
|
openPhoneReply: z.boolean(),
|
||||||
user: z.unknown()
|
user: z.unknown()
|
||||||
}).strict();
|
}).strict();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user