feat: migrate SMS/voice to Telnyx (TeXML), keep browser dial pad on Twilio

Twilio now handles only the in-browser dial pad (Access Token + browser-Device
outbound TwiML); Telnyx takes over SMS, inbound voicemail, the AI voice
conversation loop, and staff-triggered outbound calls via TeXML. Adds
TelnyxSettings model, Communication.provider discriminator, new comms/routes
modules, and a real Telnyx Settings page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 15:39:43 -04:00
co-authored by Claude Sonnet 5
parent b899659e13
commit 79ac6aaa31
463 changed files with 7823 additions and 3397 deletions
+9 -7
View File
@@ -1,6 +1,6 @@
import twilio from "twilio";
import { prisma as db } from "@repo/db/client";
import { storage } from "../storage";
import { sendTelnyxSms } from "../comms/telnyx-client";
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.`,
@@ -27,20 +27,22 @@ export async function sendAppointmentConfirmationSms(
lang: string,
): Promise<void> {
try {
const [twilioSettings, patient] = await Promise.all([
storage.getTwilioSettings(userId),
const [telnyxSettings, patient] = await Promise.all([
storage.getTelnyxSettings(userId),
db.patient.findUnique({ where: { id: patientId }, select: { phone: true } }),
]);
if (!twilioSettings || !patient?.phone) return;
if (!telnyxSettings || !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 sendTelnyxSms({
apiKey: telnyxSettings.apiKey, from: telnyxSettings.phoneNumber, to: patient.phone, text: body,
messagingProfileId: telnyxSettings.messagingProfileId,
});
await storage.createCommunication({
patientId, userId, channel: "sms", direction: "outbound", status: "sent", body,
patientId, userId, channel: "sms", direction: "outbound", status: "sent", body, provider: "telnyx",
});
} catch {
// Silent — don't crash the booking flow over a notification failure
@@ -1,6 +1,6 @@
import twilio from "twilio";
import { prisma as db } from "@repo/db/client";
import { storage } from "../storage";
import { sendTelnyxSms } from "../comms/telnyx-client";
import { runReminderGraph } from "./reminder-graph";
import { runRecallGraph, runRecallBookingStep, hasDateOrTimeSignal } from "./recall-graph";
import { runNewPatientStep } from "./new-patient-graph";
@@ -62,7 +62,7 @@ export function applyOfficeName(template: string, name: string): string {
/** Save an outbound SMS message (always sms channel — used for async follow-ups). */
async function saveOutboundSms(patientId: number, body: string): Promise<void> {
await storage.createCommunication({
patientId, channel: "sms", direction: "outbound", status: "sent", body,
patientId, channel: "sms", direction: "outbound", status: "sent", body, provider: "telnyx",
});
}
@@ -132,8 +132,8 @@ async function runMassHealthCheckAndNotify(
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(patient.userId, "MH");
if (!credentials) return;
const twilioSettings = await storage.getTwilioSettings(patient.userId);
if (!twilioSettings || !patient.phone) return;
const telnyxSettings = await storage.getTelnyxSettings(patient.userId);
if (!telnyxSettings || !patient.phone) return;
await runEligibilityProcessor({
userId: patient.userId,
@@ -198,11 +198,12 @@ async function runMassHealthCheckAndNotify(
? "asked_self_pay"
: "asked_other_insurance_after_inactive";
const client = twilio(twilioSettings.accountSid, twilioSettings.authToken);
await client.messages.create({
body: resultText,
from: twilioSettings.phoneNumber,
to: patient.phone,
await sendTelnyxSms({
apiKey: telnyxSettings.apiKey,
from: telnyxSettings.phoneNumber,
to: patient.phone,
text: resultText,
messagingProfileId: telnyxSettings.messagingProfileId,
});
await saveOutboundSms(patient.id, resultText);