108 lines
3.7 KiB
TypeScript
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;
|