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:
@@ -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;
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user