diff --git a/apps/Backend/src/ai/internal-chat-workflow.ts b/apps/Backend/src/ai/internal-chat-workflow.ts index c483ae13..bb52d72c 100644 --- a/apps/Backend/src/ai/internal-chat-workflow.ts +++ b/apps/Backend/src/ai/internal-chat-workflow.ts @@ -65,6 +65,13 @@ export function deriveSiteKey(provider: string): string { return "MH"; } +// Returns true when the user said "united" without a specific qualifier +// (e.g. "united sco", "united healthcare") — needs clarification. +function isAmbiguousUnited(hint: string | null | undefined): boolean { + const h = (hint ?? "").toLowerCase().trim(); + return h === "united"; +} + // siteKey → autoCheck value used by the insurance-status page prefill. // For MH, pass the patient's DOB so under-21 patients route to CMSP. export function siteKeyToAutoCheck(siteKey: string, dob?: string | Date | null): string { @@ -109,6 +116,32 @@ interface StorageLike { // ─── Shared helpers ─────────────────────────────────────────────────────────── +function calcAge(dob: string | null | undefined, on?: string | null): number | null { + if (!dob) return null; + try { + const birth = new Date(dob); + const ref = on ? new Date(on) : new Date(); + let age = ref.getFullYear() - birth.getFullYear(); + const m = ref.getMonth() - birth.getMonth(); + if (m < 0 || (m === 0 && ref.getDate() < birth.getDate())) age--; + return age; + } catch { return null; } +} + +function checkD1120Age( + matched: CdtResult[], + patientName: string, + dob: string | null | undefined, + serviceDate?: string | null +): ChatResponse | null { + if (!matched.some((r) => r.code === "D1120")) return null; + const age = calcAge(dob, serviceDate); + if (age === null || age < 14) return null; + return { + reply: `${patientName} is ${age} years old. D1120 (child prophy) is only for patients under 14 — I recommend using D1110 (adult prophy) instead. If you still need D1120, please claim manually for this patient.`, + }; +} + function patientToResult(p: any): ResolvedPatient { return { id: p.id, @@ -317,6 +350,13 @@ async function handleEligibilityById( }; } + // User explicitly chose "Other United" — it's not in the app + if ((c.insuranceHint ?? "").toLowerCase().includes("other united")) { + return { + reply: "That United plan is not currently supported in the app. Please check it manually.", + }; + } + // Try to resolve existing patient for name display + insurance (use DOB to pick the right family member) const existingPatient = await findPatientByMemberId(memberId, dob, storage); const patient: ResolvedPatient | null = existingPatient @@ -331,6 +371,24 @@ async function handleEligibilityById( }; } + // "united" alone is ambiguous — there's United SCO (Dental Hub) in the app + // and other United plans that are not supported. + if (isAmbiguousUnited(c.insuranceHint) && !patient?.insuranceProvider) { + const label = patient + ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() + : `Member ID ${memberId}`; + return { + reply: `Which United plan is ${label}?`, + action: "need_insurance_clarification", + actionData: { + memberId, + dob: resolvedDob, + patient, + options: ["United SCO / Dental Hub", "Other United (not in app)"], + }, + }; + } + // Determine siteKey const siteKey = resolveSiteKey( patient?.insuranceProvider ?? null, @@ -348,7 +406,7 @@ async function handleEligibilityById( memberId, dob: resolvedDob, patient, - options: ["MassHealth", "BCBS MA", "CCA", "Tufts SCO", "Delta Dental MA", "United/DentalHub"], + options: ["MassHealth", "BCBS MA", "CCA", "Tufts SCO", "Delta Dental MA", "United SCO / Dental Hub"], }, }; } @@ -378,6 +436,13 @@ async function handleCheckAndClaim( storage: StorageLike, customAliases: { phrase: string; cdtCode: string }[] = [] ): Promise { + // User explicitly chose "Other United" — it's not in the app + if ((c.insuranceHint ?? "").toLowerCase().includes("other united")) { + return { + reply: "That United plan is not currently supported in the app. Please check it manually.", + }; + } + // 1. Resolve patient let patient: ResolvedPatient | null = null; let memberId = c.memberId?.trim() ?? null; @@ -408,6 +473,24 @@ async function handleCheckAndClaim( }; } + // "united" alone is ambiguous — clarify before proceeding + if (isAmbiguousUnited(c.insuranceHint) && !patient?.insuranceProvider) { + const label = patient + ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() + : `Member ID ${memberId}`; + return { + reply: `Which United plan is ${label}?`, + action: "need_insurance_clarification", + actionData: { + memberId, + dob: resolvedDob, + patient, + procedureNames: c.procedureNames ?? [], + options: ["United SCO / Dental Hub", "Other United (not in app)"], + }, + }; + } + // 2. Determine siteKey const siteKey = resolveSiteKey( patient?.insuranceProvider ?? null, @@ -426,7 +509,7 @@ async function handleCheckAndClaim( dob, patient, procedureNames: c.procedureNames ?? [], - options: ["MassHealth", "BCBS MA", "CCA", "Tufts SCO", "Delta Dental MA", "United/DentalHub"], + options: ["MassHealth", "BCBS MA", "CCA", "Tufts SCO", "Delta Dental MA", "United SCO / Dental Hub"], }, }; } @@ -454,6 +537,10 @@ async function handleCheckAndClaim( ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() : `Member ID ${memberId}`; + // D1120 age check — recommend D1110 for patients 14+ + const d1120Warning = checkD1120Age(matched, label, patient?.dateOfBirth ?? resolvedDob, c.appointmentDate); + if (d1120Warning) return d1120Warning; + const reply = `Ready to check eligibility for ${label} and claim: ${ matched.map((r) => `${r.code} (${r.description})`).join(", ") || "no procedures mapped" }.`; @@ -521,6 +608,10 @@ async function handleClaimOnly( }; } + // D1120 age check — recommend D1110 for patients 14+ + const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate); + if (d1120Warning) return d1120Warning; + // Resolve service date: use explicit date from message, then latest appointment, then ask let serviceDate: string | null = c.appointmentDate ?? null; let appointmentId: number | null = null; diff --git a/apps/Backend/src/data/insuranceAliases.json b/apps/Backend/src/data/insuranceAliases.json index e65c0a8e..70f52d9b 100644 --- a/apps/Backend/src/data/insuranceAliases.json +++ b/apps/Backend/src/data/insuranceAliases.json @@ -23,8 +23,10 @@ { "keyword": "united healthone sco", "siteKey": "UNITED_SCO" }, { "keyword": "united healthcare sco", "siteKey": "UNITED_SCO" }, { "keyword": "united healthcare", "siteKey": "UNITED_SCO" }, + { "keyword": "united sco / dental hub", "siteKey": "UNITED_SCO" }, { "keyword": "united sco", "siteKey": "UNITED_SCO" }, { "keyword": "dentalhub", "siteKey": "UNITED_SCO" }, + { "keyword": "dental hub", "siteKey": "UNITED_SCO" }, { "keyword": "united_sco", "siteKey": "UNITED_SCO" }, { "keyword": "blue cross blue shield", "siteKey": "BCBS_MA" }, diff --git a/apps/Frontend/src/pages/claims-page.tsx b/apps/Frontend/src/pages/claims-page.tsx index fb5e189b..486ab50d 100755 --- a/apps/Frontend/src/pages/claims-page.tsx +++ b/apps/Frontend/src/pages/claims-page.tsx @@ -813,6 +813,18 @@ export default function ClaimsPage() { setIsClaimFormOpen(false); clearUrlParams(["newPatient", "appointmentId"]); + + // If an AI claim queue is pending resume, go straight back to appointments + try { + const raw = sessionStorage.getItem("ai_claim_queue"); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed?.pendingResume && Array.isArray(parsed.appointments) && parsed.appointments.length > 0) { + setWouterLocation("/appointments"); + return; + } + } + } catch {} }; // Pre Auth section diff --git a/apps/Frontend/src/pages/insurance-status-page.tsx b/apps/Frontend/src/pages/insurance-status-page.tsx index 898f6579..b7aea242 100755 --- a/apps/Frontend/src/pages/insurance-status-page.tsx +++ b/apps/Frontend/src/pages/insurance-status-page.tsx @@ -170,6 +170,7 @@ export default function InsuranceStatusPage() { const pendingAutoCheck = useRef<"mh" | "mh-history" | "cmsp" | "ddma" | "delta-ins" | "united-sco" | "tufts-sco" | "cca" | null>(null); const [triggerTarget, setTriggerTarget] = useState(null); const pendingScrollTo = useRef(null); + const [prefillTick, setPrefillTick] = useState(0); // Prefill from chatbot useEffect(() => { @@ -188,6 +189,9 @@ export default function InsuranceStatusPage() { } if (ac) pendingAutoCheck.current = ac; sessionStorage.removeItem("chatbot_eligibility"); + // Increment tick so the auto-trigger effect re-fires even when + // memberId/dob are unchanged (same patient submitted a second time). + setPrefillTick((n) => n + 1); } catch {} }; apply(); @@ -658,7 +662,7 @@ export default function InsuranceStatusPage() { setTriggerTarget(check); } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [memberId, dateOfBirth]); + }, [memberId, dateOfBirth, prefillTick]); // small helper: remove given query params from the current URL (silent, no reload) const clearUrlParams = (params: string[]) => {