fix: complete United Ucard claim flow and remember OTP device

- Click the first visible 'Next' button on the Patient Information step
  instead of a stale hidden one, so the wizard advances after member select
- Read procedure fee from totalBilled/billedAmount (matching every other
  claim worker) instead of a fee key the frontend never sends
- Implement claim confirmation PDF capture + claim number extraction
- Check "Remember this device" on the OTP page so future logins skip OTP

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 22:45:05 -04:00
co-authored by Claude Sonnet 5
parent d7015270a8
commit 865b754ae1
3 changed files with 119 additions and 9 deletions
@@ -160,6 +160,22 @@ async def start_uniteducard_claim_run(sid: str, data: dict, url: str):
)
otp_input.clear()
otp_input.send_keys(otp_value)
# Check "Remember this device" so future logins skip OTP.
# Confirmed against live site (2026-09-15):
# <span role="checkbox" data-cy="data-checkbox-rememberDevice"
# aria-checked="false" ...>
try:
remember_checkbox = driver.find_element(By.CSS_SELECTOR,
"[data-cy='data-checkbox-rememberDevice']")
if remember_checkbox.get_attribute("aria-checked") != "true":
driver.execute_script(
"arguments[0].scrollIntoView({block:'center'});", remember_checkbox)
remember_checkbox.click()
print("[UnitedUcard Claim OTP] Checked 'Remember this device'")
except Exception as remember_err:
print(f"[UnitedUcard Claim OTP] Could not check 'Remember this device': {remember_err}")
try:
try:
verify_btn = driver.find_element(By.ID, "continuebtn")
@@ -179,6 +179,22 @@ async def start_uniteducard_run(sid: str, data: dict, url: str):
)
otp_input.clear()
otp_input.send_keys(otp_value)
# Check "Remember this device" so future logins skip OTP.
# Confirmed against live site (2026-09-15):
# <span role="checkbox" data-cy="data-checkbox-rememberDevice"
# aria-checked="false" ...>
try:
remember_checkbox = driver.find_element(By.CSS_SELECTOR,
"[data-cy='data-checkbox-rememberDevice']")
if remember_checkbox.get_attribute("aria-checked") != "true":
driver.execute_script(
"arguments[0].scrollIntoView({block:'center'});", remember_checkbox)
remember_checkbox.click()
print("[UnitedUcard OTP] Checked 'Remember this device'")
except Exception as remember_err:
print(f"[UnitedUcard OTP] Could not check 'Remember this device': {remember_err}")
try:
# Try the confirmed id first — the broader text/type
# fallback below can match the WRONG button if more
@@ -22,6 +22,8 @@ from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import time
import os
import re
import base64
from uniteducard_browser_manager import get_browser_manager
@@ -285,13 +287,26 @@ class AutomationUnitedUcardClaimSubmit:
the visible label is a plain <div> ("Next"); on the Claim Information
step it's instead a chevron_right.jpg image with no text. Either way
the click target is whichever clickable ancestor wraps it.
On the Patient Information step specifically, after a member is
selected and its details card is shown, the old text-based "Next"
div is hidden (not removed) and a new chevron_right.jpg-based Next
renders below the member info section. Both XPath branches can
therefore match simultaneously, with the hidden old one appearing
first in DOM order — so this must pick the first *visible* match,
not just the first *present* one, or it silently clicks a dead
element and the wizard never advances.
"""
try:
next_label = WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.XPATH,
"//div[normalize-space(text())='Next'] | "
"//img[contains(@src,'chevron_right')]"))
)
def _first_visible_next(driver):
for el in driver.find_elements(By.XPATH,
"//div[normalize-space(text())='Next'] | "
"//img[contains(@src,'chevron_right')]"):
if el.is_displayed():
return el
return None
next_label = WebDriverWait(self.driver, 15).until(_first_visible_next)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", next_label)
time.sleep(0.3)
# Prefer clicking a clickable ancestor (button/a/[role=button]) so
@@ -650,7 +665,7 @@ class AutomationUnitedUcardClaimSubmit:
<div onclick="saveProcCode()">Save Procedure</div>
"""
procedure_code = str(line.get("procedureCode", "")).strip()
fee = line.get("fee")
fee = line.get("totalBilled") or line.get("billedAmount") or line.get("fee")
procedure_date = line.get("procedureDate") or self.serviceDate
if not procedure_code:
@@ -811,6 +826,69 @@ class AutomationUnitedUcardClaimSubmit:
return True
def step7_save_confirmation_pdf(self):
"""TODO: capture the claim confirmation number and save/print the confirmation as PDF
into self.download_dir. Return {"pdf_path": "...", "claimNumber": "..."}."""
return {}
"""
Wait for the "Claim Submitted" confirmation page, extract the Claim
ID, and save the page as a PDF.
Confirmed against the live site (2026-09-15):
<h1>Claim Submitted</h1>
Claim ID: <a>260483578000</a>
Submitted On: Tue Sep 15 2026 22:29:39 GMT-0400 (Eastern Daylight Time)
"""
try:
WebDriverWait(self.driver, 30).until(
lambda d: "claim submitted" in d.page_source.lower()
)
time.sleep(2)
print(f"[UnitedUcard Claim step7] Confirmation page URL: {self.driver.current_url}")
claim_number = None
try:
body_text = self.driver.find_element(By.TAG_NAME, "body").text
match = re.search(r'Claim ID:?\s*(\d{6,})', body_text, re.IGNORECASE)
if match:
claim_number = match.group(1)
print(f"[UnitedUcard Claim step7] Extracted claim number: {claim_number}")
else:
match = re.search(r'\b(\d{10,})\b', body_text)
if match:
claim_number = match.group(1)
print(f"[UnitedUcard Claim step7] Extracted claim number (fallback): {claim_number}")
except Exception as e:
print(f"[UnitedUcard Claim step7] Could not extract claim number: {e}")
shared_downloads = os.path.join(_SERVICE_DIR, "downloads")
os.makedirs(shared_downloads, exist_ok=True)
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
safe_claim = ("_" + claim_number[:20]) if claim_number else ""
timestamp = time.strftime("%Y%m%d_%H%M%S")
pdf_filename = f"uniteducard_claim_confirmation_{safe_member}{safe_claim}_{timestamp}.pdf"
pdf_path = os.path.join(shared_downloads, pdf_filename)
try:
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {
"printBackground": True,
"paperWidth": 8.5,
"paperHeight": 11,
"marginTop": 0.4,
"marginBottom": 0.4,
"marginLeft": 0.4,
"marginRight": 0.4,
})
pdf_bytes = base64.b64decode(pdf_data["data"])
with open(pdf_path, "wb") as f:
f.write(pdf_bytes)
print(f"[UnitedUcard Claim step7] PDF saved: {pdf_path}")
except Exception as e:
print(f"[UnitedUcard Claim step7] PDF capture failed: {e}")
return f"ERROR: step7 PDF failed: {e}"
return {
"status": "success",
"pdf_path": pdf_path,
"claimNumber": claim_number,
}
except Exception as e:
print(f"[UnitedUcard Claim step7] Exception: {e}")
return f"ERROR: step7 failed: {e}"