From d35efe7366418a487752a26d1a454209a1e4c160 Mon Sep 17 00:00:00 2001 From: Gitead Date: Sat, 5 Sep 2026 13:11:58 -0400 Subject: [PATCH] fix: reliability fixes for UHC SCO and CMSP eligibility checks - unitedSCOEligibilityProcessor: overwrite patient's stored insuranceId when the eligibility page reports a different (updated) Member ID than the one searched with. - CMSP worker: add AngularJS late-binding retry to results-table extraction (ported from the MH worker's a0417653 fix), since every patient now routes through this worker for the accumulator check. - cmspEligibilityHistoryRemainingProcessor: stop forcing patient status to UNKNOWN when extraction fails; a failed run landing after a successful one was clobbering a correct ACTIVE/INACTIVE status. Co-Authored-By: Claude Sonnet 5 --- ...mspEligibilityHistoryRemainingProcessor.ts | 17 ++++-- .../unitedSCOEligibilityProcessor.ts | 33 ++++++++--- ..._eligibilityHistoryRemainingCheckWorker.py | 56 +++++++++++-------- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts b/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts index 2d10086b..b902fae9 100644 --- a/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts +++ b/apps/Backend/src/queue/processors/cmspEligibilityHistoryRemainingProcessor.ts @@ -101,11 +101,16 @@ export async function runCmspEligibilityHistoryRemainingProcessor( } if (patient && patient.id !== undefined) { - let newStatus = "UNKNOWN"; + // eligibility is null when extraction found no matching row (e.g. the + // AngularJS grid didn't finish rendering in time). Leave status untouched + // in that case rather than forcing UNKNOWN, so a failed run can't clobber + // a correct ACTIVE/INACTIVE status set by another (e.g. concurrent) run. + let newStatus: string | null = null; if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE"; else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE"; - const updates: any = { status: newStatus }; + const updates: any = {}; + if (newStatus) updates.status = newStatus; if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance; const remainingBenefit = seleniumResult.remaining_amount?.trim() || null; @@ -118,8 +123,12 @@ export async function runCmspEligibilityHistoryRemainingProcessor( updates.thirdPartyInsurance = seleniumResult.third_party_insurance?.trim() || null; } - await storage.updatePatient(patient.id, updates); - outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`; + if (Object.keys(updates).length > 0) { + await storage.updatePatient(patient.id, updates); + } + outputResult.patientUpdateStatus = newStatus + ? `Patient status updated to ${newStatus}` + : "Eligibility unknown — status not changed"; outputResult.remainingBenefit = remainingBenefit; outputResult.thirdPartyInsurance = updates.thirdPartyInsurance ?? null; diff --git a/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts b/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts index a78831e1..db1991b7 100644 --- a/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts +++ b/apps/Backend/src/queue/processors/unitedSCOEligibilityProcessor.ts @@ -108,16 +108,31 @@ async function processUnitedSCOResult( eligStatus === "active" || eligStatus === "y" ? "ACTIVE" : eligStatus === "inactive" ? "INACTIVE" : null; - if (newStatus) { - await storage.updatePatient(patient.id, { - status: newStatus, - insuranceProvider: "United Healthcare SCO", + const statusUpdates: any = { insuranceProvider: "United Healthcare SCO" }; + if (newStatus) statusUpdates.status = newStatus; + + // The eligibility page can report a different (updated) Member ID than the + // one we searched with — e.g. UHC MA reissues IDs. Overwrite the patient's + // stored insuranceId so future lookups use the current one. + const scrapedMemberId = + typeof seleniumResult?.memberId === "string" + ? seleniumResult.memberId.replace(/\s+/g, "") + : null; + if (scrapedMemberId && scrapedMemberId !== insuranceId) { + statusUpdates.insuranceId = scrapedMemberId; + log("unitedsco-processor", "member ID changed, overwriting on patient record", { + patientId: patient.id, + oldInsuranceId: insuranceId, + newInsuranceId: scrapedMemberId, }); - output.patientUpdateStatus = `Patient status updated to ${newStatus}`; - } else { - // Unknown eligibility — still update insuranceProvider but leave status unchanged - await storage.updatePatient(patient.id, { insuranceProvider: "United Healthcare SCO" }); - output.patientUpdateStatus = `Eligibility unknown — status not changed`; + } + + await storage.updatePatient(patient.id, statusUpdates); + output.patientUpdateStatus = newStatus + ? `Patient status updated to ${newStatus}` + : `Eligibility unknown — status not changed`; + if (statusUpdates.insuranceId) { + output.patientUpdateStatus += ` (Member ID updated from ${insuranceId} to ${statusUpdates.insuranceId})`; } // 5) Resolve PDF buffer from file path (same as DDMA) diff --git a/apps/SeleniumService/selenium_CMSP_eligibilityHistoryRemainingCheckWorker.py b/apps/SeleniumService/selenium_CMSP_eligibilityHistoryRemainingCheckWorker.py index 1f7ed7b7..349856b0 100644 --- a/apps/SeleniumService/selenium_CMSP_eligibilityHistoryRemainingCheckWorker.py +++ b/apps/SeleniumService/selenium_CMSP_eligibilityHistoryRemainingCheckWorker.py @@ -181,30 +181,38 @@ class AutomationCMSPEligibilityHistoryRemainingCheck: (By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr") ) ) - for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]: - rows = self.driver.find_elements( - By.XPATH, - f"//h4[text()='{status_label}']/following::table[1]/tbody/tr" - ) - for row in rows: - cells = row.find_elements(By.TAG_NAME, "td") - if len(cells) < 3: - continue - norm_cell = self._normalize_id(self._cell_text(cells[2])) - norm_self = self._normalize_id(self.memberId) - if norm_self and norm_cell and (norm_self in norm_cell or norm_cell in norm_self): - full_name = self._cell_text(cells[4]) if len(cells) > 4 else "" - plan_name = ( - self._cell_text(cells[6]) if len(cells) > 6 - else (self._cell_text(cells[-1]) if len(cells) > 4 else "") - ) - name_parts = full_name.split() - return { - "eligibility": elig_flag, - "firstName": name_parts[0] if name_parts else "", - "lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "", - "insurance": plan_name, - } + # AngularJS populates cell text after elements appear in the DOM. + # Retry a few times until a row with non-empty cell[2] is found. + for attempt in range(4): + for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]: + rows = self.driver.find_elements( + By.XPATH, + f"//h4[text()='{status_label}']/following::table[1]/tbody/tr" + ) + for row in rows: + cells = row.find_elements(By.TAG_NAME, "td") + if len(cells) < 3: + continue + norm_cell = self._normalize_id(self._cell_text(cells[2])) + norm_self = self._normalize_id(self.memberId) + if norm_self and norm_cell and (norm_self in norm_cell or norm_cell in norm_self): + full_name = self._cell_text(cells[4]) if len(cells) > 4 else "" + plan_name = ( + self._cell_text(cells[6]) if len(cells) > 6 + else (self._cell_text(cells[-1]) if len(cells) > 4 else "") + ) + name_parts = full_name.split() + return { + "eligibility": elig_flag, + "firstName": name_parts[0] if name_parts else "", + "lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "", + "insurance": plan_name, + } + if attempt < 3: + print(f"[extraction] cells not yet populated (attempt {attempt + 1}), retrying...") + time.sleep(1.5) + + print(f"[extraction] No matching row for memberId='{self.memberId}'") return {"eligibility": None} except Exception as e: print("Extraction error:", e)