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:
@@ -13,6 +13,7 @@ export type InternalChatIntent =
|
|||||||
| "find_patient" // look up patient record only
|
| "find_patient" // look up patient record only
|
||||||
| "schedule_appointment" // add patient to today's (or specified) schedule
|
| "schedule_appointment" // add patient to today's (or specified) schedule
|
||||||
| "claim_only" // submit claim for procedures (no eligibility check)
|
| "claim_only" // submit claim for procedures (no eligibility check)
|
||||||
|
| "multi_claim" // different procedures for different patients by name
|
||||||
| "preauth" // submit pre-authorization for procedures
|
| "preauth" // submit pre-authorization for procedures
|
||||||
| "navigate_claims"
|
| "navigate_claims"
|
||||||
| "navigate_schedule"
|
| "navigate_schedule"
|
||||||
@@ -35,6 +36,8 @@ export interface ChatClassification {
|
|||||||
renderingProvider?: string; // raw name, e.g. "Kai Gao", "Dr. Smith"
|
renderingProvider?: string; // raw name, e.g. "Kai Gao", "Dr. Smith"
|
||||||
// --- procedures (raw text, NOT CDT codes — CDT lookup is done in workflow) ---
|
// --- procedures (raw text, NOT CDT codes — CDT lookup is done in workflow) ---
|
||||||
procedureNames?: string[]; // for check_and_claim, e.g. ["perio exam", "adult cleaning"]
|
procedureNames?: string[]; // for check_and_claim, e.g. ["perio exam", "adult cleaning"]
|
||||||
|
// --- multi_claim (different procedures per patient) ---
|
||||||
|
claimGroups?: { patientName: string; procedureNames: string[] }[];
|
||||||
// --- scheduling ---
|
// --- scheduling ---
|
||||||
appointmentDate?: string; // for schedule_appointment, YYYY-MM-DD (omit = today)
|
appointmentDate?: string; // for schedule_appointment, YYYY-MM-DD (omit = today)
|
||||||
appointmentTime?: string; // for schedule_appointment, HH:MM 24h (omit = 09:00)
|
appointmentTime?: string; // for schedule_appointment, HH:MM 24h (omit = 09:00)
|
||||||
@@ -61,6 +64,7 @@ Respond ONLY with valid JSON (no markdown fences):
|
|||||||
"insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>",
|
"insuranceHint": "<insurance name only if explicitly stated in the message, e.g. 'masshealth', 'BCBS MA', 'CCA'>",
|
||||||
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
|
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
|
||||||
"procedureNames": ["<raw procedure name>", ...],
|
"procedureNames": ["<raw procedure name>", ...],
|
||||||
|
"claimGroups": [{"patientName": "<name>", "procedureNames": ["<procedure>", ...]}, ...],
|
||||||
"appointmentDate": "<YYYY-MM-DD; use today's date (${today}) if user says 'today'; omit only if no date is mentioned at all>",
|
"appointmentDate": "<YYYY-MM-DD; use today's date (${today}) if user says 'today'; omit only if no date is mentioned at all>",
|
||||||
"appointmentTime": "<HH:MM 24h if a specific time is mentioned, omit if not stated>",
|
"appointmentTime": "<HH:MM 24h if a specific time is mentioned, omit if not stated>",
|
||||||
"fallbackReply": "<1-2 sentence reply to show the user>"
|
"fallbackReply": "<1-2 sentence reply to show the user>"
|
||||||
@@ -94,6 +98,13 @@ Intents:
|
|||||||
e.g. "perio exam, adult cleaning for Maria and John"
|
e.g. "perio exam, adult cleaning for Maria and John"
|
||||||
Use this ONLY when procedures AND two or more patient names are given.
|
Use this ONLY when procedures AND two or more patient names are given.
|
||||||
Put each patient name into the "patientNames" array. Put procedure names in "procedureNames".
|
Put each patient name into the "patientNames" array. Put procedure names in "procedureNames".
|
||||||
|
- multi_claim : user wants to claim DIFFERENT procedures for DIFFERENT patients identified by NAME
|
||||||
|
e.g. "claim #13 crown for flor and claim d5212 for bian"
|
||||||
|
e.g. "claim perio exam for Maria and claim adult prophy for John"
|
||||||
|
e.g. "claim D0120 for Jane and D1110 for Bob"
|
||||||
|
Use this when each patient has their OWN distinct set of procedures.
|
||||||
|
Put each patient+procedures group into the "claimGroups" array.
|
||||||
|
Do NOT use batch_claim for this — batch_claim is ONLY for the SAME procedures applied to ALL patients.
|
||||||
- batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them
|
- batch_check_and_claim : user provides MULTIPLE member IDs with DOBs AND wants to claim PROCEDURES for all of them
|
||||||
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy"
|
e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000, and claim perio exam and adult prophy"
|
||||||
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000 and claim D0120 D1110 for them"
|
e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000 and claim D0120 D1110 for them"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface ChatResponse {
|
|||||||
| "eligibility_id_ready"
|
| "eligibility_id_ready"
|
||||||
| "batch_eligibility_ready"
|
| "batch_eligibility_ready"
|
||||||
| "batch_claim_ready"
|
| "batch_claim_ready"
|
||||||
|
| "multi_claim_ready"
|
||||||
| "batch_check_and_claim_ready"
|
| "batch_check_and_claim_ready"
|
||||||
| "check_and_claim_ready"
|
| "check_and_claim_ready"
|
||||||
| "need_insurance_clarification"
|
| "need_insurance_clarification"
|
||||||
@@ -345,6 +346,12 @@ export async function runInternalChatWorkflow(
|
|||||||
return await handleBatchClaim(classification, storage, customAliases);
|
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) ─────────────────────────────────────
|
// ── Claim only (no eligibility check) ─────────────────────────────────────
|
||||||
|
|
||||||
if (intent === "claim_only") {
|
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 ─────────────────────────────────────────────────────────
|
// ─── check_and_claim ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async function handleCheckAndClaim(
|
async function handleCheckAndClaim(
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ type Step =
|
|||||||
| "need-cdt-clarification"
|
| "need-cdt-clarification"
|
||||||
| "claim-ready"
|
| "claim-ready"
|
||||||
| "batch-claim-ready"
|
| "batch-claim-ready"
|
||||||
|
| "multi-claim-ready"
|
||||||
| "batch-check-and-claim-ready"
|
| "batch-check-and-claim-ready"
|
||||||
| "preauth-ready";
|
| "preauth-ready";
|
||||||
|
|
||||||
@@ -589,6 +590,16 @@ export function ChatbotButton() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.action === "multi_claim_ready" && data.actionData) {
|
||||||
|
setBatchClaimData({
|
||||||
|
queue: data.actionData.queue ?? [],
|
||||||
|
matchedCodes: [],
|
||||||
|
renderingProvider: data.actionData.renderingProvider ?? null,
|
||||||
|
});
|
||||||
|
setStep("multi-claim-ready");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.action === "claim_only_ready" && data.actionData) {
|
if (data.action === "claim_only_ready" && data.actionData) {
|
||||||
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
|
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
|
||||||
setClaimReadyData({
|
setClaimReadyData({
|
||||||
@@ -638,6 +649,7 @@ export function ChatbotButton() {
|
|||||||
step === "batch-eligibility-ready" ||
|
step === "batch-eligibility-ready" ||
|
||||||
step === "check-and-claim-ready" ||
|
step === "check-and-claim-ready" ||
|
||||||
step === "batch-claim-ready" ||
|
step === "batch-claim-ready" ||
|
||||||
|
step === "multi-claim-ready" ||
|
||||||
step === "batch-check-and-claim-ready" ||
|
step === "batch-check-and-claim-ready" ||
|
||||||
step === "need-insurance-clarification" ||
|
step === "need-insurance-clarification" ||
|
||||||
step === "need-appointment-selection";
|
step === "need-appointment-selection";
|
||||||
@@ -1193,6 +1205,72 @@ export function ChatbotButton() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Multi claim ready — different procedures per patient */}
|
||||||
|
{step === "multi-claim-ready" && batchClaimData && (
|
||||||
|
<div className="bg-orange-50 border border-orange-200 rounded-xl p-3 space-y-2">
|
||||||
|
<p className="text-xs font-semibold text-orange-800">
|
||||||
|
Claim for {batchClaimData.queue.length} patients:
|
||||||
|
</p>
|
||||||
|
{batchClaimData.queue.map((item: any, i: number) => (
|
||||||
|
<div key={i} className="pl-2 space-y-0.5">
|
||||||
|
<p className="text-xs text-orange-600 font-medium">
|
||||||
|
{i + 1}. {item.patient.firstName} {item.patient.lastName}
|
||||||
|
</p>
|
||||||
|
{item.matchedCodes?.length > 0 && (
|
||||||
|
<div className="pl-3">
|
||||||
|
{item.matchedCodes.map((c: any) => (
|
||||||
|
<p key={c.code} className="text-xs text-gray-700">
|
||||||
|
<span className="font-medium">{c.code}</span> — {c.description}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="flex flex-col gap-2 pt-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
className="w-full h-8 text-xs bg-orange-600 hover:bg-orange-700 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
const { queue, renderingProvider } = batchClaimData;
|
||||||
|
addMsg("user", `Claim all ${queue.length} patients`);
|
||||||
|
addMsg("bot", `Claiming ${queue.length} patients one by one...`);
|
||||||
|
const [first, ...rest] = queue as any[];
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({
|
||||||
|
remaining: rest,
|
||||||
|
matchedCodes: null,
|
||||||
|
renderingProvider,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
if (first!.patient.id && first!.matchedCodes?.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||||
|
codes: first!.matchedCodes,
|
||||||
|
siteKey: first!.siteKey,
|
||||||
|
serviceDate: first!.serviceDate,
|
||||||
|
autoSubmit: true,
|
||||||
|
renderingProvider: renderingProvider ?? null,
|
||||||
|
dob: first!.patient.dateOfBirth ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
setChatbotPendingFiles(pendingFiles);
|
||||||
|
markJobStarted();
|
||||||
|
const url = first!.appointmentId
|
||||||
|
? `/claims?appointmentId=${first!.appointmentId}`
|
||||||
|
: `/claims?newPatient=${first!.patient.id}`;
|
||||||
|
setTimeout(() => { setLocation(url); setOpen(false); resetStep(); }, 600);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileText className="h-3 w-3 mr-1" />
|
||||||
|
Claim All ({batchClaimData.queue.length} patients)
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="ghost" className="w-full h-8 text-xs" onClick={resetStep}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Claim ready — confirm before submitting */}
|
{/* Claim ready — confirm before submitting */}
|
||||||
{step === "claim-ready" && claimReadyData && (() => {
|
{step === "claim-ready" && claimReadyData && (() => {
|
||||||
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");
|
const [sy, sm, sd] = (claimReadyData.serviceDate ?? "").split("-");
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export default function ClaimsPage() {
|
|||||||
// CCA result: pdfFileId is already saved by the processor — open preview directly
|
// CCA result: pdfFileId is already saved by the processor — open preview directly
|
||||||
if (jobResult.pdfFileId) {
|
if (jobResult.pdfFileId) {
|
||||||
advanceAiClaimQueue();
|
advanceAiClaimQueue();
|
||||||
|
advanceChatbotClaimQueue();
|
||||||
setPreviewPdfId(jobResult.pdfFileId);
|
setPreviewPdfId(jobResult.pdfFileId);
|
||||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||||
setPreviewOpen(true);
|
setPreviewOpen(true);
|
||||||
@@ -333,6 +334,82 @@ export default function ClaimsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Advance the chatbot_claim_queue / chatbot_check_claim_queue after selenium PDF is saved.
|
||||||
|
// NOT called from closeClaim — called only after the selenium job completes and PDF is downloaded.
|
||||||
|
const advanceChatbotClaimQueue = () => {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const remaining = parsed?.remaining as any[] | undefined;
|
||||||
|
const matchedCodes = parsed?.matchedCodes ?? [];
|
||||||
|
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||||
|
if (remaining && remaining.length > 0) {
|
||||||
|
const [next, ...rest] = remaining;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||||
|
}
|
||||||
|
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
||||||
|
codes: matchedCodes,
|
||||||
|
siteKey: next.siteKey,
|
||||||
|
patientId: next.patient?.id ?? null,
|
||||||
|
memberId: next.memberId,
|
||||||
|
dob: next.dob,
|
||||||
|
serviceDate: null,
|
||||||
|
renderingProvider,
|
||||||
|
}));
|
||||||
|
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
||||||
|
memberId: next.memberId,
|
||||||
|
dob: next.dob,
|
||||||
|
autoCheck: next.autoCheck,
|
||||||
|
}));
|
||||||
|
setTimeout(() => {
|
||||||
|
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
||||||
|
setWouterLocation("/insurance-status");
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem("chatbot_check_claim_queue");
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem("chatbot_claim_queue");
|
||||||
|
if (raw) {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const remaining = parsed?.remaining as any[] | undefined;
|
||||||
|
const sharedMatchedCodes = parsed?.matchedCodes ?? [];
|
||||||
|
const renderingProvider = parsed?.renderingProvider ?? null;
|
||||||
|
if (remaining && remaining.length > 0) {
|
||||||
|
const [next, ...rest] = remaining;
|
||||||
|
if (rest.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes: sharedMatchedCodes, renderingProvider }));
|
||||||
|
} else {
|
||||||
|
sessionStorage.removeItem("chatbot_claim_queue");
|
||||||
|
}
|
||||||
|
const codes = next.matchedCodes?.length > 0 ? next.matchedCodes : sharedMatchedCodes;
|
||||||
|
if (next.patient?.id && codes.length > 0) {
|
||||||
|
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
||||||
|
codes,
|
||||||
|
siteKey: next.siteKey,
|
||||||
|
serviceDate: next.serviceDate,
|
||||||
|
autoSubmit: true,
|
||||||
|
renderingProvider,
|
||||||
|
dob: next.patient.dateOfBirth ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sessionStorage.removeItem("chatbot_claim_queue");
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
};
|
||||||
|
|
||||||
// Advance the ai_claim_queue by removing the first item (current patient).
|
// Advance the ai_claim_queue by removing the first item (current patient).
|
||||||
// Called only on successful submission so that cancel leaves the queue intact.
|
// Called only on successful submission so that cancel leaves the queue intact.
|
||||||
const advanceAiClaimQueue = () => {
|
const advanceAiClaimQueue = () => {
|
||||||
@@ -370,7 +447,10 @@ export default function ClaimsPage() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
||||||
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
||||||
if (!isDraft) advanceAiClaimQueue();
|
if (!isDraft) {
|
||||||
|
advanceAiClaimQueue();
|
||||||
|
advanceChatbotClaimQueue();
|
||||||
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -884,7 +964,10 @@ export default function ClaimsPage() {
|
|||||||
duration: isPreAuth ? 10000 : 5000,
|
duration: isPreAuth ? 10000 : 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isPreAuth) advanceAiClaimQueue();
|
if (!isPreAuth) {
|
||||||
|
advanceAiClaimQueue();
|
||||||
|
advanceChatbotClaimQueue();
|
||||||
|
}
|
||||||
|
|
||||||
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
// Pop up the final PDF so the user doesn't need to navigate to Documents
|
||||||
if (result.pdfFileId) {
|
if (result.pdfFileId) {
|
||||||
@@ -933,78 +1016,10 @@ export default function ClaimsPage() {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
// If a chatbot batch check+claim queue is pending, navigate to eligibility for the next patient
|
// Chatbot batch queues (chatbot_claim_queue, chatbot_check_claim_queue) are NOT
|
||||||
try {
|
// advanced here — they are advanced in advanceChatbotClaimQueue() which is called
|
||||||
const raw = sessionStorage.getItem("chatbot_check_claim_queue");
|
// only after the selenium job completes and PDF is downloaded. This prevents the
|
||||||
if (raw) {
|
// next patient's claim from starting before the current patient's PDF is saved.
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
const remaining = parsed?.remaining as any[] | undefined;
|
|
||||||
const matchedCodes = parsed?.matchedCodes ?? [];
|
|
||||||
const renderingProvider = parsed?.renderingProvider ?? null;
|
|
||||||
if (remaining && remaining.length > 0) {
|
|
||||||
const [next, ...rest] = remaining;
|
|
||||||
if (rest.length > 0) {
|
|
||||||
sessionStorage.setItem("chatbot_check_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
|
||||||
} else {
|
|
||||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
|
||||||
}
|
|
||||||
sessionStorage.setItem("chatbot_claim_codes", JSON.stringify({
|
|
||||||
codes: matchedCodes,
|
|
||||||
siteKey: next.siteKey,
|
|
||||||
patientId: next.patient?.id ?? null,
|
|
||||||
memberId: next.memberId,
|
|
||||||
dob: next.dob,
|
|
||||||
serviceDate: null,
|
|
||||||
renderingProvider,
|
|
||||||
}));
|
|
||||||
sessionStorage.setItem("chatbot_eligibility", JSON.stringify({
|
|
||||||
memberId: next.memberId,
|
|
||||||
dob: next.dob,
|
|
||||||
autoCheck: next.autoCheck,
|
|
||||||
}));
|
|
||||||
setTimeout(() => {
|
|
||||||
window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill"));
|
|
||||||
setWouterLocation("/insurance-status");
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sessionStorage.removeItem("chatbot_check_claim_queue");
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// If a chatbot batch claim queue is pending, open the next patient
|
|
||||||
try {
|
|
||||||
const raw = sessionStorage.getItem("chatbot_claim_queue");
|
|
||||||
if (raw) {
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
const remaining = parsed?.remaining as any[] | undefined;
|
|
||||||
const matchedCodes = parsed?.matchedCodes ?? [];
|
|
||||||
const renderingProvider = parsed?.renderingProvider ?? null;
|
|
||||||
if (remaining && remaining.length > 0) {
|
|
||||||
const [next, ...rest] = remaining;
|
|
||||||
if (rest.length > 0) {
|
|
||||||
sessionStorage.setItem("chatbot_claim_queue", JSON.stringify({ remaining: rest, matchedCodes, renderingProvider }));
|
|
||||||
} else {
|
|
||||||
sessionStorage.removeItem("chatbot_claim_queue");
|
|
||||||
}
|
|
||||||
if (next.patient?.id && matchedCodes.length > 0) {
|
|
||||||
sessionStorage.setItem("chatbot_claim_prefill", JSON.stringify({
|
|
||||||
codes: matchedCodes,
|
|
||||||
siteKey: next.siteKey,
|
|
||||||
serviceDate: next.serviceDate,
|
|
||||||
autoSubmit: true,
|
|
||||||
renderingProvider,
|
|
||||||
dob: next.patient.dateOfBirth ?? null,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
setTimeout(() => {
|
|
||||||
setWouterLocation(`/claims?newPatient=${next.patient.id}`);
|
|
||||||
}, 500);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
sessionStorage.removeItem("chatbot_claim_queue");
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Pre Auth section
|
// Pre Auth section
|
||||||
|
|||||||
@@ -423,19 +423,19 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
def step1(self):
|
def step1(self):
|
||||||
"""
|
"""
|
||||||
Navigate to Eligibility page and fill the Patient Information form.
|
Navigate to Eligibility page and fill the Patient Information form.
|
||||||
|
|
||||||
Workflow based on actual DOM testing:
|
Workflow:
|
||||||
1. Navigate directly to eligibility page
|
1. Navigate directly to eligibility page
|
||||||
2. Fill First Name (id='firstName_Back'), Last Name (id='lastName_Back'), DOB (id='dateOfBirth_Back')
|
2. Fill Subscriber ID and DOB
|
||||||
3. Select Payer: "UnitedHealthcare Massachusetts" from ng-select dropdown
|
3. Select Payer: "UnitedHealthcare Massachusetts" from ng-select dropdown
|
||||||
4. Click Continue
|
4. Click Continue
|
||||||
5. Handle Practitioner & Location page - click paymentGroupId dropdown, select Summit Dental Care
|
5. Handle Practitioner & Location page - select Treatment Location and Billing Entity
|
||||||
6. Click Continue again
|
6. Click Continue again
|
||||||
"""
|
"""
|
||||||
from selenium.webdriver.common.action_chains import ActionChains
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"[UnitedSCO step1] Starting eligibility search for: {self.firstName} {self.lastName}, DOB: {self.dateOfBirth}")
|
print(f"[UnitedSCO step1] Starting eligibility search for: memberId={self.memberId}, DOB: {self.dateOfBirth}")
|
||||||
|
|
||||||
# Navigate directly to eligibility page
|
# Navigate directly to eligibility page
|
||||||
print("[UnitedSCO step1] Navigating to eligibility page...")
|
print("[UnitedSCO step1] Navigating to eligibility page...")
|
||||||
@@ -644,23 +644,34 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
print("[UnitedSCO step1] Selecting Treatment Location...")
|
print("[UnitedSCO step1] Selecting Treatment Location...")
|
||||||
location_selected = False
|
location_selected = False
|
||||||
try:
|
try:
|
||||||
location_ng = WebDriverWait(self.driver, 10).until(
|
location_input = self.driver.find_element(By.ID, "treatmentLocation")
|
||||||
EC.element_to_be_clickable((By.XPATH,
|
if location_input.is_displayed():
|
||||||
"//label[@for='treatmentLocation']/following-sibling::ng-select | "
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_input)
|
||||||
"//label[@for='treatmentLocation']/..//ng-select"
|
location_input.click()
|
||||||
))
|
print("[UnitedSCO step1] Clicked Treatment Location dropdown")
|
||||||
)
|
time.sleep(1)
|
||||||
# Center in viewport so panel opens downward instead of upward
|
try:
|
||||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_ng)
|
summit_option = WebDriverWait(self.driver, 5).until(
|
||||||
arrow = location_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper")
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
arrow.click()
|
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||||
first_option = WebDriverWait(self.driver, 5).until(
|
))
|
||||||
EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option"))
|
)
|
||||||
)
|
summit_option.click()
|
||||||
option_text = first_option.text.strip()
|
print("[UnitedSCO step1] Selected Treatment Location: Summit Dental Care")
|
||||||
first_option.click()
|
location_selected = True
|
||||||
print(f"[UnitedSCO step1] Selected Treatment Location: {option_text}")
|
except TimeoutException:
|
||||||
location_selected = True
|
try:
|
||||||
|
first_option = self.driver.find_element(By.XPATH,
|
||||||
|
"//ng-dropdown-panel//div[contains(@class,'ng-option')]"
|
||||||
|
)
|
||||||
|
option_text = first_option.text.strip()
|
||||||
|
first_option.click()
|
||||||
|
print(f"[UnitedSCO step1] Selected Treatment Location (fallback): {option_text}")
|
||||||
|
location_selected = True
|
||||||
|
except Exception:
|
||||||
|
print("[UnitedSCO step1] No options available in Treatment Location dropdown")
|
||||||
|
ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
|
||||||
|
time.sleep(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[UnitedSCO step1] Treatment Location selection failed: {e}")
|
print(f"[UnitedSCO step1] Treatment Location selection failed: {e}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user