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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user