diff --git a/apps/Backend/src/ai/confirmationSms.ts b/apps/Backend/src/ai/confirmationSms.ts new file mode 100644 index 00000000..c850f7cc --- /dev/null +++ b/apps/Backend/src/ai/confirmationSms.ts @@ -0,0 +1,48 @@ +import twilio from "twilio"; +import { prisma as db } from "@repo/db/client"; +import { storage } from "../storage"; + +const CONFIRMATION_FALLBACKS: Record string> = { + English: (d, t) => `Confirmed! Your appointment is on ${d} at ${t}. We look forward to seeing you.`, + Spanish: (d, t) => `¡Confirmado! Su cita es el ${d} a las ${t}. Le esperamos.`, + Portuguese: (d, t) => `Confirmado! Sua consulta é em ${d} às ${t}. Aguardamos sua visita.`, + Mandarin: (d, t) => `已确认!您的预约在 ${d} ${t}。期待您的光临。`, + Cantonese: (d, t) => `已確認!您的預約喺 ${d} ${t}。期待見到您。`, + Arabic: (d, t) => `تم التأكيد! موعدك في ${d} الساعة ${t}. نتطلع لرؤيتك.`, + "Haitian Creole": (d, t) => `Konfime! Randevou ou se ${d} a ${t}. N'ap tann ou.`, +}; + +/** + * Sends a standalone appointment-confirmation text, independent of whichever + * channel (SMS chat or AI phone call) the booking conversation happened on — + * a voice-booked appointment would otherwise leave the patient with nothing + * to refer back to once the call ends. Swallows all errors so a notification + * failure never breaks the booking flow that already succeeded. + */ +export async function sendAppointmentConfirmationSms( + userId: number, + patientId: number, + dateLabel: string, + time: string, + lang: string, +): Promise { + try { + const [twilioSettings, patient] = await Promise.all([ + storage.getTwilioSettings(userId), + db.patient.findUnique({ where: { id: patientId }, select: { phone: true } }), + ]); + if (!twilioSettings || !patient?.phone) return; + + const build = CONFIRMATION_FALLBACKS[lang] ?? CONFIRMATION_FALLBACKS["English"]!; + const body = build(dateLabel, time); + + const client = twilio(twilioSettings.accountSid, twilioSettings.authToken); + await client.messages.create({ body, from: twilioSettings.phoneNumber, to: patient.phone }); + + await storage.createCommunication({ + patientId, userId, channel: "sms", direction: "outbound", status: "sent", body, + }); + } catch { + // Silent — don't crash the booking flow over a notification failure + } +} diff --git a/apps/Backend/src/ai/patient-conversation-router.ts b/apps/Backend/src/ai/patient-conversation-router.ts index d0055619..67cf7657 100644 --- a/apps/Backend/src/ai/patient-conversation-router.ts +++ b/apps/Backend/src/ai/patient-conversation-router.ts @@ -15,6 +15,7 @@ import { } from "./reschedule-graph"; import { getLlm, type AiProvider } from "./llm-factory"; import { runEligibilityProcessor } from "../queue/processors/eligibilityProcessor"; +import { sendAppointmentConfirmationSms } from "./confirmationSms"; import { getPendingReschedule, setPendingReschedule, clearPendingReschedule, setStage, type ConversationStage, @@ -604,6 +605,7 @@ export async function routePatientTurn(params: { status: "scheduled", movedByAi: true, } as any); + await sendAppointmentConfirmationSms(patient.userId, patient.id, pending.dayLabel, timeLabel(startTime), language); } catch { /* silent — message still sent, staff can create manually */ } } diff --git a/apps/Backend/src/ai/recall-graph.ts b/apps/Backend/src/ai/recall-graph.ts index 9d204670..1b6b4ce3 100644 --- a/apps/Backend/src/ai/recall-graph.ts +++ b/apps/Backend/src/ai/recall-graph.ts @@ -10,6 +10,7 @@ import { parseDateOnlyFromMessage, parseTime, isOfficeDayOpen, isWithinOfficeHours, getOfficeHoursDisplay, timeLabel, } from "./reschedule-graph"; +import { sendAppointmentConfirmationSms } from "./confirmationSms"; const GraphState = Annotation.Root({ message: Annotation(), @@ -400,6 +401,7 @@ async function bookRecallSlot( const build = slot.matchedRequest ? (BOOKED_EXACT_FALLBACKS[lang] ?? BOOKED_EXACT_FALLBACKS["English"]!) : (BOOKED_ALT_FALLBACKS[lang] ?? BOOKED_ALT_FALLBACKS["English"]!); + await sendAppointmentConfirmationSms(userId, patientId, dateLabel, timeLbl, lang); return { reply: build(dateLabel, timeLbl), nextStage: "done" }; } diff --git a/apps/Backend/src/ai/reschedule-graph.ts b/apps/Backend/src/ai/reschedule-graph.ts index 1e492c33..8cd8612c 100644 --- a/apps/Backend/src/ai/reschedule-graph.ts +++ b/apps/Backend/src/ai/reschedule-graph.ts @@ -7,6 +7,7 @@ import { getPendingReschedule, clearPendingReschedule, } from "./aiHandoffStore"; +import { sendAppointmentConfirmationSms } from "./confirmationSms"; // ── Date helpers ────────────────────────────────────────────────────────────── @@ -837,6 +838,8 @@ export async function runRescheduleStep( fallback, apiKey, provider, model ); + const datePart = displayLabel.split(" at ")[0] ?? displayLabel; + await sendAppointmentConfirmationSms(userId, patientId, datePart, timeLabel(startTime), lang); return { reply, nextStage: "done" }; } @@ -1126,6 +1129,7 @@ export async function runRescheduleStep( `You are a friendly dental office assistant. The patient's appointment has been rescheduled to ${apptLabel}. Confirm in ${lang}: say the appointment is moved to ${apptLabel} and that our dental receptionist will confirm it with them tomorrow. 2 sentences, no formatting.`, `Appointment moved to ${apptLabel}.`, fallback, apiKey, provider, model ); + await sendAppointmentConfirmationSms(userId, patientId, pending.dayLabel, timeLabel(startTime), lang); return { reply, nextStage: "done" }; }