feat: United Ucard name+DOB eligibility search via chat, duplicate-record handling, Product ID as member ID

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-20 22:30:01 -04:00
co-authored by Claude Sonnet 5
parent ce25cdd2a2
commit 7ec82902d9
7 changed files with 366 additions and 64 deletions
@@ -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: