diff --git a/apps/Backend/src/ai/internal-chat-graph.ts b/apps/Backend/src/ai/internal-chat-graph.ts index 3afcc7fc..743303d4 100644 --- a/apps/Backend/src/ai/internal-chat-graph.ts +++ b/apps/Backend/src/ai/internal-chat-graph.ts @@ -5,6 +5,7 @@ import { getLlm, type AiProvider } from "./llm-factory"; export type InternalChatIntent = | "check_eligibility" // by patient name → look up in DB | "eligibility_by_id" // by explicit memberId + dob (no name) + | "eligibility_by_name_dob" // by explicit first name + last name + dob (no memberId) — United Ucard name search | "batch_eligibility" // multiple patients by memberId + dob | "batch_eligibility_by_name" // multiple patients by name (no memberId) | "batch_claim" // claim same procedures for multiple patients by name @@ -26,6 +27,8 @@ export interface ChatClassification { patientName?: string; // for check_eligibility / find_patient / schedule_appointment memberId?: string; // for eligibility_by_id / check_and_claim dob?: string; // for eligibility_by_id / check_and_claim (MM/DD/YYYY) + firstName?: string; // for eligibility_by_name_dob + lastName?: string; // for eligibility_by_name_dob // --- batch eligibility (multiple patients) --- patients?: { memberId: string; dob: string }[]; // for batch_eligibility // --- batch claim (same procedures for multiple patients by name) --- @@ -58,6 +61,8 @@ Respond ONLY with valid JSON (no markdown fences): "intent": "", "patientName": "", "memberId": "", + "firstName": "", + "lastName": "", "dob": "", "patients": [{"memberId": "", "dob": ""}, ...], "patientNames": ["", "", ...], @@ -86,6 +91,10 @@ Intents: e.g. "check mh for 100xxxx, 10/10/1988 and schedule on 4/10/2026" e.g. "check mh for 100xxxx, 10/10/1988 and make appointment on 5/1/2026" In these cases set appointmentDate to the mentioned date (YYYY-MM-DD) +- eligibility_by_name_dob : user provides a first name, last name AND date of birth but NO member ID + e.g. "check ucard Kai Gao 08/30/1997", "Kai Gao, 8/30/1997" + e.g. "canin gomez, 08/30/1997, ucard" → firstName "canin", lastName "gomez" (ALWAYS: first word = first name, all remaining words = last name, e.g. "min ping wang" → firstName "min", lastName "ping wang") + Set firstName, lastName and dob (MM/DD/YYYY). Set insuranceHint if stated (e.g. "ucard"). - batch_eligibility : user provides MULTIPLE member IDs with dates of birth in one message e.g. "check mh for 100xxxx 10/10/1988 and 200xxxx 5/5/2000" e.g. "check 100xxxx, 10/10/1988 and 200xxxx, 5/5/2000" @@ -236,6 +245,19 @@ function parseBareIdDob(message: string): { memberId: string; dob: string } | nu return null; } +// Deterministic fast-path for a bare "First Last DOB" (or "DOB First Last") message, +// e.g. "Kai Gao 08/30/1997" or "Kai Gao, 8/30/1997". Two or more alphabetic words: first word = first name, the rest = last name. +function parseBareNameDob(message: string): { firstName: string; lastName: string; dob: string } | null { + const text = message.trim(); + const dob = "(\\d{1,2}[\\/\\-]\\d{1,2}[\\/\\-]\\d{4})"; + const name = "([A-Za-z][A-Za-z'\\-]*)\\s+([A-Za-z][A-Za-z'\\-]*(?:\\s+[A-Za-z][A-Za-z'\\-]*)*)"; + const nameThenDob = text.match(new RegExp(`^${name}[\\s,]+${dob}$`)); + if (nameThenDob) return { firstName: nameThenDob[1]!, lastName: nameThenDob[2]!, dob: nameThenDob[3]!.replace(/-/g, "/") }; + const dobThenName = text.match(new RegExp(`^${dob}[\\s,]+${name}$`)); + if (dobThenName) return { firstName: dobThenName[2]!, lastName: dobThenName[3]!, dob: dobThenName[1]!.replace(/-/g, "/") }; + return null; +} + export async function classifyInternalChat( message: string, apiKey: string, @@ -261,6 +283,17 @@ export async function classifyInternalChat( }; } + const bareName = parseBareNameDob(message); + if (bareName) { + return { + intent: "eligibility_by_name_dob", + firstName: bareName.firstName, + lastName: bareName.lastName, + dob: bareName.dob, + fallbackReply: `Checking eligibility for ${bareName.firstName} ${bareName.lastName}...`, + }; + } + if (!apiKey) return fallback; // Prefer the client's local date (avoids UTC midnight rollover for US timezones) diff --git a/apps/Backend/src/ai/internal-chat-workflow.ts b/apps/Backend/src/ai/internal-chat-workflow.ts index 048467df..1d854c6a 100644 --- a/apps/Backend/src/ai/internal-chat-workflow.ts +++ b/apps/Backend/src/ai/internal-chat-workflow.ts @@ -324,6 +324,12 @@ export async function runInternalChatWorkflow( return await handleEligibilityById(classification, storage); } + // ── Eligibility by first name + last name + DOB (United Ucard name search) ─ + + if (intent === "eligibility_by_name_dob") { + return handleEligibilityByNameDob(classification); + } + // ── Batch eligibility (multiple patients) ───────────────────────────────── if (intent === "batch_eligibility") { @@ -458,6 +464,44 @@ async function handleEligibilityById( }; } +// ─── eligibility_by_name_dob ────────────────────────────────────────────────── + +// Only United Ucard supports searching by name + DOB (no member ID). +function handleEligibilityByNameDob(c: ChatClassification): ChatResponse { + // Rule: first word = first name, everything after = last name ("min ping wang" → "min" / "ping wang"). + // Re-split regardless of how the classifier divided it. + const fullName = (c.patientName?.trim() || `${c.firstName ?? ""} ${c.lastName ?? ""}`.trim()); + const parts = fullName.split(/\s+/).filter(Boolean); + const firstName = parts[0]; + const lastName = parts.length >= 2 ? parts.slice(1).join(" ") : undefined; + const dob = c.dob?.trim(); + if (!firstName || !lastName || !dob) { + return { reply: "Please provide the first name, last name and Date of Birth (MM/DD/YYYY)." }; + } + + const hint = (c.insuranceHint ?? "").toLowerCase(); + if (hint && !hint.includes("ucard") && !hint.includes("united")) { + return { + reply: "Searching by name + DOB is only supported for United Ucard. Please provide a Member ID for other plans.", + }; + } + + return { + reply: `Ready to check United Ucard eligibility for ${firstName} ${lastName} by name.`, + action: "eligibility_id_ready", + actionData: { + patient: null, + memberId: "", + firstName, + lastName, + dob, + siteKey: "UNITED_UCARD", + autoCheck: siteKeyToAutoCheck("UNITED_UCARD"), + appointmentDate: c.appointmentDate ?? null, + }, + }; +} + // ─── batch_eligibility ─────────────────────────────────────────────────────── async function handleBatchEligibility( diff --git a/apps/Backend/src/queue/processors/unitedUcardEligibilityProcessor.ts b/apps/Backend/src/queue/processors/unitedUcardEligibilityProcessor.ts index 23a17186..788bef93 100644 --- a/apps/Backend/src/queue/processors/unitedUcardEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/unitedUcardEligibilityProcessor.ts @@ -66,12 +66,16 @@ export interface UnitedUcardEligibilityProcessorResult { async function processUnitedUcardResult( userId: number, - insuranceId: string, + searchedInsuranceId: string, formFirstName: string | undefined, formLastName: string | undefined, formDob: string | undefined, seleniumResult: any ): Promise { + // Name-search jobs have no member ID up front; the worker returns the page's Product ID instead. + const scrapedId = + typeof seleniumResult?.memberId === "string" ? seleniumResult.memberId.replace(/\s+/g, "") : ""; + const insuranceId = searchedInsuranceId || scrapedId; const output: UnitedUcardEligibilityProcessorResult = {}; let createdPdfFileId: number | string | null = null; @@ -82,21 +86,50 @@ async function processUnitedUcardResult( ? seleniumResult.patientName.trim() : null; - const { firstName, lastName } = rawName + // Name-search jobs (no member ID) always use the names the user typed. + const { firstName, lastName } = rawName && searchedInsuranceId ? splitName(rawName) : { firstName: formFirstName ?? "", lastName: formLastName ?? "" }; - // 2) Create / update patient - await createOrUpdatePatientByInsuranceId({ - insuranceId, - firstName, - lastName, - dob: formDob, - userId, - }); - - // 3) Fetch patient - const patient = await storage.getPatientByInsuranceId(insuranceId); + // 2-3) Resolve the patient record. + // Name + DOB searches have no member ID (the portal masks it), so we can only + // update a patient that is already on file — match on first/last name + DOB. + let patient: any = null; + if (insuranceId) { + await createOrUpdatePatientByInsuranceId({ + insuranceId, + firstName, + lastName, + dob: formDob, + userId, + }); + patient = await storage.getPatientByInsuranceId(insuranceId); + } else { + const dobDate = formDob ? new Date(formDob) : null; + const matches = await storage.searchPatients({ + filters: { + firstName: { equals: firstName, mode: "insensitive" }, + lastName: { equals: lastName, mode: "insensitive" }, + ...(dobDate && !isNaN(dobDate.getTime()) ? { dateOfBirth: dobDate } : {}), + }, + limit: 1, + offset: 0, + }); + patient = matches?.[0] ?? null; + if (!patient) { + // The portal masks the subscriber ID, so create the patient from the typed name + DOB + // (no insuranceId); the Member ID can be added later. + patient = await storage.createPatient({ + firstName, + lastName, + dateOfBirth: dobDate && !isNaN(dobDate.getTime()) ? dobDate : null, + gender: "", + phone: "", + userId, + } as any); + log("uniteducard-processor", "created patient from name search", { patientId: patient?.id }); + } + } if (!patient?.id) { output.patientUpdateStatus = "Patient not found; no update performed"; return output; @@ -115,7 +148,7 @@ async function processUnitedUcardResult( typeof seleniumResult?.memberId === "string" ? seleniumResult.memberId.replace(/\s+/g, "") : null; - if (scrapedMemberId && scrapedMemberId !== insuranceId) { + if (insuranceId && scrapedMemberId && scrapedMemberId !== insuranceId) { statusUpdates.insuranceId = scrapedMemberId; log("uniteducard-processor", "member ID changed, overwriting on patient record", { patientId: patient.id, diff --git a/apps/Frontend/src/components/insurance-status/united-ucard-button-modal.tsx b/apps/Frontend/src/components/insurance-status/united-ucard-button-modal.tsx index 21063ce4..0aaf9d24 100644 --- a/apps/Frontend/src/components/insurance-status/united-ucard-button-modal.tsx +++ b/apps/Frontend/src/components/insurance-status/united-ucard-button-modal.tsx @@ -113,10 +113,10 @@ export function UnitedUcardEligibilityButton({ const [isSubmittingOtp, setIsSubmittingOtp] = useState(false); const handleStart = async () => { - if (!memberId || !dateOfBirth) { + if (!dateOfBirth || (!memberId && !(firstName && lastName))) { toast({ title: "Missing fields", - description: "Member ID and Date of Birth are required.", + description: "Date of Birth plus either Member ID or First + Last Name are required.", variant: "destructive", }); return; diff --git a/apps/Frontend/src/components/layout/chatbot.tsx b/apps/Frontend/src/components/layout/chatbot.tsx index d5c291ae..73bc8441 100644 --- a/apps/Frontend/src/components/layout/chatbot.tsx +++ b/apps/Frontend/src/components/layout/chatbot.tsx @@ -231,7 +231,7 @@ export function ChatbotButton() { // Snapshot of insuranceId/dateOfBirth as originally fetched, so we can tell whether the // user hand-edited them in the patient-found card and only PUT to the DB when they did. const originalPatientResultRef = useRef<{ insuranceId: string | null; dateOfBirth: string | null } | null>(null); - const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null); + const [eligibilityIdData, setEligibilityIdData] = useState<{ memberId: string; firstName?: string; lastName?: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null; appointmentDate?: string | null } | null>(null); const [batchEligibilityData, setBatchEligibilityData] = useState<{ memberId: string; dob: string; siteKey: string; autoCheck: string; patient: PatientResult | null }[] | null>(null); const [checkAndClaimData, setCheckAndClaimData] = useState(null); const [clarificationData, setClarificationData] = useState<{ memberId: string; dob: string; patient: PatientResult | null; procedureNames: string[]; options: string[] } | null>(null); @@ -562,8 +562,8 @@ export function ChatbotButton() { prefillAndNavigate(patientResult.insuranceId, patientResult.dateOfBirth, getAutoCheck(patientResult.dateOfBirth)); }; - const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string) => { - sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck })); + const prefillAndNavigate = (memberId: string, dobISO: string, autoCheck: string, names?: { firstName?: string; lastName?: string }) => { + sessionStorage.setItem("chatbot_eligibility", JSON.stringify({ memberId, dob: dobISO, autoCheck, firstName: names?.firstName, lastName: names?.lastName })); window.dispatchEvent(new CustomEvent("chatbot:eligibility-prefill")); markJobStarted(); if (pendingFolderRenameRef.current) { @@ -598,8 +598,13 @@ export function ChatbotButton() { if (!eligibilityIdData) return; addMsg("user", "Check eligibility now"); addMsg("bot", "Opening the eligibility check page..."); - await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id); - prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck); + if (eligibilityIdData.memberId) { + await syncEditedPatientFields(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.patient?.id); + } + prefillAndNavigate(eligibilityIdData.memberId, eligibilityIdData.dob, eligibilityIdData.autoCheck, { + firstName: eligibilityIdData.firstName, + lastName: eligibilityIdData.lastName, + }); }; const handleBatchEligibilityRun = () => { @@ -759,6 +764,8 @@ export function ChatbotButton() { if (data.action === "eligibility_id_ready" && data.actionData) { setEligibilityIdData({ memberId: data.actionData.memberId, + firstName: data.actionData.firstName, + lastName: data.actionData.lastName, dob: data.actionData.dob, siteKey: data.actionData.siteKey, autoCheck: data.actionData.autoCheck, @@ -1134,6 +1141,30 @@ export function ChatbotButton() { className="h-7 text-xs bg-white" /> + {eligibilityIdData.firstName !== undefined && ( +
+
+ + + setEligibilityIdData((prev) => (prev ? { ...prev, firstName: e.target.value } : prev)) + } + className="h-7 text-xs bg-white" + /> +
+
+ + + setEligibilityIdData((prev) => (prev ? { ...prev, lastName: e.target.value } : prev)) + } + className="h-7 text-xs bg-white" + /> +
+
+ )}
{ - if (!pendingAutoCheck.current || !memberId || !dateOfBirth) return; + if (!pendingAutoCheck.current || !dateOfBirth) return; + if (!memberId && !(pendingAutoCheck.current === "united-ucard" && firstName && lastName)) return; const check = pendingAutoCheck.current; pendingAutoCheck.current = null; if (check === "mh" || check === "mh-history") { @@ -1209,7 +1214,7 @@ export default function InsuranceStatusPage() { dateOfBirth={dateOfBirth} firstName={firstName} lastName={lastName} - isFormIncomplete={isFormIncomplete} + isFormIncomplete={isUcardFormIncomplete} autoTrigger={triggerTarget === "united-ucard"} onAutoTriggered={() => setTriggerTarget(null)} onPdfReady={async (pdfId, fallbackFilename) => { diff --git a/apps/SeleniumService/selenium_UnitedUcard_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_UnitedUcard_eligibilityCheckWorker.py index 510a8abc..206bb6ab 100644 --- a/apps/SeleniumService/selenium_UnitedUcard_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_UnitedUcard_eligibilityCheckWorker.py @@ -394,48 +394,183 @@ class AutomationUnitedUcardEligibilityCheck: except Exception as e: return f"ERROR: Could not enter Service Date: {e}" - # Member Date of Birth - try: - dob_digits = re.sub(r"\D", "", self._format_dob(self.dateOfBirth)) - _fill_field("eligMemDob", dob_digits, "Member DOB") - except Exception as e: - return f"ERROR: Could not enter Member Date of Birth: {e}" + # Attempt 1: Subscriber ID + DOB (default "Search By" radio). + # Skipped if we have no member ID. + if self.memberId: + err = self._search_by_subscriber_id(_fill_field) + if err: + return err + if self._search_succeeded(): + print("[UnitedUcard step1] Eligibility search submitted (Subscriber ID)") + return "Success" + print("[UnitedUcard step1] Subscriber ID + DOB search did not return a result") + else: + print("[UnitedUcard step1] No memberId provided; going straight to name search") - # Subscriber ID - try: - _fill_field("eligSubsId", self.memberId, "Subscriber ID") - except Exception as e: - return f"ERROR: Could not enter Subscriber ID: {e}" + # Attempt 2: fall back to First Name + Last Name + DOB. + if not (self.firstName and self.lastName and self.dateOfBirth): + return "ERROR: Subscriber ID + DOB search failed and first name / last name / DOB are not all available for a name search" - time.sleep(0.5) - - # Click SEARCH button - try: - search_btn = WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.XPATH, - "//input[@type='button' and @value='search']")) - ) - self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", search_btn) - time.sleep(0.3) - try: - search_btn.click() - except Exception: - self.driver.execute_script("arguments[0].click();", search_btn) - print("[UnitedUcard step1] Clicked SEARCH button") - except Exception as e: - return f"ERROR: Could not click SEARCH button: {e}" - - time.sleep(3) - - # TODO: confirm what the results look like (same page update? new - # tab? modal?) once we see it, then extend step2() accordingly. - print("[UnitedUcard step1] Eligibility search submitted") - return "Success" + print(f"[UnitedUcard step1] Falling back to name search: {self.firstName} {self.lastName}, DOB={self.dateOfBirth}") + err = self._search_by_name(_fill_field) + if err: + return err + if self._search_succeeded(): + print("[UnitedUcard step1] Eligibility search submitted (Name)") + return "Success" + return "ERROR: Eligibility search returned no result by Subscriber ID + DOB or by Name + DOB" except Exception as e: print(f"[UnitedUcard step1] Exception: {e}") return f"ERROR:STEP1 - {e}" + def _click_search(self): + search_btn = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.XPATH, + "//input[@type='button' and @value='search']")) + ) + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", search_btn) + time.sleep(0.3) + try: + search_btn.click() + except Exception: + self.driver.execute_script("arguments[0].click();", search_btn) + print("[UnitedUcard step1] Clicked SEARCH button") + time.sleep(3) + + def _select_search_by(self, value): + """Click the 'Search By' radio (value 'searchbyname' or the subscriber ID one).""" + radio = WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, f"input[type='radio'][name='searchby'][value='{value}']")) + ) + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", radio) + try: + radio.click() + except Exception: + self.driver.execute_script("arguments[0].click();", radio) + time.sleep(1) + print(f"[UnitedUcard step1] Selected Search By radio: {value}") + + def _search_by_subscriber_id(self, _fill_field): + """Returns an error string, or None once SEARCH has been clicked.""" + try: + dob_digits = re.sub(r"\D", "", self._format_dob(self.dateOfBirth)) + _fill_field("eligMemDob", dob_digits, "Member DOB") + except Exception as e: + return f"ERROR: Could not enter Member Date of Birth: {e}" + try: + _fill_field("eligSubsId", self.memberId, "Subscriber ID") + except Exception as e: + return f"ERROR: Could not enter Subscriber ID: {e}" + time.sleep(0.5) + try: + self._click_search() + except Exception as e: + return f"ERROR: Could not click SEARCH button: {e}" + return None + + def _search_by_name(self, _fill_field): + """Switch to the 'Name' radio and search by first/last name + DOB. + Returns an error string, or None once SEARCH has been clicked.""" + try: + self._select_search_by("searchbyname") + except Exception as e: + return f"ERROR: Could not select the Name search radio: {e}" + + # Switching the radio re-renders the form, so re-enter Service Date too. + try: + from datetime import date + _fill_field("serviceDate", date.today().strftime("%m%d%Y"), "Service Date") + except Exception as e: + return f"ERROR: Could not enter Service Date: {e}" + + for field_id, value, label in (("eligFname", self.firstName, "First Name"), + ("eligLname", self.lastName, "Last Name")): + try: + _fill_field(field_id, value, label) + except Exception as e: + return f"ERROR: Could not enter {label}: {e}" + + try: + dob_digits = re.sub(r"\D", "", self._format_dob(self.dateOfBirth)) + _fill_field("eligMemDob", dob_digits, "Member DOB") + except Exception as e: + return f"ERROR: Could not enter Member Date of Birth: {e}" + + time.sleep(0.5) + try: + self._click_search() + except Exception as e: + return f"ERROR: Could not click SEARCH button: {e}" + return None + + def _handle_duplicate_records(self): + """If the portal lists duplicate member records ("Duplicate records have been + found."), select the Active one (falling back to the last row) and resubmit. + Returns True if a record was selected.""" + radios = self.driver.find_elements(By.CSS_SELECTOR, "input[type='radio'][name='refineMemDupliGroup']") + radios = [r for r in radios if r.is_displayed()] + if not radios: + return False + + chosen = None + for r in radios: + try: + row_text = r.find_element(By.XPATH, "./ancestor::tr[1]").text.lower() + except Exception: + row_text = "" + if "active" in row_text and "termed" not in row_text and "inactive" not in row_text: + chosen = r + break + if chosen is None: + chosen = radios[-1] + + print(f"[UnitedUcard step1] {len(radios)} duplicate records; selecting value={chosen.get_attribute('value')}") + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", chosen) + try: + chosen.click() + except Exception: + self.driver.execute_script("arguments[0].click();", chosen) + time.sleep(1.5) + + # Click the "select" button (ng-click="refineSearch()") shown below the duplicate list. + try: + select_btn = WebDriverWait(self.driver, 10).until( + EC.element_to_be_clickable((By.XPATH, + "//input[@type='button' and @value='select' and contains(@ng-click,'refineSearch')]")) + ) + self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", select_btn) + try: + select_btn.click() + except Exception: + self.driver.execute_script("arguments[0].click();", select_btn) + print("[UnitedUcard step1] Clicked select button for duplicate record") + except Exception as e: + print(f"[UnitedUcard step1] Could not click select button: {e}") + return True + + def _search_succeeded(self): + """True once the browser lands on the Eligibility Summary page; False as + soon as the form shows a "Member not found" error (or after 30s). + Duplicate-record lists are resolved along the way.""" + deadline = time.time() + 30 + duplicates_handled = False + while time.time() < deadline: + try: + if "eligibility-summary" in self.driver.current_url.lower(): + return True + body = self.driver.find_element(By.TAG_NAME, "body").text.lower() + if "member not found" in body: + print("[UnitedUcard step1] Portal reported: Member not found") + return False + if not duplicates_handled and self._handle_duplicate_records(): + duplicates_handled = True + continue + except Exception as e: + print(f"[UnitedUcard step1] Result check error: {e}") + time.sleep(0.5) + return False + def step2(self): """ Extract patient/eligibility info from the "Eligibility Summary" page @@ -462,6 +597,13 @@ class AutomationUnitedUcardEligibilityCheck: ) except Exception: pass + # Wait for the summary content (not just the page shell) before reading/printing it. + try: + WebDriverWait(self.driver, 20).until( + lambda d: "insurance information" in d.find_element(By.TAG_NAME, "body").text.lower() + ) + except Exception: + print("[UnitedUcard step2] WARNING: 'Insurance Information' not visible yet; capturing anyway") time.sleep(2) current_url = self.driver.current_url @@ -522,8 +664,21 @@ class AutomationUnitedUcardEligibilityCheck: if not name_extracted: print("[UnitedUcard step2] WARNING: Could not extract patient name from page; using form values") + # ── Member ID ──────────────────────────────────────────────── + # The Subscriber ID badge is masked, but "Product ID" is the full ID + # (the value the Subscriber ID search field accepts, e.g. "D0033034"). + # This page looks the same however the search was done (ID or name), + # so always prefer it; fall back to the ID we searched with. + memberId = self.memberId + pid_match = re.search(r'Product ID:\s*([A-Za-z0-9]+)', page_text) + if pid_match: + memberId = pid_match.group(1) + print(f"[UnitedUcard step2] Member ID from Product ID: {memberId}") + else: + print(f"[UnitedUcard step2] WARNING: Product ID not found on page; using searched ID '{memberId}'") + # ── Capture PDF ────────────────────────────────────────────── - pdf_path = self._capture_pdf(self.memberId) + pdf_path = self._capture_pdf(memberId) if not pdf_path: return {"status": "error", "message": "STEP2 FAILED: Could not generate PDF"} @@ -539,10 +694,9 @@ class AutomationUnitedUcardEligibilityCheck: "ss_path": pdf_path, "pdf_path": pdf_path, "patientName": patientName, - # NOTE: Subscriber ID shown on this page is masked (e.g. - # "******444"), so we return the ID we searched with rather - # than a scraped-and-truncated value. - "memberId": self.memberId, + # NOTE: the Subscriber ID badge is masked (e.g. "******444"), so + # we return the page's Product ID (falling back to the searched ID). + "memberId": memberId, } except Exception as e: