packages/db/usedSchemas/browser.ts (the browser-resolved copy of the Patient zod schema, kept separate from the Node copy since it can't import Prisma-generated code) was missing several fields added to the real schema over time — preferredLanguage, gender, insuranceProvider, insuranceId, groupNumber, policyHolder, and the PLAN_NOT_ACCEPTED status. Since the schema isn't strict, those fields were silently stripped from every patient create/update in the browser with no validation error, so edits to them never reached the server. Also fixes the AI call greeting always playing in English regardless of the patient's preferred language — the greeting text was a single hardcoded string, unlike every later turn in the conversation which already branched by language.
211 lines
10 KiB
TypeScript
211 lines
10 KiB
TypeScript
import { prisma as db } from "@repo/db/client";
|
|
|
|
// Exported so twilio-webhooks.ts can detect an admin-customized template
|
|
// (vs. this untouched default) and swap in a translated greeting instead.
|
|
export const DEFAULT_GREETING =
|
|
"Hi {firstName}, this is Lisa, the AI assistant at {officeName}. How can I help you today?";
|
|
export const DEFAULT_RECALL_GREETING =
|
|
"Hi {firstName}, this is Lisa from {officeName}. We wanted to check in — you're due for a routine exam and cleaning, which most insurance plans cover at no cost. Would you like to schedule one?";
|
|
|
|
export type TwilioSettingsData = {
|
|
accountSid: string;
|
|
authToken: string;
|
|
phoneNumber: string;
|
|
greetingMessage?: string | null;
|
|
twimlAppSid?: string | null;
|
|
};
|
|
|
|
export type CommunicationCreateData = {
|
|
patientId: number;
|
|
userId?: number;
|
|
channel: "sms" | "voice";
|
|
direction: "outbound" | "inbound";
|
|
status: "queued" | "sent" | "delivered" | "failed" | "completed" | "busy" | "no_answer";
|
|
body?: string;
|
|
callDuration?: number;
|
|
twilioSid?: string;
|
|
};
|
|
|
|
export const twilioStorage = {
|
|
async getTwilioSettings(userId: number) {
|
|
return db.twilioSettings.findUnique({ where: { userId } });
|
|
},
|
|
|
|
async upsertTwilioSettings(userId: number, data: TwilioSettingsData) {
|
|
const { twimlAppSid, ...rest } = data;
|
|
const existing = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existingTemplates = (existing?.templates as Record<string, string>) || {};
|
|
const templates: Record<string, string> = { ...existingTemplates };
|
|
if (twimlAppSid !== undefined) {
|
|
if (twimlAppSid) templates["_twiml_app_sid"] = twimlAppSid;
|
|
else delete templates["_twiml_app_sid"];
|
|
}
|
|
return db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { ...rest, templates },
|
|
create: { userId, ...rest, templates },
|
|
});
|
|
},
|
|
|
|
async createCommunication(data: CommunicationCreateData) {
|
|
return db.communication.create({ data: data as any });
|
|
},
|
|
|
|
async getCommunicationsByPatient(patientId: number) {
|
|
return db.communication.findMany({
|
|
where: { patientId },
|
|
orderBy: { createdAt: "asc" },
|
|
});
|
|
},
|
|
|
|
async getTemplates(userId: number): Promise<Record<string, string>> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
if (!settings?.templates) return {};
|
|
return settings.templates as Record<string, string>;
|
|
},
|
|
|
|
async saveTemplate(userId: number, key: string, body: string) {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existing = (settings?.templates as Record<string, string>) || {};
|
|
const updated = { ...existing, [key]: body };
|
|
return db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { templates: updated },
|
|
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
|
|
});
|
|
},
|
|
|
|
async getAiChatTemplates(userId: number): Promise<Record<string, string>> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const all = (settings?.templates as Record<string, string>) || {};
|
|
return {
|
|
reminderGreeting: all["_ai_chat_reminder_greeting"] ?? "",
|
|
newPatientGreeting: all["_ai_chat_new_patient_greeting"] ?? "",
|
|
generalFallback: all["_ai_chat_general_fallback"] ?? "",
|
|
rescheduleGreeting: all["_ai_chat_reschedule_greeting"] ?? "",
|
|
reminderSms: all["_ai_chat_reminder_sms"] ?? "",
|
|
};
|
|
},
|
|
|
|
async saveAiChatTemplates(userId: number, templates: { reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string; rescheduleGreeting?: string; reminderSms?: string }) {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existing = (settings?.templates as Record<string, string>) || {};
|
|
const updated: Record<string, string> = { ...existing };
|
|
if (templates.reminderGreeting !== undefined) updated["_ai_chat_reminder_greeting"] = templates.reminderGreeting;
|
|
if (templates.newPatientGreeting !== undefined) updated["_ai_chat_new_patient_greeting"] = templates.newPatientGreeting;
|
|
if (templates.generalFallback !== undefined) updated["_ai_chat_general_fallback"] = templates.generalFallback;
|
|
if (templates.rescheduleGreeting !== undefined) updated["_ai_chat_reschedule_greeting"] = templates.rescheduleGreeting;
|
|
if (templates.reminderSms !== undefined) updated["_ai_chat_reminder_sms"] = templates.reminderSms;
|
|
return db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { templates: updated },
|
|
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
|
|
});
|
|
},
|
|
|
|
// ── Call Templates — wording spoken during live AI phone calls, kept
|
|
// separate from the SMS wording above (routePatientTurn() consumes
|
|
// whichever set the caller passes in). ────────────────────────────────────
|
|
|
|
async getAiCallTemplates(userId: number): Promise<Record<string, string>> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const all = (settings?.templates as Record<string, string>) || {};
|
|
return {
|
|
greeting: all["_ai_call_greeting"] ?? all["_ai_chat_call_template"] ?? DEFAULT_GREETING,
|
|
reminderGreeting: all["_ai_call_reminder_greeting"] ?? "Sure, I can help with that.",
|
|
newPatientGreeting: all["_ai_call_new_patient_greeting"] ?? "No problem, let's get you set up.",
|
|
generalFallback: all["_ai_call_general_fallback"] ?? "Sure, how can I help?",
|
|
recallGreeting: all["_ai_call_recall_greeting"] ?? DEFAULT_RECALL_GREETING,
|
|
};
|
|
},
|
|
|
|
async saveAiCallTemplates(userId: number, templates: { greeting?: string; reminderGreeting?: string; newPatientGreeting?: string; generalFallback?: string; recallGreeting?: string }) {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existing = (settings?.templates as Record<string, string>) || {};
|
|
const updated: Record<string, string> = { ...existing };
|
|
if (templates.greeting !== undefined) updated["_ai_call_greeting"] = templates.greeting;
|
|
if (templates.reminderGreeting !== undefined) updated["_ai_call_reminder_greeting"] = templates.reminderGreeting;
|
|
if (templates.newPatientGreeting !== undefined) updated["_ai_call_new_patient_greeting"] = templates.newPatientGreeting;
|
|
if (templates.generalFallback !== undefined) updated["_ai_call_general_fallback"] = templates.generalFallback;
|
|
if (templates.recallGreeting !== undefined) updated["_ai_call_recall_greeting"] = templates.recallGreeting;
|
|
return db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { templates: updated },
|
|
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
|
|
});
|
|
},
|
|
|
|
async getSmsTemplateList(userId: number): Promise<{ id: string; name: string; body: string }[]> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const all = (settings?.templates as Record<string, string>) || {};
|
|
const raw = all["_sms_template_list"];
|
|
if (!raw) return [];
|
|
try { return JSON.parse(raw); } catch { return []; }
|
|
},
|
|
|
|
async saveSmsTemplateList(userId: number, templates: { id: string; name: string; body: string }[]) {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existing = (settings?.templates as Record<string, string>) || {};
|
|
const updated: Record<string, string> = {
|
|
...existing,
|
|
"_sms_template_list": JSON.stringify(templates),
|
|
};
|
|
// Keep _ai_chat_reminder_sms in sync with the first template for batch sends
|
|
if (templates.length > 0) updated["_ai_chat_reminder_sms"] = templates[0]!.body;
|
|
return db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { templates: updated },
|
|
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
|
|
});
|
|
},
|
|
|
|
async getInternalChatSystemPrompt(userId: number): Promise<string> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const all = (settings?.templates as Record<string, string>) || {};
|
|
return all["_internal_chat_system_prompt"] ?? "";
|
|
},
|
|
|
|
async saveInternalChatSystemPrompt(userId: number, prompt: string): Promise<void> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existing = (settings?.templates as Record<string, string>) || {};
|
|
const updated = { ...existing, "_internal_chat_system_prompt": prompt };
|
|
await db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { templates: updated },
|
|
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
|
|
});
|
|
},
|
|
|
|
async getCdtAliases(userId: number): Promise<{ phrase: string; cdtCode: string }[]> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const all = (settings?.templates as Record<string, any>) || {};
|
|
const raw = all["_cdt_aliases"];
|
|
if (!Array.isArray(raw)) return [];
|
|
return raw.filter(
|
|
(r: any) => typeof r?.phrase === "string" && typeof r?.cdtCode === "string"
|
|
);
|
|
},
|
|
|
|
async saveCdtAliases(userId: number, aliases: { phrase: string; cdtCode: string }[]): Promise<void> {
|
|
const settings = await db.twilioSettings.findUnique({ where: { userId } });
|
|
const existing = (settings?.templates as Record<string, any>) || {};
|
|
const updated = { ...existing, "_cdt_aliases": aliases };
|
|
await db.twilioSettings.upsert({
|
|
where: { userId },
|
|
update: { templates: updated },
|
|
create: { userId, accountSid: "", authToken: "", phoneNumber: "", templates: updated },
|
|
});
|
|
},
|
|
|
|
async getRecentCommunicationsByUser(userId: number, limit = 20) {
|
|
return db.communication.findMany({
|
|
where: { patient: { userId } },
|
|
orderBy: { createdAt: "desc" },
|
|
take: limit,
|
|
include: {
|
|
patient: { select: { id: true, firstName: true, lastName: true, phone: true } },
|
|
},
|
|
});
|
|
},
|
|
};
|