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 <noreply@anthropic.com>
This commit is contained in:
2026-09-05 13:11:58 -04:00
co-authored by Claude Sonnet 5
parent 74d5c7c7ff
commit d35efe7366
3 changed files with 69 additions and 37 deletions
@@ -101,11 +101,16 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
} }
if (patient && patient.id !== undefined) { 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"; if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE";
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE"; 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; if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
const remainingBenefit = seleniumResult.remaining_amount?.trim() || null; const remainingBenefit = seleniumResult.remaining_amount?.trim() || null;
@@ -118,8 +123,12 @@ export async function runCmspEligibilityHistoryRemainingProcessor(
updates.thirdPartyInsurance = seleniumResult.third_party_insurance?.trim() || null; updates.thirdPartyInsurance = seleniumResult.third_party_insurance?.trim() || null;
} }
if (Object.keys(updates).length > 0) {
await storage.updatePatient(patient.id, updates); await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`; }
outputResult.patientUpdateStatus = newStatus
? `Patient status updated to ${newStatus}`
: "Eligibility unknown — status not changed";
outputResult.remainingBenefit = remainingBenefit; outputResult.remainingBenefit = remainingBenefit;
outputResult.thirdPartyInsurance = updates.thirdPartyInsurance ?? null; outputResult.thirdPartyInsurance = updates.thirdPartyInsurance ?? null;
@@ -108,16 +108,31 @@ async function processUnitedSCOResult(
eligStatus === "active" || eligStatus === "y" ? "ACTIVE" : eligStatus === "active" || eligStatus === "y" ? "ACTIVE" :
eligStatus === "inactive" ? "INACTIVE" : null; eligStatus === "inactive" ? "INACTIVE" : null;
if (newStatus) { const statusUpdates: any = { insuranceProvider: "United Healthcare SCO" };
await storage.updatePatient(patient.id, { if (newStatus) statusUpdates.status = newStatus;
status: newStatus,
insuranceProvider: "United Healthcare SCO", // 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, statusUpdates);
await storage.updatePatient(patient.id, { insuranceProvider: "United Healthcare SCO" }); output.patientUpdateStatus = newStatus
output.patientUpdateStatus = `Eligibility unknown — status not changed`; ? `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) // 5) Resolve PDF buffer from file path (same as DDMA)
@@ -181,6 +181,9 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
(By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr") (By.XPATH, "//h4[text()='Eligible' or text()='Ineligible']/following::table[1]/tbody/tr")
) )
) )
# 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")]: for status_label, elig_flag in [("Eligible", "Y"), ("Ineligible", "N")]:
rows = self.driver.find_elements( rows = self.driver.find_elements(
By.XPATH, By.XPATH,
@@ -205,6 +208,11 @@ class AutomationCMSPEligibilityHistoryRemainingCheck:
"lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "", "lastName": " ".join(name_parts[1:]) if len(name_parts) > 1 else "",
"insurance": plan_name, "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} return {"eligibility": None}
except Exception as e: except Exception as e:
print("Extraction error:", e) print("Extraction error:", e)