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.clear()
otp_input.send_keys(otp_value) 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:
try: try:
verify_btn = driver.find_element(By.ID, "continuebtn") 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.clear()
otp_input.send_keys(otp_value) 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:
# Try the confirmed id first — the broader text/type # Try the confirmed id first — the broader text/type
# fallback below can match the WRONG button if more # 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 from selenium.webdriver.support import expected_conditions as EC
import time import time
import os import os
import re
import base64
from uniteducard_browser_manager import get_browser_manager 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 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 step it's instead a chevron_right.jpg image with no text. Either way
the click target is whichever clickable ancestor wraps it. 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: try:
next_label = WebDriverWait(self.driver, 15).until( def _first_visible_next(driver):
EC.presence_of_element_located((By.XPATH, for el in driver.find_elements(By.XPATH,
"//div[normalize-space(text())='Next'] | " "//div[normalize-space(text())='Next'] | "
"//img[contains(@src,'chevron_right')]")) "//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) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", next_label)
time.sleep(0.3) time.sleep(0.3)
# Prefer clicking a clickable ancestor (button/a/[role=button]) so # Prefer clicking a clickable ancestor (button/a/[role=button]) so
@@ -650,7 +665,7 @@ class AutomationUnitedUcardClaimSubmit:
<div onclick="saveProcCode()">Save Procedure</div> <div onclick="saveProcCode()">Save Procedure</div>
""" """
procedure_code = str(line.get("procedureCode", "")).strip() 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 procedure_date = line.get("procedureDate") or self.serviceDate
if not procedure_code: if not procedure_code:
@@ -811,6 +826,69 @@ class AutomationUnitedUcardClaimSubmit:
return True return True
def step7_save_confirmation_pdf(self): 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": "..."}.""" Wait for the "Claim Submitted" confirmation page, extract the Claim
return {} 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}"