Files
DentalManagement09/apps/SeleniumService/selenium_UnitedUcard_claimSubmitWorker.py
T
GiteadandClaude Sonnet 5 865b754ae1 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>
2026-09-15 22:45:05 -04:00

895 lines
42 KiB
Python

"""
United Ucard (UHC Dental / OneHealthcareID) claim submission worker.
Scaffolded from selenium_UnitedUcard_eligibilityCheckWorker.py — same shape
(config_driver / login / claim steps) so it plugs into the same
helpers_uniteducard_claim.py + agent.py session/OTP pattern, and shares the
persistent browser (uniteducard_browser_manager.py) with the eligibility
worker so a device already trusted by an eligibility check skips OTP here too.
Login flow is identical to the eligibility worker (confirmed against the
live site, 2026-09-13) — see selenium_UnitedUcard_eligibilityCheckWorker.py
for the full annotated step-by-step.
The claim-specific steps below (search patient / open claim form / fill
service lines / attach files / submit / save confirmation PDF) are left as
TODO stubs — fill these in once we know the exact DOM for the claim
submission flow on the UHC Dental provider dashboard.
"""
from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium.webdriver.common.by import By
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
_SERVICE_DIR = os.path.dirname(os.path.abspath(__file__))
_BACKEND_CWD = os.path.normpath(os.path.join(_SERVICE_DIR, "..", "Backend"))
class AutomationUnitedUcardClaimSubmit:
def __init__(self, data):
self.headless = False
self.driver = None
self.data = data.get("claim", {}) if isinstance(data, dict) else {}
# Flatten values for convenience
self.memberId = self.data.get("memberId", "")
self.dateOfBirth = self.data.get("dateOfBirth", "")
self.serviceDate = self.data.get("serviceDate", "")
self.firstName = self.data.get("firstName", "")
self.lastName = self.data.get("lastName", "")
self.serviceLines = self.data.get("serviceLines", [])
self.claimFiles = self.data.get("claimFiles", [])
self.providerName = self.data.get("providerName", "")
self.uniteducard_username = self.data.get("uniteducardUsername", "")
self.uniteducard_password = self.data.get("uniteducardPassword", "")
self.payment_group_id = self.data.get("paymentGroupId", "")
# Use browser manager's download dir (shared with eligibility worker)
self.download_dir = get_browser_manager().download_dir
os.makedirs(self.download_dir, exist_ok=True)
def config_driver(self):
# Use persistent browser from manager (keeps device trust tokens,
# shared with the eligibility worker's login session)
self.driver = get_browser_manager().get_driver(self.headless)
def _force_logout(self):
"""Force logout by clearing cookies for the One Healthcare ID domain."""
try:
print("[UnitedUcard Claim login] Forcing logout due to credential change...")
browser_manager = get_browser_manager()
try:
self.driver.delete_all_cookies()
print("[UnitedUcard Claim login] Cleared all cookies")
except Exception as e:
print(f"[UnitedUcard Claim login] Error clearing cookies: {e}")
browser_manager.clear_credentials_hash()
print("[UnitedUcard Claim login] Logout complete")
return True
except Exception as e:
print(f"[UnitedUcard Claim login] Error during forced logout: {e}")
return False
def login(self, url):
"""
Log in via https://www.uhcdental.com/content/provider/dental.html (Sign In ->
Provider Login), which OIDC-redirects through identity.onehealthcareid.com.
Identical flow to AutomationUnitedUcardEligibilityCheck.login().
Returns one of:
"ALREADY_LOGGED_IN" / "SUCCESS" - logged in, caller should proceed to claim steps
"OTP_REQUIRED" - caller should poll for OTP entry
"ERROR: <msg>" - login failed
"""
wait = WebDriverWait(self.driver, 30)
browser_manager = get_browser_manager()
try:
if self.uniteducard_username and browser_manager.credentials_changed(self.uniteducard_username):
self._force_logout()
self.driver.get(url)
time.sleep(2)
self.driver.get(url)
time.sleep(3)
current_url = self.driver.current_url
print(f"[UnitedUcard Claim login] Current URL: {current_url}")
if "secure.uhcdental.com" in current_url.lower():
print("[UnitedUcard Claim login] Already logged in - on UHC Dental dashboard")
return "ALREADY_LOGGED_IN"
if "onehealthcareid.com" not in current_url.lower():
try:
signin_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH,
"//span[contains(@class,'header-signin-btn') or contains(text(),'Sign In')]"))
)
signin_btn.click()
print("[UnitedUcard Claim login] Clicked 'Sign In' to open dropdown")
time.sleep(1)
except TimeoutException:
return "ERROR: 'Sign In' button not found on UHC Dental provider site"
clicked_provider_login = False
try:
provider_login_link = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH,
"//a[normalize-space(text())='Provider Login']"))
)
self.driver.execute_script(
"arguments[0].scrollIntoView({block:'center'});", provider_login_link
)
time.sleep(0.3)
try:
provider_login_link.click()
print("[UnitedUcard Claim login] Clicked 'Provider Login' (direct)")
clicked_provider_login = True
except Exception as direct_click_err:
print(f"[UnitedUcard Claim login] Direct click failed ({direct_click_err}); trying JS click")
self.driver.execute_script("arguments[0].click();", provider_login_link)
print("[UnitedUcard Claim login] Clicked 'Provider Login' (JS)")
clicked_provider_login = True
time.sleep(3)
except TimeoutException:
pass
if not clicked_provider_login:
try:
dropdown_link = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH,
"(//a[contains(@class,'sign-in-button-link')])[1]"))
)
self.driver.execute_script("arguments[0].click();", dropdown_link)
print("[UnitedUcard Claim login] Clicked first dropdown link (Provider Login, by position)")
clicked_provider_login = True
time.sleep(3)
except Exception as e:
return f"ERROR: 'Provider Login' link not found in Sign In dropdown: {e}"
current_url = self.driver.current_url
print(f"[UnitedUcard Claim login] After Provider Login click URL: {current_url}")
if "secure.uhcdental.com" in current_url.lower():
print("[UnitedUcard Claim login] Already logged in - redirected straight to dashboard")
return "ALREADY_LOGGED_IN"
try:
WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.XPATH,
"//input[@type='tel' or contains(@placeholder,'code') or contains(@placeholder,'Code') or "
"contains(@aria-label,'Verification') or contains(@id,'otp') or contains(@name,'otp')]"))
)
print("[UnitedUcard Claim login] OTP input found on landing")
return "OTP_REQUIRED"
except TimeoutException:
pass
try:
username_field = wait.until(
EC.element_to_be_clickable((By.ID, "username"))
)
username_field.clear()
username_field.send_keys(self.uniteducard_username)
print(f"[UnitedUcard Claim login] Entered username: {self.uniteducard_username}")
except TimeoutException:
return "ERROR: Username field not found on login page"
try:
continue_btn = wait.until(
EC.element_to_be_clickable((By.ID, "btnLogin"))
)
continue_btn.click()
print("[UnitedUcard Claim login] Clicked Continue (btnLogin) after username")
time.sleep(2)
except TimeoutException:
return "ERROR: Continue button (btnLogin) not found after entering username"
try:
password_field = wait.until(
EC.presence_of_element_located((By.ID, "login-pwd"))
)
password_field.clear()
password_field.send_keys(self.uniteducard_password)
print("[UnitedUcard Claim login] Entered password")
except TimeoutException:
return "ERROR: Password field not found on login page"
try:
signin_button = wait.until(
EC.element_to_be_clickable((By.ID, "btnLogin"))
)
signin_button.click()
print("[UnitedUcard Claim login] Clicked Continue (btnLogin) on password page")
except TimeoutException:
return "ERROR: Continue button (btnLogin) not found on password page"
if self.uniteducard_username:
browser_manager.save_credentials_hash(self.uniteducard_username)
time.sleep(5)
current_url_after_login = self.driver.current_url.lower()
print(f"[UnitedUcard Claim login] After login URL: {current_url_after_login}")
if "secure.uhcdental.com" in current_url_after_login:
print("[UnitedUcard Claim login] Login successful - redirected to UHC Dental dashboard")
return "SUCCESS"
if "rba/options" in current_url_after_login:
try:
text_msg_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.ID, "textMsg"))
)
text_msg_btn.click()
print("[UnitedUcard Claim login] Clicked 'Via Text Message' on MFA options page")
time.sleep(3)
except TimeoutException:
print("[UnitedUcard Claim login] MFA options page detected but 'Via Text Message' button not found")
current_url_after_login = self.driver.current_url.lower()
print(f"[UnitedUcard Claim login] After MFA method selection URL: {current_url_after_login}")
try:
WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.XPATH,
"//input[@type='tel' or contains(@placeholder,'code') or contains(@placeholder,'Code') or "
"contains(@aria-label,'Verification') or contains(@aria-label,'verification') or "
"contains(@name,'otp') or contains(@name,'code') or contains(@id,'otp') or contains(@id,'code')]"
))
)
print("[UnitedUcard Claim login] OTP input detected -> OTP_REQUIRED")
return "OTP_REQUIRED"
except TimeoutException:
print("[UnitedUcard Claim login] No OTP input detected")
current_url_after_login = self.driver.current_url.lower()
if "secure.uhcdental.com" in current_url_after_login:
print("[UnitedUcard Claim login] Login successful - redirected to UHC Dental dashboard")
return "SUCCESS"
try:
error_elem = self.driver.find_element(By.XPATH,
"//*[contains(@class,'error') or contains(@class,'alert')]")
error_text = error_elem.text
if error_text:
print(f"[UnitedUcard Claim login] Error on page: {error_text}")
return f"ERROR: {error_text}"
except Exception:
pass
return "ERROR: Login did not complete - still on identity domain"
except Exception as e:
print(f"[UnitedUcard Claim login] Exception: {e}")
return f"ERROR:LOGIN FAILED: {e}"
# ------------------------------------------------------------------
# Claim steps — the UHC Dental "Dental Claims" flow is a wizard:
# Provider Information -> Patient Information -> Claim Information ->
# Upload Documents -> Review and Sign
# Each step should return "ERROR: <message>" on failure, or a truthy
# result on success.
# ------------------------------------------------------------------
def _click_next(self, context: str = ""):
"""Click the wizard's bottom 'Next' pill button.
Confirmed against the live site (2026-09-13): on most wizard steps
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:
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
# the wizard's own click handler fires; fall back to the div itself.
target = next_label
try:
target = next_label.find_element(By.XPATH,
"ancestor::*[self::button or self::a or @role='button'][1]")
except Exception:
pass
try:
target.click()
except Exception:
self.driver.execute_script("arguments[0].click();", target)
print(f"[UnitedUcard Claim] Clicked 'Next'{f' ({context})' if context else ''}")
time.sleep(2)
return True
except TimeoutException:
return f"ERROR: 'Next' button not found{f' on {context}' if context else ''}"
def step1_open_claims_page(self):
"""
Click 'Claims' in the top nav to open the Dental Claims wizard.
Confirmed against the live site (2026-09-13):
<a id="Claims" onclick="preLoginProviderSearch(event , 'Claims')"
href="/content/dental-benefits-provider/en/secure/dental-claim-form.html">Claims</a>
Lands on the "Provider Information" step, pre-filled from the
logged-in account (phone, email, treating location, NPI, TIN).
"""
try:
claims_link = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.ID, "Claims"))
)
claims_link.click()
print("[UnitedUcard Claim] Clicked 'Claims' nav link")
except Exception as click_err:
try:
self.driver.get(
"https://www.uhcdental.com/content/dental-benefits-provider/en/secure/dental-claim-form.html"
)
print("[UnitedUcard Claim] Navigated directly to dental-claim-form.html (nav click failed)")
except Exception as nav_err:
return f"ERROR: Could not open Claims page: {click_err} / {nav_err}"
try:
WebDriverWait(self.driver, 20).until(
EC.presence_of_element_located((By.XPATH,
"//*[contains(text(),'Provider Information')]"))
)
print("[UnitedUcard Claim] Dental Claims wizard loaded - Provider Information step")
return True
except TimeoutException:
return "ERROR: Provider Information step did not load after clicking Claims"
def step2_confirm_provider_information(self):
"""
Provider Information is pre-filled from the account (Phone, Email,
Treating Location, NPI, TIN) — nothing to fill, just advance.
"""
return self._click_next("Provider Information step")
def _format_date_digits(self, date_str):
"""Convert YYYY-MM-DD to MMDDYYYY (no separators) for typing into a
native <input type="date"> — Chrome's date widget auto-advances
between the month/day/year segments as digits are typed."""
if date_str and "-" in date_str:
parts = date_str.split("-")
if len(parts) == 3:
yyyy, mm, dd = parts
return f"{mm}{dd}{yyyy}"
return date_str
def step3_fill_patient_information(self):
"""
Fill the Patient Information step: Date of Service, Subscriber ID,
Date of Birth, then click Search to look up the member.
Confirmed against the live site (2026-09-13):
<input type="date" class="date-input date-of-service" placeholder="mm/dd/yyyy">
<input type="text" class="subscriber-id">
<input type="date" class="date-input date-of-birth" placeholder="mm/dd/yyyy">
<button class="search-button member-search">Search</button>
"""
try:
service_date_field = WebDriverWait(self.driver, 20).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "input.date-of-service"))
)
service_date_field.click()
service_date_field.send_keys(self._format_date_digits(self.serviceDate))
print(f"[UnitedUcard Claim] Entered Date of Service: {self.serviceDate}")
except TimeoutException:
return "ERROR: Date of Service field not found on Patient Information step"
try:
subscriber_id_field = self.driver.find_element(By.CSS_SELECTOR, "input.subscriber-id")
subscriber_id_field.clear()
subscriber_id_field.send_keys(self.memberId)
print(f"[UnitedUcard Claim] Entered Subscriber ID: {self.memberId}")
except Exception as e:
return f"ERROR: Subscriber ID field not found on Patient Information step: {e}"
try:
dob_field = self.driver.find_element(By.CSS_SELECTOR, "input.date-of-birth")
dob_field.click()
dob_field.send_keys(self._format_date_digits(self.dateOfBirth))
print(f"[UnitedUcard Claim] Entered Date of Birth: {self.dateOfBirth}")
except Exception as e:
return f"ERROR: Date of Birth field not found on Patient Information step: {e}"
try:
search_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "button.search-button.member-search"))
)
search_btn.click()
print("[UnitedUcard Claim] Clicked Search (member-search)")
time.sleep(2)
except TimeoutException:
return "ERROR: Search button not found on Patient Information step"
return self._select_search_result()
def _select_search_result(self):
"""
When the Subscriber ID + DOB search matches multiple coverage records
("Duplicate records have been found"), select the LAST row in the
results table (the default/most-recent coverage record) and click
SELECT. If the "Member Details" name link doesn't show up afterward
(the last row turned out to be an inactive coverage record — seen
for some providers' patients, e.g. provider "JOHN"), fall back to
selecting the FIRST row instead and click SELECT again.
Confirmed against the live site (2026-09-13): a results table with
columns Name / Group / Coverage End Date / Address, a radio button
in the first cell of each row, and CANCEL / SELECT pill buttons
below the table.
"""
try:
result_rows = WebDriverWait(self.driver, 15).until(
lambda d: d.find_elements(By.CSS_SELECTOR, "table tbody tr") or None
)
except TimeoutException:
# A single unambiguous match may skip the results table entirely
# and advance straight to Claim Information.
try:
WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(text(),'Claim Information')]"))
)
print("[UnitedUcard Claim] Single match — already advanced to Claim Information")
return True
except TimeoutException:
return "ERROR: No search results table found after Search"
if not result_rows:
return "ERROR: No matching patient records found"
total_rows = len(result_rows)
result = self._select_row_and_wait_for_member_link(-1, "last", total_rows, member_link_timeout=8)
if result is True:
return self._click_next("Patient Information step")
if result != "NO_MEMBER_LINK":
return result # propagate a hard ERROR (radio/select button not found)
if total_rows < 2:
return "ERROR: Member Details link not found after selecting last row"
print("[UnitedUcard Claim] Last row showed no active member — retrying with first row")
try:
WebDriverWait(self.driver, 10).until(
lambda d: d.find_elements(By.CSS_SELECTOR, "table tbody tr") or None
)
except TimeoutException:
return "ERROR: Results table no longer available to retry with first row"
result = self._select_row_and_wait_for_member_link(0, "first", total_rows, member_link_timeout=15)
if result is True:
return self._click_next("Patient Information step")
if result == "NO_MEMBER_LINK":
return "ERROR: Member Details link not found after selecting either the last or first row"
return result
def _select_row_and_wait_for_member_link(self, row_index, row_label, total_rows, member_link_timeout):
"""
Clicks the radio button in the row at `row_index` (re-located fresh,
since a prior failed SELECT may have altered the DOM), clicks SELECT,
then waits up to `member_link_timeout` seconds for the "Member
Details" name link to appear.
Returns True (link found & clicked), "NO_MEMBER_LINK" (link never
appeared — likely an inactive record), or an "ERROR: ..." string for
a hard failure (radio/select button not found).
"""
try:
row = self.driver.find_elements(By.CSS_SELECTOR, "table tbody tr")[row_index]
except IndexError:
return f"ERROR: Could not re-locate {row_label} search result row"
try:
radio = row.find_element(By.CSS_SELECTOR, "input[type='radio']")
except Exception:
try:
radio = row.find_element(By.XPATH, ".//*[@role='radio' or contains(@class,'radio')]")
except Exception as e:
return f"ERROR: Could not find radio button in {row_label} search result row: {e}"
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", radio)
time.sleep(0.3)
try:
radio.click()
except Exception:
self.driver.execute_script("arguments[0].click();", radio)
print(f"[UnitedUcard Claim] Selected {row_label} matching patient record ({total_rows} found)")
# Confirmed against the live site (2026-09-13):
# <input type="button" class="btn btn-sm btn-primary pull-right margin-left-10"
# onclick="refineSearch(this);" value="select">
try:
select_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "input[type='button'][value='select']"))
)
select_btn.click()
print("[UnitedUcard Claim] Clicked SELECT (refineSearch)")
time.sleep(2)
except TimeoutException:
try:
select_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.XPATH,
"//input[@onclick='refineSearch(this);'] | "
"//*[self::button or self::input][translate(@value,'SELECT','select')='select' or "
"normalize-space(text())='SELECT' or normalize-space(text())='Select']"))
)
select_btn.click()
print("[UnitedUcard Claim] Clicked SELECT (fallback selector)")
time.sleep(2)
except TimeoutException:
return f"ERROR: SELECT button not found after choosing {row_label} patient record"
# After SELECT, a "Member Details" table appears below with the
# patient's name as a link. Clicking it does NOT navigate away —
# it replaces the table with a confirmation card (avatar initials +
# name, Group Number, Subscriber ID, DOB, Address) on the same
# Patient Information page, and enables the bottom "Next" button.
# If the selected record is inactive, this link never appears.
try:
member_link = WebDriverWait(self.driver, member_link_timeout).until(
EC.element_to_be_clickable((By.XPATH,
"//*[contains(text(),'Member Details')]/following::table[1]//a[1]"))
)
member_name = member_link.text.strip()
member_link.click()
print(f"[UnitedUcard Claim] Clicked member link: {member_name}")
time.sleep(2)
return True
except TimeoutException:
print(f"[UnitedUcard Claim] Member Details link did not appear after selecting {row_label} row")
return "NO_MEMBER_LINK"
def _click_radio_by_id(self, element_id, label=""):
try:
radio = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.ID, element_id))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", radio)
try:
radio.click()
except Exception:
self.driver.execute_script("arguments[0].click();", radio)
print(f"[UnitedUcard Claim] Selected '{label or element_id}'")
return True
except TimeoutException:
return f"ERROR: Radio button #{element_id} ({label}) not found on Claim Information step"
def step4_fill_claim_information(self):
"""
Fill the top of the Claim Information step's claim-level questions,
then (TODO) the Procedure section (Procedure Code / Date of Service /
Procedure Fee / Diagnosis Code(s)) from self.serviceLines.
Confirmed against the live site (2026-09-13):
"Does the patient have additional insurance...?" -> No
<input type="radio" name="PatientAdditionalCoverage" id="PatientAdditionalCoverage1" value="No">
"Does this claim include EPSDT / Title XIX?" -> No
<input type="radio" name="ClaimEpsdt" id="epsdtNo" value="No">
"Who should be reimbursed for this claim?" -> Provider
<input type="radio" name="PHSubscriberDirectPayment" id="PHSubscriberDirectPayment0" value="Provider">
"""
for element_id, label in [
("PatientAdditionalCoverage1", "Additional Insurance: No"),
("epsdtNo", "EPSDT / Title XIX: No"),
("PHSubscriberDirectPayment0", "Reimbursement: Provider"),
]:
result = self._click_radio_by_id(element_id, label)
if isinstance(result, str) and result.startswith("ERROR"):
return result
if not self.serviceLines:
return "ERROR: No procedure/service lines to submit"
for idx, line in enumerate(self.serviceLines):
if idx > 0:
add_result = self._click_add_procedure()
if isinstance(add_result, str) and add_result.startswith("ERROR"):
return add_result
result = self._fill_and_save_procedure(line, idx)
if isinstance(result, str) and result.startswith("ERROR"):
return result
# After all procedures are saved, three more claim-level questions
# appear. Confirmed against the live site (2026-09-13):
# "Does this claim include orthodontics?" -> No
# <input type="radio" name="ClaimOrtho" id="ClaimOrtho1" value="No">
# "Does this claim include prosthesis (crown, bridge, or denture)?" -> No
# <input type="radio" name="ClaimProsthesis" id="ClaimProsthesis1" value="No">
# "Does this claim relate to an accident/injury/illness?" -> No
# <input type="radio" name="OrthAccidentInjury" id="OrthAccidentInjury1" value="No">
for element_id, label in [
("ClaimOrtho1", "Orthodontics: No"),
("ClaimProsthesis1", "Prosthesis: No"),
("OrthAccidentInjury1", "Accident/Injury/Illness: No"),
]:
result = self._click_radio_by_id(element_id, label)
if isinstance(result, str) and result.startswith("ERROR"):
return result
return self._click_next("Claim Information step")
def _click_add_procedure(self):
"""
For a 2nd+ procedure line, click "Add a Procedure" to open a fresh
Procedure Code / Date of Service / Procedure Fee row before filling it.
Confirmed against the live site (2026-09-13):
<div id="dentalProcedures" onclick="addProcCode()">Add a Procedure</div>
"""
try:
add_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//*[@onclick='addProcCode()']"))
)
add_btn.click()
print("[UnitedUcard Claim] Clicked 'Add a Procedure'")
time.sleep(1)
return True
except TimeoutException:
return "ERROR: 'Add a Procedure' button not found"
def _fill_and_save_procedure(self, line: dict, idx: int):
"""
Fill one procedure row (Procedure Code, Date of Service, Procedure
Fee) and click Save Procedure.
Confirmed against the live site (2026-09-13):
<input id="dentalProcCode" type="text" min="5" max="5"> (5-char code, e.g. D0120)
<input id="serviceDate" type="date">
<input id="ROSFeeX" type="number" class="currency-input">
<div onclick="saveProcCode()">Save Procedure</div>
"""
procedure_code = str(line.get("procedureCode", "")).strip()
fee = line.get("totalBilled") or line.get("billedAmount") or line.get("fee")
procedure_date = line.get("procedureDate") or self.serviceDate
if not procedure_code:
return f"ERROR: Procedure line {idx + 1} has no procedure code"
try:
code_field = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.ID, "dentalProcCode"))
)
code_field.clear()
code_field.send_keys(procedure_code)
print(f"[UnitedUcard Claim] Entered Procedure Code: {procedure_code}")
except TimeoutException:
return f"ERROR: Procedure Code field not found for line {idx + 1}"
try:
date_field = self.driver.find_element(By.ID, "serviceDate")
date_field.click()
date_field.send_keys(self._format_date_digits(procedure_date))
print(f"[UnitedUcard Claim] Entered procedure Date of Service: {procedure_date}")
except Exception as e:
return f"ERROR: Date of Service field not found for line {idx + 1}: {e}"
try:
fee_field = self.driver.find_element(By.ID, "ROSFeeX")
fee_field.clear()
fee_field.send_keys(str(fee) if fee is not None else "")
print(f"[UnitedUcard Claim] Entered Procedure Fee: {fee}")
except Exception as e:
return f"ERROR: Procedure Fee field not found for line {idx + 1}: {e}"
try:
save_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//*[@onclick='saveProcCode()']"))
)
save_btn.click()
print(f"[UnitedUcard Claim] Clicked Save Procedure for line {idx + 1}")
time.sleep(1.5)
except TimeoutException:
return f"ERROR: Save Procedure button not found for line {idx + 1}"
return True
def step5_upload_documents(self):
"""
Upload claim attachments via the dropzone's hidden file input, then
click "Review and Sign" to advance.
Confirmed against the live site (2026-09-13):
<input type="file" id="file-input" class="file-input" multiple
accept=".pdf,.png,.jpg,.jpeg,.txt,.tif,.tiff,.gif,.bmp">
<div>...Review and Sign</div> (48px pill button, below Helpful Resources)
"""
attached = 0
for cf in self.claimFiles:
relative_path = cf.get("filePath") or ""
if not relative_path:
print(f"[UnitedUcard Claim step5] Skipping file with no filePath: {cf}")
continue
abs_path = os.path.normpath(os.path.join(_BACKEND_CWD, relative_path.lstrip("/")))
if not os.path.isfile(abs_path):
print(f"[UnitedUcard Claim step5] File not found on disk: {abs_path}")
continue
try:
# Don't click the visible "Browse" button — it's wired to the
# hidden #file-input and triggers the OS-native file picker,
# which WebDriver cannot control. Send the path directly to
# the input instead (chromedriver allows send_keys on file
# inputs even while display:none/hidden by the dropzone CSS).
file_input = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, "file-input"))
)
file_input.send_keys(abs_path)
time.sleep(1.5)
print(f"[UnitedUcard Claim step5] Attached: {os.path.basename(abs_path)}")
attached += 1
except Exception as e:
print(f"[UnitedUcard Claim step5] Could not attach {abs_path}: {type(e).__name__}: {e}")
print(f"[UnitedUcard Claim step5] Attached {attached}/{len(self.claimFiles)} file(s)")
try:
review_label = WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.XPATH,
"//div[normalize-space(text())='Review and Sign']"))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", review_label)
time.sleep(0.3)
target = review_label
try:
target = review_label.find_element(By.XPATH,
"ancestor::*[self::button or self::a or @role='button' or @data-state][1]")
except Exception:
pass
try:
target.click()
except Exception:
self.driver.execute_script("arguments[0].click();", target)
print("[UnitedUcard Claim] Clicked 'Review and Sign'")
time.sleep(2)
return True
except TimeoutException:
return "ERROR: 'Review and Sign' button not found on Upload Documents step"
def step6_review_and_sign(self):
"""
Check the two "signature on file" attestation checkboxes at the
bottom of the Review and Sign step.
Confirmed against the live site (2026-09-13):
<input type="checkbox" name="MemberPatientSignatureOnFile" id="MemberPatientSignatureOnFile_0" value="yes">
<input type="checkbox" name="PHSubscriberSignatureOnFile" id="PHSubscriberSignatureOnFile_0" value="yes">
"""
for element_id, label in [
("MemberPatientSignatureOnFile_0", "Patient signature on file"),
("PHSubscriberSignatureOnFile_0", "Subscriber signature on file"),
]:
result = self._click_radio_by_id(element_id, label)
if isinstance(result, str) and result.startswith("ERROR"):
return result
# Provider signature — the default provider configured in Settings
# (sortOrder asc, first entry), passed through as self.providerName.
# Confirmed against the live site (2026-09-13):
# <input type="text" id="userSignatureText">
try:
signature_field = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.ID, "userSignatureText"))
)
signature_field.clear()
signature_field.send_keys(self.providerName)
print(f"[UnitedUcard Claim] Entered provider signature: {self.providerName}")
except TimeoutException:
return "ERROR: Provider signature field (#userSignatureText) not found on Review and Sign step"
# Confirmed against the live site (2026-09-13):
# <div class="submit-claim" onclick="submitClaimForm();">Submit</div>
try:
submit_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.CSS_SELECTOR, ".submit-claim"))
)
submit_btn.click()
print("[UnitedUcard Claim] Clicked Submit (submitClaimForm)")
time.sleep(2)
except TimeoutException:
try:
submit_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.XPATH, "//*[@onclick='submitClaimForm();']"))
)
submit_btn.click()
print("[UnitedUcard Claim] Clicked Submit (fallback selector)")
time.sleep(2)
except TimeoutException:
return "ERROR: Submit button (.submit-claim) not found on Review and Sign step"
return True
def step7_save_confirmation_pdf(self):
"""
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}"