From f8235787dbd0490af37f1fabd46e4a5501f790d8 Mon Sep 17 00:00:00 2001 From: Gitead Date: Tue, 1 Sep 2026 21:20:38 -0400 Subject: [PATCH] fix: handle DDMA site redesign in eligibility check flow DDMA now often skips the family-members results table and navigates straight to the single-patient detail page, and labels the patient's name "Subscriber name:" instead of "Name:". Update step1/step2 to detect the detail-page URL correctly (it's nested under the search path, so a plain substring check couldn't tell them apart), extract the name from the new label, and fall back to parsing "Coverage is:" for eligibility status when there's no results row to read it from. Also fix a status-text substring bug ("active" matching inside "inactive") and stop the backend from collapsing an unknown eligibility result into INACTIVE. Skip name extraction entirely when the caller already knows the patient (existing-patient recheck). Co-Authored-By: Claude Sonnet 5 --- .../processors/ddmaEligibilityProcessor.ts | 5 +- .../selenium_DDMA_eligibilityCheckWorker.py | 97 ++++++++++++++----- 2 files changed, 75 insertions(+), 27 deletions(-) diff --git a/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts b/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts index b316101c..4fe6de1d 100644 --- a/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/ddmaEligibilityProcessor.ts @@ -138,8 +138,9 @@ async function processDdmaResult( // 4) Determine and update eligibility status + insurance provider name const eligStatus = (seleniumResult?.eligibility ?? "").toLowerCase(); - const newStatus = - eligStatus === "active" || eligStatus === "y" ? "ACTIVE" : "INACTIVE"; + let newStatus = "UNKNOWN"; + if (eligStatus === "active" || eligStatus === "y") newStatus = "ACTIVE"; + else if (eligStatus === "inactive" || eligStatus === "n" || eligStatus === "termed") newStatus = "INACTIVE"; await storage.updatePatient(patient.id, { status: newStatus, insuranceProvider: "Delta Dental MA", diff --git a/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py index d1a1b1fd..2f4f5479 100755 --- a/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py @@ -366,16 +366,26 @@ class AutomationDeltaDentalMAEligibilityCheck: search_btn.click() print("[DDMA step1] Clicked Search button") - # Wait for the Members results table to render + # Wait for either the Members results table to render, or (when the + # Subscriber ID + DOB match exactly one family member) DDMA to skip + # the results list and navigate straight to the patient detail page. + # Note: the detail page URL is nested UNDER the search page path + # (".../check-patient-benefits-search/check-patient-benefit-details"), + # so "check-patient-benefits-search" alone can't distinguish them — + # only the detail page's own "-benefit-details" segment can. try: WebDriverWait(self.driver, 15).until( - EC.presence_of_element_located((By.XPATH, "//table//tbody//tr")) + lambda d: d.find_elements(By.XPATH, "//table//tbody//tr") + or "check-patient-benefit-details" in d.current_url.lower() ) time.sleep(2) except TimeoutException: print("[DDMA step1] Warning: No results table appeared within timeout") return "ERROR: INVALID SEARCH CRITERIA" + if "check-patient-benefit-details" in self.driver.current_url.lower(): + print(f"[DDMA step1] Search navigated directly to detail page: {self.driver.current_url}") + print("[DDMA step1] Search completed successfully") return "Success" @@ -390,26 +400,40 @@ class AutomationDeltaDentalMAEligibilityCheck: try: import re - # Wait for results table, then pause for full render - try: - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.XPATH, "//tbody//tr")) - ) - time.sleep(2) # Let the row content fully render after table appears - except TimeoutException: - print("[DDMA step2] Warning: Results table not found within timeout") - eligibilityText = "unknown" foundMemberId = self.memberId or "" - patientName = "" + # If the caller already knows this patient (e.g. an existing patient record + # being rechecked), use that name directly — no need to scrape/parse it off + # the DDMA page, only the eligibility status is actually needed. + patientName = f"{self.firstName} {self.lastName}".strip() if self.firstName and self.lastName else "" + known_patient_name = bool(patientName) - # The Members table lists the whole family sharing this subscriber ID — - # find the row matching this patient's DOB (falls back to last name, then first row). - rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr") - print(f"[DDMA step2] Found {len(rows)} family member row(s)") + # DDMA now sometimes skips the family-members results table entirely and + # navigates straight to the single-patient detail page (when Subscriber ID + + # DOB match exactly one person). In that case there's no members table to + # parse here — skip straight to the detail-page fallback extraction below. + already_on_detail_page = "check-patient-benefit-details" in self.driver.current_url.lower() + rows = [] target_row = None + if not already_on_detail_page: + # Wait for results table, then pause for full render + try: + WebDriverWait(self.driver, 10).until( + EC.presence_of_element_located((By.XPATH, "//tbody//tr")) + ) + time.sleep(2) # Let the row content fully render after table appears + except TimeoutException: + print("[DDMA step2] Warning: Results table not found within timeout") + + # The Members table lists the whole family sharing this subscriber ID — + # find the row matching this patient's DOB (falls back to last name, then first row). + rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr") + print(f"[DDMA step2] Found {len(rows)} family member row(s)") + else: + print(f"[DDMA step2] Already on detail page: {self.driver.current_url}") + if self.dateOfBirth and rows: try: dob_parts = self.dateOfBirth.split("-") @@ -436,17 +460,17 @@ class AutomationDeltaDentalMAEligibilityCheck: if target_row is not None: try: cells = target_row.find_elements(By.XPATH, "./td") - if cells: + if cells and not known_patient_name: patientName = cells[0].text.strip() print(f"[DDMA step2] Extracted patient name from row: '{patientName}'") if len(cells) >= 2 and cells[1].text.strip(): foundMemberId = cells[1].text.strip() if len(cells) >= 7: status_text = cells[6].text.strip().lower() - if "active" in status_text: - eligibilityText = "active" - elif "inactive" in status_text or "termed" in status_text: + if "inactive" in status_text or "termed" in status_text: eligibilityText = "inactive" + elif "active" in status_text: + eligibilityText = "active" else: eligibilityText = status_text print(f"[DDMA step2] Found eligibility status: {eligibilityText}") @@ -463,10 +487,10 @@ class AutomationDeltaDentalMAEligibilityCheck: else: print("[DDMA step2] Warning: No family member rows found in search results") - # Wait for the detail page to load (URL moves away from the search page) + # Wait for the detail page to load (URL gains the "-benefit-details" segment) try: WebDriverWait(self.driver, 15).until( - lambda d: "check-patient-benefits-search" not in d.current_url + lambda d: "check-patient-benefit-details" in d.current_url.lower() ) print(f"[DDMA step2] Navigated to detail page: {self.driver.current_url}") except Exception: @@ -522,11 +546,13 @@ class AutomationDeltaDentalMAEligibilityCheck: # forward past accessibility text to the first plausible name value. for i, line in enumerate(page_lines): # Case 1: "Name : Value" or "Name: Value" on the same line + # (DDMA's detail page labels this "Subscriber name:", so that + # prefix must be accepted rather than excluded) same_line = re.match( - r'^(?:member\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE + r'^(?:member\s+|subscriber\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE ) if same_line and not re.search( - r'(provider|group|subscriber|plan)\s+name', line, re.IGNORECASE + r'(provider|group|plan)\s+name', line, re.IGNORECASE ): candidate = same_line.group(1).strip() if looks_like_name(candidate): @@ -535,7 +561,7 @@ class AutomationDeltaDentalMAEligibilityCheck: break # Case 2: "Name" or "Name:" alone on a line — scan forward for value - elif re.match(r'^(?:member\s+)?name\s*:?\s*$', line, re.IGNORECASE): + elif re.match(r'^(?:member\s+|subscriber\s+)?name\s*:?\s*$', line, re.IGNORECASE): for j in range(i + 1, min(i + 6, len(page_lines))): candidate = page_lines[j].strip() if looks_like_name(candidate): @@ -554,6 +580,27 @@ class AutomationDeltaDentalMAEligibilityCheck: if not patientName: print("[DDMA step2] Could not extract patient name from detail page") + # Fallback: refine eligibility status from the detail page's "Coverage is:" + # label if the family-table row extraction didn't set it (e.g. DDMA + # navigated straight to the detail page, skipping the members table). + if eligibilityText == "unknown": + try: + page_text = self.driver.execute_script("return document.body.innerText;") + coverage_match = re.search( + r'coverage\s+is\s*[:\-]?\s*([A-Za-z]+)', page_text, re.IGNORECASE + ) + if coverage_match: + status_text = coverage_match.group(1).strip().lower() + if "inactive" in status_text or "termed" in status_text: + eligibilityText = "inactive" + elif "active" in status_text: + eligibilityText = "active" + else: + eligibilityText = status_text + print(f"[DDMA step2] Extracted eligibility from 'Coverage is:' label: '{eligibilityText}'") + except Exception as e: + print(f"[DDMA step2] Could not extract eligibility from 'Coverage is:' label: {e}") + # Wait for the page to be fully ready before capturing PDF try: WebDriverWait(self.driver, 30).until(