feat: send appointment confirmation SMS after AI-booked appointments

Voice-call bookings previously only spoke the confirmation during the
live call, leaving nothing for the patient afterward. Now every
successful booking (recall, reschedule, and new-patient) also sends a
standalone confirmation text, regardless of which channel the
conversation happened on.
This commit is contained in:
ff
2026-08-07 00:28:56 -04:00
parent 67e71a02e4
commit 41bed21302
4 changed files with 56 additions and 0 deletions
+48
View File
@@ -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, (dateLabel: string, time: string) => 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<void> {
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
}
}
@@ -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 */ }
}
+2
View File
@@ -10,6 +10,7 @@ import {
parseDateOnlyFromMessage, parseTime, isOfficeDayOpen, isWithinOfficeHours,
getOfficeHoursDisplay, timeLabel,
} from "./reschedule-graph";
import { sendAppointmentConfirmationSms } from "./confirmationSms";
const GraphState = Annotation.Root({
message: Annotation<string>(),
@@ -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" };
}
+4
View File
@@ -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" };
}