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
|
||||
| "schedule_appointment" // add patient to today's (or specified) schedule
|
||||
| "claim_only" // submit claim for procedures (no eligibility check)
|
||||
| "multi_claim" // different procedures for different patients by name
|
||||
| "preauth" // submit pre-authorization for procedures
|
||||
| "navigate_claims"
|
||||
| "navigate_schedule"
|
||||
@@ -35,6 +36,8 @@ export interface ChatClassification {
|
||||
renderingProvider?: string; // raw name, e.g. "Kai Gao", "Dr. Smith"
|
||||
// --- procedures (raw text, NOT CDT codes — CDT lookup is done in workflow) ---
|
||||
procedureNames?: string[]; // for check_and_claim, e.g. ["perio exam", "adult cleaning"]
|
||||
// --- multi_claim (different procedures per patient) ---
|
||||
claimGroups?: { patientName: string; procedureNames: string[] }[];
|
||||
// --- scheduling ---
|
||||
appointmentDate?: string; // for schedule_appointment, YYYY-MM-DD (omit = today)
|
||||
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'>",
|
||||
"renderingProvider": "<provider/doctor name only if explicitly stated, e.g. 'Kai Gao', 'Dr. Smith' — omit if not mentioned>",
|
||||
"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>",
|
||||
"appointmentTime": "<HH:MM 24h if a specific time is mentioned, omit if not stated>",
|
||||
"fallbackReply": "<1-2 sentence reply to show the user>"
|
||||
@@ -94,6 +98,13 @@ Intents:
|
||||
e.g. "perio exam, adult cleaning for Maria and John"
|
||||
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".
|
||||
- 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
|
||||
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"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -33,6 +33,7 @@ type Step =
|
||||
| "need-cdt-clarification"
|
||||
| "claim-ready"
|
||||
| "batch-claim-ready"
|
||||
| "multi-claim-ready"
|
||||
| "batch-check-and-claim-ready"
|
||||
| "preauth-ready";
|
||||
|
||||
@@ -589,6 +590,16 @@ export function ChatbotButton() {
|
||||
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) {
|
||||
const { patient, matchedCodes, siteKey, serviceDate, appointmentId, renderingProvider } = data.actionData;
|
||||
setClaimReadyData({
|
||||
@@ -638,6 +649,7 @@ export function ChatbotButton() {
|
||||
step === "batch-eligibility-ready" ||
|
||||
step === "check-and-claim-ready" ||
|
||||
step === "batch-claim-ready" ||
|
||||
step === "multi-claim-ready" ||
|
||||
step === "batch-check-and-claim-ready" ||
|
||||
step === "need-insurance-clarification" ||
|
||||
step === "need-appointment-selection";
|
||||
@@ -1193,6 +1205,72 @@ export function ChatbotButton() {
|
||||
</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 */}
|
||||
{step === "claim-ready" && claimReadyData && (() => {
|
||||
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
|
||||
if (jobResult.pdfFileId) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
setPreviewPdfId(jobResult.pdfFileId);
|
||||
setPreviewFallbackFilename(jobResult.pdfFilename ?? `cca_claim_${jobResult.claimNumber ?? "unknown"}.pdf`);
|
||||
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).
|
||||
// Called only on successful submission so that cancel leaves the queue intact.
|
||||
const advanceAiClaimQueue = () => {
|
||||
@@ -370,7 +447,10 @@ export default function ClaimsPage() {
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data?.message || "Failed to save claim");
|
||||
queryClient.invalidateQueries({ queryKey: QK_CLAIMS_BASE });
|
||||
if (!isDraft) advanceAiClaimQueue();
|
||||
if (!isDraft) {
|
||||
advanceAiClaimQueue();
|
||||
advanceChatbotClaimQueue();
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -884,7 +964,10 @@ export default function ClaimsPage() {
|
||||
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
|
||||
if (result.pdfFileId) {
|
||||
@@ -933,78 +1016,10 @@ export default function ClaimsPage() {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
// If a chatbot batch check+claim queue is pending, navigate to eligibility for the next patient
|
||||
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 {}
|
||||
|
||||
// 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 {}
|
||||
// Chatbot batch queues (chatbot_claim_queue, chatbot_check_claim_queue) are NOT
|
||||
// advanced here — they are advanced in advanceChatbotClaimQueue() which is called
|
||||
// only after the selenium job completes and PDF is downloaded. This prevents the
|
||||
// next patient's claim from starting before the current patient's PDF is saved.
|
||||
};
|
||||
|
||||
// Pre Auth section
|
||||
|
||||
@@ -423,19 +423,19 @@ class AutomationUnitedSCOEligibilityCheck:
|
||||
def step1(self):
|
||||
"""
|
||||
Navigate to Eligibility page and fill the Patient Information form.
|
||||
|
||||
Workflow based on actual DOM testing:
|
||||
|
||||
Workflow:
|
||||
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
|
||||
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
|
||||
"""
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
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
|
||||
print("[UnitedSCO step1] Navigating to eligibility page...")
|
||||
@@ -644,23 +644,34 @@ class AutomationUnitedSCOEligibilityCheck:
|
||||
print("[UnitedSCO step1] Selecting Treatment Location...")
|
||||
location_selected = False
|
||||
try:
|
||||
location_ng = WebDriverWait(self.driver, 10).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//label[@for='treatmentLocation']/following-sibling::ng-select | "
|
||||
"//label[@for='treatmentLocation']/..//ng-select"
|
||||
))
|
||||
)
|
||||
# Center in viewport so panel opens downward instead of upward
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_ng)
|
||||
arrow = location_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper")
|
||||
arrow.click()
|
||||
first_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option"))
|
||||
)
|
||||
option_text = first_option.text.strip()
|
||||
first_option.click()
|
||||
print(f"[UnitedSCO step1] Selected Treatment Location: {option_text}")
|
||||
location_selected = True
|
||||
location_input = self.driver.find_element(By.ID, "treatmentLocation")
|
||||
if location_input.is_displayed():
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_input)
|
||||
location_input.click()
|
||||
print("[UnitedSCO step1] Clicked Treatment Location dropdown")
|
||||
time.sleep(1)
|
||||
try:
|
||||
summit_option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//ng-dropdown-panel//div[contains(@class,'ng-option') and contains(.,'Summit Dental Care')]"
|
||||
))
|
||||
)
|
||||
summit_option.click()
|
||||
print("[UnitedSCO step1] Selected Treatment Location: Summit Dental Care")
|
||||
location_selected = True
|
||||
except TimeoutException:
|
||||
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:
|
||||
print(f"[UnitedSCO step1] Treatment Location selection failed: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user