feat: AI claim queue auto-return + D1120 age warning

- claims-page: after auto-submit closes the form, automatically navigate
  back to /appointments when an AI claim queue is pending resume, eliminating
  the manual navigation delay between appointments
- internal-chat-workflow: warn and block claim when D1120 (child prophy) is
  requested for a patient aged 14+ — recommend D1110 (adult prophy) instead,
  and advise manual claim if user insists on D1120

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-12 22:18:59 -04:00
parent 831f67b093
commit fd4feb3e76
4 changed files with 112 additions and 3 deletions

View File

@@ -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<ChatResponse> {
// 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;

View File

@@ -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" },