Files
DentalManagementMH06/apps/Backend/src/routes/twilio-webhooks.ts

108 lines
3.7 KiB
TypeScript

import express, { Request, Response } from "express";
import { storage } from "../storage";
import { prisma as db } from "@repo/db/client";
const router = express.Router();
// POST /api/twilio/webhook/sms (Twilio posts inbound SMS here — no auth)
router.post("/webhook/sms", async (req: Request, res: Response): Promise<any> => {
try {
const { From, Body, MessageSid } = req.body;
const normalizedFrom = (From || "").replace(/\D/g, "");
const allPatients = await db.patient.findMany({ select: { id: true, phone: true } });
const patient = allPatients.find(
(p: { id: number; phone: string | null }) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
);
if (patient) {
await storage.createCommunication({
patientId: patient.id,
channel: "sms",
direction: "inbound",
status: "delivered",
body: Body,
twilioSid: MessageSid,
});
}
res.set("Content-Type", "text/xml");
return res.send("<Response></Response>");
} catch (err) {
res.set("Content-Type", "text/xml");
return res.send("<Response></Response>");
}
});
// POST /api/twilio/webhook/voice (Twilio posts here when someone calls — no auth)
router.post("/webhook/voice", async (req: Request, res: Response): Promise<any> => {
try {
const { From, CallSid } = req.body;
const normalizedFrom = (From || "").replace(/\D/g, "");
const allPatients = await db.patient.findMany({ select: { id: true, phone: true, userId: true } });
const patient = allPatients.find(
(p: { id: number; phone: string | null; userId: number }) => p.phone && p.phone.replace(/\D/g, "") === normalizedFrom
);
let greeting = "Thank you for calling. Please leave a message after the beep and we will get back to you shortly.";
if (patient) {
const settings = await storage.getTwilioSettings(patient.userId);
if (settings?.greetingMessage?.trim()) {
greeting = settings.greetingMessage.trim();
}
}
if (patient) {
await storage.createCommunication({
patientId: patient.id,
channel: "voice",
direction: "inbound",
status: "completed",
body: "(Inbound call — voicemail below)",
twilioSid: CallSid,
});
}
const recordingCallbackUrl = `${process.env.BASE_URL || "https://communitydentistsoflowell.mydentalofficemanagement.com"}/api/twilio/webhook/voice-recording`;
const twiml = `<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say voice="alice">${greeting}</Say>
<Record maxLength="120" action="${recordingCallbackUrl}" transcribeCallback="${recordingCallbackUrl}" playBeep="true"/>
<Say voice="alice">We did not receive a recording. Goodbye.</Say>
</Response>`;
res.set("Content-Type", "text/xml");
return res.send(twiml);
} catch (err) {
res.set("Content-Type", "text/xml");
return res.send(`<?xml version="1.0" encoding="UTF-8"?><Response><Say>Thank you for calling. Please try again later.</Say></Response>`);
}
});
// POST /api/twilio/webhook/voice-recording (Twilio posts recording URL here — no auth)
router.post("/webhook/voice-recording", async (req: Request, res: Response): Promise<any> => {
try {
const { CallSid, RecordingUrl } = req.body;
if (RecordingUrl && CallSid) {
const comm = await db.communication.findFirst({ where: { twilioSid: CallSid } });
if (comm) {
await db.communication.update({
where: { id: comm.id },
data: { body: `Voicemail: ${RecordingUrl}.mp3` },
});
}
}
res.set("Content-Type", "text/xml");
return res.send("<Response></Response>");
} catch (err) {
res.set("Content-Type", "text/xml");
return res.send("<Response></Response>");
}
});
export default router;