""" 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: " - 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: " 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
("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): Claims 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 — 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): """ 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): """ After the Subscriber ID + DOB search, the site does one of two things: 1. Multiple coverage records exist ("Duplicate records have been found") — a results table appears with a radio button in each row (Name / Group / Coverage End Date / Address columns) and CANCEL / SELECT pill buttons below it. Select the LAST row (the default/most-recent coverage record) and click SELECT. 2. A single unambiguous match — no insurance history table and no radio at all; the page goes straight to a "Member Details" table with the patient's name as a link (confirmed live for patient Nassim, 2026-09-15). Either path ends by clicking the "Member Details" name link, which replaces the table with a confirmation card and enables the bottom "Next" button. """ try: result_rows = WebDriverWait(self.driver, 15).until( lambda d: [r for r in d.find_elements(By.CSS_SELECTOR, "table tbody tr") if r.find_elements(By.CSS_SELECTOR, "input[type='radio']")] or None ) except TimeoutException: result_rows = None if result_rows: result = self._select_last_row_and_click_select(len(result_rows)) if result is not True: return result # propagate a hard ERROR (radio/select button not found) else: print("[UnitedUcard Claim] No insurance history/radio shown — single unambiguous match") result = self._click_member_link_or_advance() if result is True: return self._click_next("Patient Information step") return result def _select_last_row_and_click_select(self, total_rows): """ Clicks the radio button in the LAST row of the insurance history table, then clicks SELECT. Returns True, or an "ERROR: ..." string for a hard failure. """ try: rows = [r for r in self.driver.find_elements(By.CSS_SELECTOR, "table tbody tr") if r.find_elements(By.CSS_SELECTOR, "input[type='radio']")] row = rows[-1] except IndexError: return "ERROR: Could not re-locate last 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 last 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 last matching patient record ({total_rows} found)") # Confirmed against the live site (2026-09-13): # 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 "ERROR: SELECT button not found after choosing last patient record" return True def _click_member_link_or_advance(self, timeout=15): """ After SELECT (or immediately after Search, for a single unambiguous match with no history table), a "Member Details" table appears 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. Returns True (link found & clicked, or already advanced to Claim Information), or an "ERROR: ..." string. """ try: member_link = WebDriverWait(self.driver, 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: pass try: WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, "//*[contains(text(),'Claim Information')]")) ) print("[UnitedUcard Claim] Already advanced to Claim Information") return True except TimeoutException: return "ERROR: Member Details link not found after Search" 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 "Does this claim include EPSDT / Title XIX?" -> No "Who should be reimbursed for this claim?" -> 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 # # "Does this claim include prosthesis (crown, bridge, or denture)?" -> No # # "Does this claim relate to an accident/injury/illness?" -> 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):
Add a Procedure
""" 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): (5-char code, e.g. D0120)
Save Procedure
""" 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):
...Review and Sign
(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): """ 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): # 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): #
Submit
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):

Claim Submitted

Claim ID: 260483578000 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}"