- Add eligibility_by_id and check_and_claim intents to internal chat - New cdt-lookup.ts: keyword search against fee schedule JSON (no LLM) - New internal-chat-workflow.ts: deterministic orchestration — patient resolution, insurance siteKey derivation, CDT code mapping - Custom CDT aliases stored per-user in DB (TwilioSettings JSON blob) with GET/PUT /api/ai/cdt-aliases endpoints - Chatbot UI: new steps for eligibility-id-ready, check-and-claim-ready, and need-insurance-clarification with insurance picker - Settings UI: CDT Aliases CRUD table with built-in alias reference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
172 lines
7.3 KiB
TypeScript
172 lines
7.3 KiB
TypeScript
import { prisma as db } from "@repo/db/client";
|
|
|
|
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 },
|
|
});
|
|
},
|
|
|
|
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 } },
|
|
},
|
|
});
|
|
},
|
|
};
|