feat: add multi_claim intent for different procedures per patient + fix batch claim PDF race

- Add multi_claim intent so AI correctly handles "claim X for patient A and Y for patient B"
  instead of applying all procedures to all patients (batch_claim)
- Each patient carries their own matchedCodes in the queue
- Fix batch claim PDF race condition: chatbot queue no longer advances in closeClaim(),
  instead advances after selenium PDF is downloaded (matching column claim behavior)
- Align United SCO eligibility worker with claim worker: only fill subscriber ID + DOB,
  use treatmentLocation by ID instead of arrow-wrapper click

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-29 01:08:41 -04:00
parent cb49298b66
commit cdda91f2b4
5 changed files with 324 additions and 96 deletions

View File

@@ -47,6 +47,7 @@ export interface ChatResponse {
| "eligibility_id_ready"
| "batch_eligibility_ready"
| "batch_claim_ready"
| "multi_claim_ready"
| "batch_check_and_claim_ready"
| "check_and_claim_ready"
| "need_insurance_clarification"
@@ -345,6 +346,12 @@ export async function runInternalChatWorkflow(
return await handleBatchClaim(classification, storage, customAliases);
}
// ── Multi claim (different procedures for different patients) ─────────────
if (intent === "multi_claim") {
return await handleMultiClaim(classification, storage, customAliases);
}
// ── Claim only (no eligibility check) ─────────────────────────────────────
if (intent === "claim_only") {
@@ -810,6 +817,112 @@ async function handleBatchClaim(
};
}
// ─── multi_claim (different procedures per patient) ─────────────────────────
async function handleMultiClaim(
c: ChatClassification,
storage: StorageLike,
customAliases: { phrase: string; cdtCode: string }[]
): Promise<ChatResponse> {
const groups = c.claimGroups ?? [];
if (groups.length < 2) {
if (groups.length === 1) {
return await handleClaimOnly(
{ ...c, patientName: groups[0]!.patientName, procedureNames: groups[0]!.procedureNames, intent: "claim_only" },
storage,
customAliases
);
}
return { reply: "Please specify at least two patients with their procedures." };
}
const queue: {
patient: ResolvedPatient;
siteKey: string;
serviceDate: string | null;
appointmentId: number | null;
matchedCodes: { code: string; description: string; toothNumber?: string; toothSurface?: string; quad?: string }[];
}[] = [];
const notFound: string[] = [];
for (const group of groups) {
const name = group.patientName?.trim();
if (!name) continue;
const procedureNames = stripAttachmentRefs(group.procedureNames ?? []);
if (procedureNames.length === 0) continue;
const cdtResults: CdtResult[] = lookupCdtCodes(procedureNames, customAliases);
const matched = cdtResults.filter((r) => r.code !== null);
const unmatched = cdtResults.filter((r) => r.code === null);
if (unmatched.length > 0) {
const phrases = unmatched.map((r) => r.input);
return {
reply: `I don't recognize ${phrases.map((p) => `"${p}"`).join(", ")} for ${name}. What CDT code${phrases.length > 1 ? "s are" : " is"} ${phrases.length > 1 ? "they" : "it"}? (e.g. D0272)`,
action: "need_cdt_clarification",
actionData: { unknownPhrases: phrases, matchedSoFar: matched.map((r) => ({ code: r.code!, description: r.description })) },
};
}
const raw = await findPatientByName(name, storage);
if (!raw) {
notFound.push(name);
continue;
}
const patient = patientToResult(raw);
const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim();
const d1120Warning = checkD1120Age(matched, fullName, patient.dateOfBirth, c.appointmentDate);
if (d1120Warning) return d1120Warning;
const siteKey = resolveSiteKey(patient.insuranceProvider ?? null, c.insuranceHint ?? null) ?? "MH";
let serviceDate: string | null = c.appointmentDate ?? null;
if (!serviceDate) {
const now = new Date();
serviceDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
}
queue.push({
patient,
siteKey,
serviceDate,
appointmentId: null,
matchedCodes: matched.map((r) => ({ code: r.code!, description: r.description, toothNumber: r.toothNumber, toothSurface: r.toothSurface, quad: r.quad })),
});
}
if (notFound.length > 0 && queue.length === 0) {
return { reply: `Could not find any patients matching: ${notFound.join(", ")}. Please check the spelling.` };
}
const labels = queue.map((r) => {
const name = `${r.patient.firstName ?? ""} ${r.patient.lastName ?? ""}`.trim();
const codes = r.matchedCodes.map((c) => `${c.code}`).join(", ");
return `${name} (${codes})`;
});
let reply = `Ready to claim for ${queue.length} patients: ${labels.join("; ")}.`;
if (notFound.length > 0) {
reply += ` Could not find: ${notFound.join(", ")}.`;
}
return {
reply,
action: "multi_claim_ready",
actionData: {
queue: queue.map((r) => ({
patient: r.patient,
siteKey: r.siteKey,
serviceDate: r.serviceDate,
appointmentId: r.appointmentId,
matchedCodes: r.matchedCodes,
})),
renderingProvider: c.renderingProvider ?? null,
},
};
}
// ─── check_and_claim ─────────────────────────────────────────────────────────
async function handleCheckAndClaim(