import express, { Request, Response } from "express"; import twilio from "twilio"; import { storage } from "../storage"; import { getHandoff, setHandoff, resetConversation, startNewPatientConversation, startRescheduleConversation, getAfterHoursHandoff, setAfterHoursHandoff } from "../ai/aiHandoffStore"; const router = express.Router(); function getTwilioClient(accountSid: string, authToken: string) { return twilio(accountSid, authToken); } // GET /api/twilio/settings router.get("/settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const settings = await storage.getTwilioSettings(userId); if (!settings) return res.status(200).json(null); return res.status(200).json({ id: settings.id, accountSid: settings.accountSid, authToken: settings.authToken, phoneNumber: settings.phoneNumber, greetingMessage: settings.greetingMessage, }); } catch (err) { return res.status(500).json({ error: "Failed to fetch Twilio settings", details: String(err) }); } }); // PUT /api/twilio/settings router.put("/settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { accountSid, authToken, phoneNumber, greetingMessage } = req.body; if (!accountSid?.trim() || !authToken?.trim() || !phoneNumber?.trim()) { return res.status(400).json({ message: "accountSid, authToken, and phoneNumber are required" }); } const settings = await storage.upsertTwilioSettings(userId, { accountSid: accountSid.trim(), authToken: authToken.trim(), phoneNumber: phoneNumber.trim(), greetingMessage: greetingMessage?.trim() || null, }); return res.status(200).json({ id: settings.id, accountSid: settings.accountSid, authToken: settings.authToken, phoneNumber: settings.phoneNumber, greetingMessage: settings.greetingMessage, }); } catch (err) { return res.status(500).json({ error: "Failed to save Twilio settings", details: String(err) }); } }); // POST /api/twilio/send-sms router.post("/send-sms", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { to, message, patientId, startFlow } = req.body; if (!to || !message) return res.status(400).json({ message: "to and message are required" }); const settings = await storage.getTwilioSettings(userId); if (!settings) { return res.status(400).json({ message: "Twilio is not configured. Please add your Twilio credentials in Settings." }); } const client = getTwilioClient(settings.accountSid, settings.authToken); const twilioMsg = await client.messages.create({ body: message, from: settings.phoneNumber, to, }); if (patientId) { const pid = Number(patientId); await storage.createCommunication({ patientId: pid, userId, channel: "sms", direction: "outbound", status: "sent", body: message, twilioSid: twilioMsg.sid, }); // Set conversation stage based on which flow was started if (startFlow === "new_patient") { await startNewPatientConversation(userId, pid); } else if (startFlow === "reschedule") { await startRescheduleConversation(userId, pid); } else { await resetConversation(userId, pid); } } return res.status(200).json({ sid: twilioMsg.sid, status: twilioMsg.status }); } catch (err: any) { return res.status(500).json({ error: err.message || "Failed to send SMS" }); } }); // GET /api/twilio/templates router.get("/templates", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const templates = await storage.getTemplates(userId); return res.status(200).json(templates); } catch (err) { return res.status(500).json({ error: "Failed to fetch templates", details: String(err) }); } }); // PUT /api/twilio/templates/:key router.put("/templates/:key", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { key } = req.params; const { body } = req.body; if (!key || !body?.trim()) return res.status(400).json({ message: "key and body are required" }); await storage.saveTemplate(userId, key, body.trim()); return res.status(200).json({ key, body: body.trim() }); } catch (err) { return res.status(500).json({ error: "Failed to save template", details: String(err) }); } }); // GET /api/twilio/after-hours-handoff router.get("/after-hours-handoff", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); return res.status(200).json({ enabled: await getAfterHoursHandoff(userId) }); } catch (err) { return res.status(500).json({ error: "Failed to get after-hours handoff state" }); } }); // PUT /api/twilio/after-hours-handoff router.put("/after-hours-handoff", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { enabled } = req.body; if (typeof enabled !== "boolean") return res.status(400).json({ message: "enabled must be a boolean" }); await setAfterHoursHandoff(userId, enabled); return res.status(200).json({ enabled }); } catch (err) { return res.status(500).json({ error: "Failed to set after-hours handoff state" }); } }); // GET /api/twilio/ai-handoff/:patientId router.get("/ai-handoff/:patientId", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const patientId = parseInt(req.params.patientId); if (isNaN(patientId)) return res.status(400).json({ message: "Invalid patientId" }); return res.status(200).json({ enabled: await getHandoff(userId, patientId) }); } catch (err) { return res.status(500).json({ error: "Failed to get AI handoff state" }); } }); // PUT /api/twilio/ai-handoff/:patientId router.put("/ai-handoff/:patientId", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const patientId = parseInt(req.params.patientId); if (isNaN(patientId)) return res.status(400).json({ message: "Invalid patientId" }); const { enabled } = req.body; if (typeof enabled !== "boolean") return res.status(400).json({ message: "enabled must be a boolean" }); await setHandoff(userId, patientId, enabled); return res.status(200).json({ enabled }); } catch (err) { return res.status(500).json({ error: "Failed to set AI handoff state" }); } }); // GET /api/twilio/recent-communications router.get("/recent-communications", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const limit = Math.min(50, parseInt(req.query.limit as string) || 20); const communications = await storage.getRecentCommunicationsByUser(userId, limit); return res.status(200).json(communications); } catch (err) { return res.status(500).json({ error: "Failed to fetch recent communications", details: String(err) }); } }); // POST /api/twilio/make-call router.post("/make-call", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { to, message, patientId } = req.body; if (!to) return res.status(400).json({ message: "to is required" }); const settings = await storage.getTwilioSettings(userId); if (!settings) { return res.status(400).json({ message: "Twilio is not configured. Please add your Twilio credentials in Settings." }); } const twimlMessage = message || "Hello, this is a call from your dental office."; const twiml = `${twimlMessage}`; const client = getTwilioClient(settings.accountSid, settings.authToken); const call = await client.calls.create({ twiml, from: settings.phoneNumber, to, }); if (patientId) { await storage.createCommunication({ patientId: Number(patientId), userId, channel: "voice", direction: "outbound", status: "queued", twilioSid: call.sid, }); } return res.status(200).json({ sid: call.sid, status: call.status }); } catch (err: any) { return res.status(500).json({ error: err.message || "Failed to make call" }); } }); export default router;