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

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

View File

@@ -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(

View File

@@ -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("-");

View File

@@ -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

View File

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