""" United/DentalHub Claim Submission Worker. Portal: app.dentalhub.com (United SCO) Flow: 1. Navigate to eligibility page, fill member ID + DOB + payer, continue through Select Insurance popup and Provider & Location page → land on Selected Patient page. 2. Click Submit Claim button on Selected Patient page. 3. Submit claim page is pre-filled — just click Continue. 4. Select Insurance popup — click Ok. 5. Practitioner & Location page — click Continue only (no dropdowns). 6. Date Entry page — fill CDT codes, tooth, billed amount, attach files, submit. 7. Capture claim number and PDF from Status & History page. """ from selenium import webdriver from selenium.common.exceptions import WebDriverException, TimeoutException from selenium.webdriver.chrome.service import Service from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from webdriver_manager.chrome import ChromeDriverManager import time import os import base64 import json from unitedsco_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 AutomationUnitedDHClaimSubmit: def __init__(self, data): self.headless = False self.driver = None claim = data.get("claim", {}) if isinstance(data, dict) else {} self.memberId = claim.get("memberId", "") self.dateOfBirth = claim.get("dateOfBirth", "") self.firstName = claim.get("firstName", "") self.lastName = claim.get("lastName", "") self.serviceDate = claim.get("serviceDate", "") self.serviceLines = claim.get("serviceLines", []) self.claimFiles = claim.get("claimFiles", []) self.patientName = claim.get("patientName", "") self.remarks = claim.get("remarks", "") self.uniteddh_username = claim.get("uniteddhUsername", "") self.uniteddh_password = claim.get("uniteddhPassword", "") self.download_dir = get_browser_manager().download_dir os.makedirs(self.download_dir, exist_ok=True) def config_driver(self): self.driver = get_browser_manager().get_driver(self.headless) def _force_logout(self): try: print("[UnitedDH Claim login] Forcing logout due to credential change...") browser_manager = get_browser_manager() try: self.driver.get("https://app.dentalhub.com/app/dashboard") time.sleep(2) for selector in [ "//button[contains(text(),'Log out') or contains(text(),'Logout') or contains(text(),'Sign out')]", "//a[contains(text(),'Log out') or contains(text(),'Logout')]", "//button[@aria-label='Log out' or @aria-label='Logout']", ]: try: btn = WebDriverWait(self.driver, 3).until(EC.element_to_be_clickable((By.XPATH, selector))) btn.click() print("[UnitedDH Claim login] Clicked logout button") time.sleep(2) break except TimeoutException: continue except Exception as e: print(f"[UnitedDH Claim login] Could not click logout button: {e}") try: self.driver.delete_all_cookies() print("[UnitedDH Claim login] Cleared all cookies") except Exception as e: print(f"[UnitedDH Claim login] Error clearing cookies: {e}") browser_manager.clear_credentials_hash() return True except Exception as e: print(f"[UnitedDH Claim login] Error during forced logout: {e}") return False def login(self, url): wait = WebDriverWait(self.driver, 30) browser_manager = get_browser_manager() try: if self.uniteddh_username and browser_manager.credentials_changed(self.uniteddh_username): self._force_logout() self.driver.get(url) time.sleep(2) try: current_url = self.driver.current_url print(f"[UnitedDH Claim login] Current URL: {current_url}") if "app.dentalhub.com" in current_url and "login" not in current_url.lower(): try: WebDriverWait(self.driver, 3).until( EC.presence_of_element_located((By.XPATH, '//input[contains(@placeholder,"Search")] | //*[contains(@class,"dashboard")] | //nav')) ) print("[UnitedDH Claim login] Already logged in") return "ALREADY_LOGGED_IN" except TimeoutException: pass except Exception as e: print(f"[UnitedDH Claim login] Error checking current state: {e}") self.driver.get(url) time.sleep(3) current_url = self.driver.current_url print(f"[UnitedDH Claim login] After navigation URL: {current_url}") if "app.dentalhub.com" in current_url and "login" not in current_url.lower(): print("[UnitedDH Claim login] Already on 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(@aria-label,'Verification')]")) ) print("[UnitedDH Claim login] OTP input found") return "OTP_REQUIRED" except TimeoutException: pass if "app.dentalhub.com" in current_url: try: login_btn = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'LOGIN') or contains(text(),'Log In') or contains(text(),'Login')]")) ) login_btn.click() print("[UnitedDH Claim login] Clicked LOGIN button") time.sleep(5) except TimeoutException: print("[UnitedDH Claim login] No LOGIN button found, proceeding...") current_url = self.driver.current_url print(f"[UnitedDH Claim login] After LOGIN click URL: {current_url}") if "b2clogin.com" in current_url or "login" in current_url.lower(): print("[UnitedDH Claim login] On B2C login page - filling credentials") try: send_code_btn = self.driver.find_element(By.XPATH, "//button[@id='sendCode'] | //input[@id='sendCode'] | " "//button[contains(text(),'Text Me') or contains(text(),'Send Code')]" ) if send_code_btn.is_displayed(): print("[UnitedDH Claim login] Already on phone verification page - clicking 'Text Me'") self.driver.execute_script("arguments[0].click();", send_code_btn) time.sleep(3) return "OTP_REQUIRED" except Exception: pass try: email_field = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//input[@id='signInName' or @name='signInName' or @name='Email address' or @type='email']")) ) email_field.clear() email_field.send_keys(self.uniteddh_username) print(f"[UnitedDH Claim login] Entered username: {self.uniteddh_username}") password_field = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//input[@id='password' or @type='password']")) ) password_field.clear() password_field.send_keys(self.uniteddh_password) print("[UnitedDH Claim login] Entered password") signin_button = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[@id='next'] | //button[@type='submit' and contains(text(),'Sign')]")) ) signin_button.click() print("[UnitedDH Claim login] Clicked Sign in button") if self.uniteddh_username: browser_manager.save_credentials_hash(self.uniteddh_username) time.sleep(5) try: continue_btn = self.driver.find_element(By.XPATH, "//button[contains(text(),'Continue')]") phone_elements = self.driver.find_elements(By.XPATH, "//*[contains(text(),'Phone')]") if continue_btn and phone_elements: print("[UnitedDH Claim login] MFA method selection page detected") try: phone_radio = self.driver.find_element(By.XPATH, "//input[@type='radio' and (contains(@value,'phone') or contains(@value,'Phone'))] | " "//label[contains(text(),'Phone')]/preceding-sibling::input[@type='radio'] | " "//input[@type='radio']" ) if phone_radio and not phone_radio.is_selected(): phone_radio.click() print("[UnitedDH Claim login] Selected 'Phone' radio button") except Exception as radio_err: print(f"[UnitedDH Claim login] Could not click Phone radio: {radio_err}") time.sleep(1) continue_btn.click() print("[UnitedDH Claim login] Clicked 'Continue' on MFA selection page") time.sleep(3) except Exception: pass try: send_code_btn = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, "//button[@id='sendCode'] | //input[@id='sendCode'] | " "//button[contains(text(),'Text Me') or contains(text(),'Send Code')]")) ) print("[UnitedDH Claim login] Found 'Text Me' / Send Code button") self.driver.execute_script("arguments[0].click();", send_code_btn) time.sleep(3) return "OTP_REQUIRED" except TimeoutException: pass try: WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, "//input[@type='tel' or contains(@placeholder,'code') or contains(@aria-label,'Verification')]")) ) print("[UnitedDH Claim login] OTP input appeared after sign-in") return "OTP_REQUIRED" except TimeoutException: pass current_url = self.driver.current_url if "app.dentalhub.com" in current_url and "login" not in current_url.lower(): print("[UnitedDH Claim login] Login succeeded without OTP") return "SUCCESS" print(f"[UnitedDH Claim login] Unexpected state - URL: {current_url}") return "SUCCESS" except Exception as e: return f"ERROR: Login failed - {e}" if "app.dentalhub.com" in current_url: return "ALREADY_LOGGED_IN" return "SUCCESS" except Exception as e: return f"ERROR: Login exception - {e}" # ── Helpers ──────────────────────────────────────────────────────────────── def _check_for_error_dialog(self): error_patterns = [ ("Patient Not Found", "Patient Not Found - please check the Subscriber ID, DOB, and Payer selection"), ("Insufficient Information", "Insufficient Information - need Subscriber ID + DOB, or First Name + Last Name + DOB"), ("No Eligibility", "No eligibility information found for this patient"), ("Error", None), ] for pattern, default_msg in error_patterns: try: dialog_elem = self.driver.find_element(By.XPATH, f"//modal-container//*[contains(text(),'{pattern}')] | " f"//div[contains(@class,'modal')]//*[contains(text(),'{pattern}')]" ) if dialog_elem.is_displayed(): try: modal = self.driver.find_element(By.XPATH, "//modal-container | //div[contains(@class,'modal-dialog')]") dialog_text = modal.text.strip()[:200] except Exception: dialog_text = dialog_elem.text.strip()[:200] print(f"[UnitedDH Claim] Error dialog detected: {dialog_text}") try: dismiss_btn = self.driver.find_element(By.XPATH, "//modal-container//button[contains(text(),'Ok') or contains(text(),'OK') or contains(text(),'Close')] | " "//div[contains(@class,'modal')]//button[contains(text(),'Ok') or contains(text(),'OK') or contains(text(),'Close')]" ) dismiss_btn.click() print("[UnitedDH Claim] Dismissed error dialog") time.sleep(1) except Exception: try: close_btn = self.driver.find_element(By.XPATH, "//modal-container//button[@class='close']") close_btn.click() except Exception: pass error_msg = default_msg if default_msg else f"ERROR: {dialog_text}" return f"ERROR: {error_msg}" except Exception: continue return None def _format_dob(self, dob_str): if dob_str and "-" in dob_str: dob_parts = dob_str.split("-") if len(dob_parts) == 3: return f"{dob_parts[1]}/{dob_parts[2]}/{dob_parts[0]}" return dob_str def _get_existing_downloads(self): import glob return set(glob.glob(os.path.join(self.download_dir, "*.pdf"))) def _wait_for_new_download(self, existing_files, timeout=15): import glob for _ in range(timeout * 2): time.sleep(0.5) current = set(glob.glob(os.path.join(self.download_dir, "*.pdf"))) new_files = current - existing_files if new_files: crdownloads = glob.glob(os.path.join(self.download_dir, "*.crdownload")) if not crdownloads: return list(new_files)[0] return None def _hide_browser(self): try: try: self.driver.get("about:blank") time.sleep(0.5) except Exception: pass try: self.driver.minimize_window() print("[UnitedDH Claim] Browser window minimized") return except Exception: pass try: self.driver.set_window_position(-10000, -10000) print("[UnitedDH Claim] Browser window moved off-screen") return except Exception: pass try: import subprocess subprocess.run(["xdotool", "getactivewindow", "windowminimize"], timeout=3, capture_output=True) print("[UnitedDH Claim] Browser minimized via xdotool") except Exception: pass except Exception as e: print(f"[UnitedDH Claim] Could not hide browser: {e}") def _capture_pdf(self, identifier): try: pdf_options = { "landscape": False, "displayHeaderFooter": False, "printBackground": True, "preferCSSPageSize": True, "paperWidth": 8.5, "paperHeight": 11, "marginTop": 0.4, "marginBottom": 0.4, "marginLeft": 0.4, "marginRight": 0.4, "scale": 0.9, } file_id = identifier if identifier else f"{self.firstName}_{self.lastName}" result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options) pdf_data = base64.b64decode(result.get("data", "")) pdf_path = os.path.join(self.download_dir, f"uniteddh_claim_{file_id}_{int(time.time())}.pdf") with open(pdf_path, "wb") as f: f.write(pdf_data) return pdf_path except Exception as e: print(f"[UnitedDH Claim _capture_pdf] Error: {e}") return None # ── Claim steps ──────────────────────────────────────────────────────────── def step1_search_patient(self): """ Navigate to the eligibility page, fill member ID + DOB + payer, continue through the Select Insurance popup and Provider & Location dropdowns to land on the Selected Patient results page. Mirrors the eligibility worker's step1() exactly. """ from selenium.webdriver.common.action_chains import ActionChains try: print(f"[UnitedDH Claim] step1: memberId={self.memberId}, dob={self.dateOfBirth}") self.driver.get("https://app.dentalhub.com/app/patient/eligibility") time.sleep(3) print(f"[UnitedDH Claim] step1 URL: {self.driver.current_url}") # Wait for Patient Information form try: WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.ID, "firstName_Back")) ) print("[UnitedDH Claim] step1: Patient Information form loaded") except TimeoutException: print("[UnitedDH Claim] step1: Patient Information form not found") return "ERROR: step1 - Patient Information form not found" # Fill Subscriber ID if self.memberId: subscriber_id_selectors = [ "//input[@id='subscriberId_Front']", "//input[@id='subscriberId_Back' or @id='subscriberID_Back']", "//input[@id='memberId_Back' or @id='memberid_Back']", "//input[@id='medicaidId_Back']", "//label[contains(text(),'Subscriber ID')]/..//input[not(@id='firstName_Back') and not(@id='lastName_Back') and not(@id='dateOfBirth_Back')]", "//input[contains(@placeholder,'Subscriber') or contains(@placeholder,'subscriber')]", "//input[contains(@placeholder,'Medicaid') or contains(@placeholder,'medicaid')]", "//input[contains(@placeholder,'Member') or contains(@placeholder,'member')]", ] subscriber_filled = False for sel in subscriber_id_selectors: try: sid_input = self.driver.find_element(By.XPATH, sel) if sid_input.is_displayed(): sid_input.clear() sid_input.send_keys(self.memberId) field_id = sid_input.get_attribute("id") or "unknown" print(f"[UnitedDH Claim] step1: Subscriber ID entered: {self.memberId} (field='{field_id}')") subscriber_filled = True break except Exception: continue if not subscriber_filled: try: all_inputs = self.driver.find_elements(By.XPATH, "//form//input[@type='text' or not(@type)]") known_ids = {'firstName_Back', 'lastName_Back', 'dateOfBirth_Back', 'procedureDate_Back', 'insurerId'} for inp in all_inputs: inp_id = inp.get_attribute("id") or "" if inp_id not in known_ids and inp.is_displayed(): inp.clear() inp.send_keys(self.memberId) print(f"[UnitedDH Claim] step1: Subscriber ID in fallback field id='{inp_id}'") subscriber_filled = True break except Exception as e2: print(f"[UnitedDH Claim] step1: Fallback subscriber field error: {e2}") if not subscriber_filled: print(f"[UnitedDH Claim] step1: WARNING - Could not find Subscriber ID field") # Fill Date of Birth try: dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back") dob_input.clear() dob_formatted = self._format_dob(self.dateOfBirth) dob_input.send_keys(dob_formatted) print(f"[UnitedDH Claim] step1: DOB entered: {dob_formatted}") except Exception as e: print(f"[UnitedDH Claim] step1: Error entering DOB: {e}") return "ERROR: step1 - Could not enter Date of Birth" time.sleep(1) # Dismiss any blocking overlays (Chrome password manager etc.) try: self.driver.execute_script(""" var dialogs = document.querySelectorAll('[role="dialog"], .cdk-overlay-container'); dialogs.forEach(function(d) { d.style.display = 'none'; }); """) except Exception: pass # Select Payer: UnitedHealthcare Massachusetts print("[UnitedDH Claim] step1: Selecting Payer...") payer_selected = False try: payer_selectors = [ "//label[contains(text(),'Payer')]/following-sibling::ng-select", "//label[contains(text(),'Payer')]/..//ng-select", "//ng-select[contains(@placeholder,'Payer') or contains(@placeholder,'payer')]", "//ng-select[.//input[contains(@placeholder,'Search by Payers')]]", ] payer_ng_select = None for sel in payer_selectors: try: elem = self.driver.find_element(By.XPATH, sel) if elem.is_displayed(): payer_ng_select = elem break except Exception: continue if payer_ng_select: self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", payer_ng_select) time.sleep(0.5) payer_ng_select.click() time.sleep(1) search_input = payer_ng_select.find_element(By.XPATH, ".//input[contains(@type,'text') or contains(@role,'combobox')]") search_input.clear() search_input.send_keys("UnitedHealthcare Massachusetts") print("[UnitedDH Claim] step1: Typed payer search text") time.sleep(2) search_input.send_keys(Keys.ENTER) print("[UnitedDH Claim] step1: Pressed Enter to select Payer") time.sleep(0.5) payer_selected = True else: print("[UnitedDH Claim] step1: Could not find Payer ng-select element") except Exception as e: print(f"[UnitedDH Claim] step1: Payer selection error: {e}") if not payer_selected: print("[UnitedDH Claim] step1: WARNING - Could not select Payer") time.sleep(1) # Click Continue (Patient Info) try: continue_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Continue')]")) ) continue_btn.click() print("[UnitedDH Claim] step1: Clicked Continue (Patient Info)") time.sleep(3) error_result = self._check_for_error_dialog() if error_result: return error_result except Exception as e: return f"ERROR: step1 - Could not click Continue: {e}" # Click Ok on Select Insurance popup print("[UnitedDH Claim] step1: Checking for Select Insurance popup...") try: ok_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary') and " "(normalize-space(.)='Ok' or normalize-space(.)='OK' or normalize-space(.)='Okay')] | " "//modal-container//button[normalize-space(.)='Ok' or normalize-space(.)='OK' or normalize-space(.)='Okay'] | " "//div[contains(@class,'modal')]//button[normalize-space(.)='Ok' or normalize-space(.)='OK' or normalize-space(.)='Okay']" )) ) try: self.driver.execute_script("arguments[0].click();", ok_btn) print("[UnitedDH Claim] step1: Clicked OK on Select Insurance popup (JS)") except Exception: ok_btn.click() print("[UnitedDH Claim] step1: Clicked OK on Select Insurance popup (direct)") try: WebDriverWait(self.driver, 10).until(EC.staleness_of(ok_btn)) print("[UnitedDH Claim] step1: Select Insurance modal closed") except TimeoutException: print("[UnitedDH Claim] step1: Modal staleness timeout — continuing anyway") WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, "//label[@for='treatmentLocation'] | //label[@for='paymentGroupId']")) ) except TimeoutException: print("[UnitedDH Claim] step1: Select Insurance popup not found — proceeding") # Provider & Location page — select Treatment Location and Billing Entity, then Continue print("[UnitedDH Claim] step1: Waiting for Provider & Location page...") try: WebDriverWait(self.driver, 20).until( EC.visibility_of_element_located((By.XPATH, "//label[@for='paymentGroupId']")) ) print("[UnitedDH Claim] step1: Selecting Treatment Location...") location_selected = False try: location_ng = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//label[@for='treatmentLocation']/following-sibling::ng-select | " "//label[@for='treatmentLocation']/..//ng-select" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_ng) arrow = location_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper") arrow.click() first_option = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option")) ) option_text = first_option.text.strip() first_option.click() print(f"[UnitedDH Claim] step1: Selected Treatment Location: {option_text}") location_selected = True except Exception as e: print(f"[UnitedDH Claim] step1: Treatment Location selection failed: {e}") if not location_selected: print("[UnitedDH Claim] step1: WARNING - Could not select Treatment Location") print("[UnitedDH Claim] step1: Selecting Billing Entity...") billing_selected = False try: billing_ng = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//label[@for='paymentGroupId']/following-sibling::ng-select | " "//label[@for='paymentGroupId']/..//ng-select" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", billing_ng) arrow = billing_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper") arrow.click() first_option = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option")) ) option_text = first_option.text.strip() first_option.click() print(f"[UnitedDH Claim] step1: Selected Billing Entity: {option_text}") billing_selected = True except Exception as e: print(f"[UnitedDH Claim] step1: Billing Entity selection failed: {e}") if not billing_selected: print("[UnitedDH Claim] step1: WARNING - Could not select Billing Entity") continue_btn2 = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Continue')]")) ) continue_btn2.click() print("[UnitedDH Claim] step1: Clicked Continue (Provider & Location) → Selected Patient page") time.sleep(5) except TimeoutException: try: results_elem = self.driver.find_element(By.XPATH, "//*[contains(text(),'Selected Patient') or contains(@id,'patient-name') or contains(@id,'eligibility')]" ) if results_elem.is_displayed(): print("[UnitedDH Claim] step1: Already on Selected Patient page") return "OK" except Exception: pass print("[UnitedDH Claim] step1: Continue not found on Provider & Location page — proceeding") except Exception as e: print(f"[UnitedDH Claim] step1: Error clicking Continue on Provider & Location page: {e}") error_result = self._check_for_error_dialog() if error_result: return error_result error_result = self._check_for_error_dialog() if error_result: return error_result print("[UnitedDH Claim] step1: Patient search complete — on Selected Patient page") return "OK" except Exception as e: return f"ERROR: step1_search_patient - {e}" def step2_click_submit_claim(self): """ On the Selected Patient results page, click the Submit Claim button (id="btnSubmitClaim"). """ try: print("[UnitedDH Claim] step2: Looking for Submit Claim button (id='btnSubmitClaim')...") time.sleep(2) submit_claim_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.ID, "btnSubmitClaim")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_claim_btn) time.sleep(0.5) submit_claim_btn.click() print("[UnitedDH Claim] step2: Clicked Submit Claim button") time.sleep(4) print(f"[UnitedDH Claim] step2 URL: {self.driver.current_url}") return "OK" except TimeoutException: # Fallback: find by text try: btn = self.driver.find_element(By.XPATH, "//button[contains(normalize-space(.),'Submit Claim') and not(contains(@class,'btn-primary'))]" ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn) btn.click() print("[UnitedDH Claim] step2: Clicked Submit Claim button (text fallback)") time.sleep(4) return "OK" except Exception as e2: return f"ERROR: step2 - Could not click Submit Claim button: {e2}" except Exception as e: return f"ERROR: step2_click_submit_claim - {e}" def step3_continue_prefilled(self): """ Submit Claim page: member ID and DOB are pre-filled. Select Payer by typing "UnitedHealthcare Massachusetts" + Enter, then click Continue. """ try: print("[UnitedDH Claim] step3: Submit Claim page — selecting Payer then clicking Continue...") # Wait for the form to load WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//label[contains(text(),'Payer')] | //ng-select" )) ) # Select Payer: type + Enter payer_selected = False try: payer_selectors = [ "//label[contains(text(),'Payer')]/following-sibling::ng-select", "//label[contains(text(),'Payer')]/..//ng-select", "//ng-select[contains(@placeholder,'Payer') or contains(@placeholder,'payer')]", "//ng-select[.//input[contains(@placeholder,'Search by Payers')]]", ] payer_ng_select = None for sel in payer_selectors: try: elem = self.driver.find_element(By.XPATH, sel) if elem.is_displayed(): payer_ng_select = elem break except Exception: continue if payer_ng_select: self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", payer_ng_select) time.sleep(0.5) payer_ng_select.click() time.sleep(1) search_input = payer_ng_select.find_element(By.XPATH, ".//input[contains(@type,'text') or contains(@role,'combobox')]") search_input.clear() search_input.send_keys("UnitedHealthcare Massachusetts") print("[UnitedDH Claim] step3: Typed payer search text") time.sleep(2) search_input.send_keys(Keys.ENTER) print("[UnitedDH Claim] step3: Pressed Enter to select Payer") time.sleep(0.5) payer_selected = True else: print("[UnitedDH Claim] step3: Could not find Payer ng-select element") except Exception as e: print(f"[UnitedDH Claim] step3: Payer selection error: {e}") if not payer_selected: print("[UnitedDH Claim] step3: WARNING - Could not select Payer") time.sleep(1) continue_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Continue')]")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", continue_btn) continue_btn.click() print("[UnitedDH Claim] step3: Clicked Continue") time.sleep(4) error_result = self._check_for_error_dialog() if error_result: return error_result print(f"[UnitedDH Claim] step3 URL: {self.driver.current_url}") return "OK" except Exception as e: return f"ERROR: step3_continue_prefilled - {e}" def step4_select_insurance_ok(self): """ Click Ok on the Select Insurance popup. """ try: print("[UnitedDH Claim] step4: Waiting for Select Insurance popup...") try: ok_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[@type='button' and contains(@class,'btn-primary') and " "(normalize-space(text())='Ok' or normalize-space(text())='OK')] | " "//modal-container//button[normalize-space(.)='Ok' or normalize-space(.)='OK'] | " "//div[contains(@class,'modal')]//button[normalize-space(.)='Ok' or normalize-space(.)='OK']" )) ) ActionChains(self.driver).move_to_element(ok_btn).pause(0.5).click().perform() print("[UnitedDH Claim] step4: Clicked Ok on Select Insurance popup") try: WebDriverWait(self.driver, 8).until(EC.staleness_of(ok_btn)) print("[UnitedDH Claim] step4: Select Insurance modal closed") except TimeoutException: print("[UnitedDH Claim] step4: Modal staleness timeout — continuing anyway") except TimeoutException: print("[UnitedDH Claim] step4: Select Insurance popup not found — proceeding") print(f"[UnitedDH Claim] step4 URL: {self.driver.current_url}") return "OK" except Exception as e: return f"ERROR: step4_select_insurance_ok - {e}" def step5_practitioner_continue(self): """ Practitioner & Location page — click Continue only, no dropdown selections. """ try: print("[UnitedDH Claim] step5: Waiting for Practitioner & Location page...") # Wait for the page to render (either treatmentLocation or paymentGroupId label) try: WebDriverWait(self.driver, 20).until( EC.visibility_of_element_located((By.XPATH, "//label[@for='treatmentLocation'] | //label[@for='paymentGroupId'] | " "//label[@for='paymentGroup']" )) ) print("[UnitedDH Claim] step5: Practitioner & Location page loaded") except TimeoutException: print("[UnitedDH Claim] step5: Practitioner & Location labels not found — trying Continue anyway") continue_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Continue')] | " "//button[contains(normalize-space(text()),'Continue')]" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", continue_btn) continue_btn.click() print("[UnitedDH Claim] step5: Clicked Continue — waiting for Code Entry page") time.sleep(3) try: WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.ID, "procedureCode")) ) print("[UnitedDH Claim] step5: Code Entry page loaded (procedureCode found)") except TimeoutException: print("[UnitedDH Claim] step5: procedureCode input not found — proceeding anyway") print(f"[UnitedDH Claim] step5 URL: {self.driver.current_url}") return "OK" except Exception as e: return f"ERROR: step5_practitioner_continue - {e}" def step6_fill_claim_form(self): """ For each service line with a procedure code: 1. Click btnAddItem to open/activate the row 2. Type CDT code into id="procedureCode" 3. Click id="btnAddItem" — billed amount input appears 4. Fill tooth number (if provided) 5. Click surface boxes (if provided) 6. Fill id="billedAmount" 7. Click the span "Add" button to confirm the row Then select "No" for Other coverage. """ try: active_lines = [ ln for ln in self.serviceLines if str(ln.get("procedureCode") or "").strip() ] print(f"[UnitedDH Claim] step6: {len(active_lines)} service line(s)") if not active_lines: print("[UnitedDH Claim] step6: No service lines — skipping") return "OK" for idx, line in enumerate(active_lines): code = str(line.get("procedureCode") or "").strip().upper() billed = str( line.get("totalBilled") or line.get("billedAmount") or line.get("fee") or "" ).strip() tooth = str(line.get("toothNumber") or line.get("tooth_number") or "").strip() surface = str(line.get("toothSurface") or line.get("tooth_surface") or "").strip().upper() print(f"[UnitedDH Claim] step6: line {idx}: code={code}, billed={billed}, tooth={tooth}, surface={surface}") # Click btnAddItem to open/activate the procedure row try: add_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.ID, "btnAddItem")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn) add_btn.click() print(f"[UnitedDH Claim] step6: clicked btnAddItem to open row {idx}") time.sleep(1) except Exception as e: print(f"[UnitedDH Claim] step6: could not click btnAddItem to open row {idx}: {e}") # Type CDT code try: proc_input = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.ID, "procedureCode")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_input) self.driver.execute_script("arguments[0].click();", proc_input) proc_input.send_keys(Keys.CONTROL + "a") proc_input.send_keys(Keys.DELETE) proc_input.send_keys(code) print(f"[UnitedDH Claim] step6: typed procedure code: {code}") time.sleep(0.5) except Exception as e: print(f"[UnitedDH Claim] step6: could not type procedure code for row {idx}: {e}") continue # Click btnAddItem to confirm code and reveal billed amount input try: add_btn = WebDriverWait(self.driver, 8).until( EC.element_to_be_clickable((By.ID, "btnAddItem")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn) add_btn.click() print(f"[UnitedDH Claim] step6: clicked btnAddItem to reveal billedAmount for row {idx}") time.sleep(1.5) except Exception as e: print(f"[UnitedDH Claim] step6: could not click btnAddItem for billed amount row {idx}: {e}") continue # Fill tooth number if tooth: try: tooth_input = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.ID, "tooth")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_input) tooth_input.click() tooth_input.send_keys(Keys.CONTROL + "a") tooth_input.send_keys(Keys.DELETE) tooth_input.send_keys(tooth) print(f"[UnitedDH Claim] step6: entered tooth number: {tooth}") time.sleep(0.3) except Exception as e: print(f"[UnitedDH Claim] step6: could not fill tooth number for row {idx}: {e}") # Click surface boxes if surface: try: surface_boxes = self.driver.find_elements(By.XPATH, "//div[contains(@class,'claim-add-item-group__box')]") if surface_boxes: for letter in surface: if not letter.strip(): continue try: box = self.driver.find_element(By.XPATH, f"//div[contains(@class,'claim-add-item-group__box') " f"and not(contains(@class,'--disabled')) " f"and @id='{letter}']" ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", box) box.click() print(f"[UnitedDH Claim] step6: clicked surface '{letter}'") time.sleep(0.2) except Exception: print(f"[UnitedDH Claim] step6: surface '{letter}' not found or disabled") else: print(f"[UnitedDH Claim] step6: no surface boxes on page — skipping") except Exception as e: print(f"[UnitedDH Claim] step6: surface click error for row {idx}: {e}") # Fill billed amount if billed: try: billed_input = WebDriverWait(self.driver, 8).until( EC.element_to_be_clickable((By.ID, "billedAmount")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", billed_input) billed_input.click() billed_input.send_keys(Keys.CONTROL + "a") billed_input.send_keys(Keys.DELETE) billed_input.send_keys(billed) print(f"[UnitedDH Claim] step6: entered billed amount: {billed}") time.sleep(0.5) except Exception as e: print(f"[UnitedDH Claim] step6: could not fill billed amount for row {idx}: {e}") # Click the span "Add" button to confirm the row try: span_add = WebDriverWait(self.driver, 8).until( EC.element_to_be_clickable((By.XPATH, "//span[contains(@class,'ng-star-inserted') and normalize-space(text())='Add'] | " "//button[normalize-space(text())='Add' and not(@id='btnAddItem')] | " "//span[normalize-space(text())='Add']" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", span_add) span_add.click() print(f"[UnitedDH Claim] step6: clicked span Add — row {idx} confirmed") time.sleep(1) except Exception as e: print(f"[UnitedDH Claim] step6: could not click span Add for row {idx}: {e}") # Other coverage: click "No" (second radio button) try: print("[UnitedDH Claim] step6: selecting 'No' for Other coverage") radio_buttons = WebDriverWait(self.driver, 8).until( lambda d: d.find_elements(By.XPATH, "//input[@type='radio']") ) if len(radio_buttons) >= 2: no_radio = radio_buttons[1] self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", no_radio) no_radio.click() print("[UnitedDH Claim] step6: Clicked 'No' (2nd radio) for Other coverage") else: print(f"[UnitedDH Claim] step6: Only {len(radio_buttons)} radio button(s) found — skipping") time.sleep(0.5) except Exception as e: print(f"[UnitedDH Claim] step6: Could not click 'No' for Other coverage (non-fatal): {e}") print("[UnitedDH Claim] step6: Done filling claim form") return "OK" except Exception as e: return f"ERROR: step6_fill_claim_form - {e}" def step7_attach_files(self): """ If there are claim files: 1. Click the fa-caret-up dropdown icon to reveal the Add Document button 2. Click id="upload-document" 3. Send the absolute file path to the file input """ try: if not self.claimFiles: print("[UnitedDH Claim] step7: No files to attach") return "OK" try: caret = WebDriverWait(self.driver, 8).until( EC.element_to_be_clickable((By.XPATH, "//em[contains(@class,'fa-caret-up')] | " "//i[contains(@class,'fa-caret-up')] | " "//*[contains(@class,'fa') and contains(@class,'fa-caret-up')]" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", caret) caret.click() print("[UnitedDH Claim] step7: Clicked caret-up to expand Attached Documents") time.sleep(1) except Exception as e: print(f"[UnitedDH Claim] step7: Could not click caret (section may already be open): {e}") attached = 0 for cf in self.claimFiles: relative_path = cf.get("filePath") or "" if not relative_path: print(f"[UnitedDH Claim] step7: 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"[UnitedDH Claim] step7: File not found on disk: {abs_path}") continue print(f"[UnitedDH Claim] step7: Attaching: {abs_path}") try: upload_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.ID, "upload-document")) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", upload_btn) upload_btn.click() time.sleep(1) file_input = WebDriverWait(self.driver, 8).until( EC.presence_of_element_located((By.XPATH, "//input[@type='file']")) ) self.driver.execute_script("arguments[0].style.display='block';", file_input) file_input.send_keys(abs_path) time.sleep(1.5) print(f"[UnitedDH Claim] step7: Attached: {os.path.basename(abs_path)}") attached += 1 except Exception as e: print(f"[UnitedDH Claim] step7: Could not attach {abs_path}: {e}") print(f"[UnitedDH Claim] step7: Attached {attached}/{len(self.claimFiles)} file(s)") return "OK" except Exception as e: return f"ERROR: step7_attach_files - {e}" def step8_submit_claim(self): """ Click Submit Claim on the Code Entry page, then click "View Status and History" on the post-submit popup. """ try: print(f"[UnitedDH Claim] step8: submitting claim — URL: {self.driver.current_url}") submit_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Submit Claim')] | " "//button[normalize-space(text())='Submit Claim'] | " "//button[contains(normalize-space(.),'Submit Claim')]" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_btn) time.sleep(0.5) submit_btn.click() print("[UnitedDH Claim] step8: Clicked Submit Claim — waiting for post-submit popup") time.sleep(3) try: view_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//button[contains(normalize-space(.),'View Status and History')] | " "//a[contains(normalize-space(.),'View Status and History')]" )) ) view_btn.click() print("[UnitedDH Claim] step8: Clicked 'View Status and History'") time.sleep(3) except TimeoutException: print("[UnitedDH Claim] step8: Post-submit popup not found — proceeding to step9") print(f"[UnitedDH Claim] step8: URL after popup: {self.driver.current_url}") return "OK" except Exception as e: return f"ERROR: step8_submit_claim - {e}" def step9_save_confirmation_pdf(self): """ On the Status & History page, read the claim number from the first row (Reference Number column), then save the page as PDF. """ import re try: print("[UnitedDH Claim] step9: waiting for Status & History page") WebDriverWait(self.driver, 20).until( lambda d: "status" in d.current_url.lower() or "history" in d.current_url.lower() or d.find_elements(By.XPATH, "//td | //th[contains(text(),'Reference')]") ) time.sleep(2) print(f"[UnitedDH Claim] step9: Status & History URL: {self.driver.current_url}") self.driver.refresh() print("[UnitedDH Claim] step9: Page refreshed — waiting for table to reload") WebDriverWait(self.driver, 15).until( EC.presence_of_element_located((By.XPATH, "//table//tr[td]")) ) time.sleep(2) claim_number = None try: first_ref = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "(//table//tr[not(th)]/td[2] | " "//table//tr[td]/td[contains(normalize-space(.),'2026') or " " contains(normalize-space(.),'2025')])[1]" )) ) ref_text = first_ref.text.strip() match = re.search(r'\b(\d{14})\b', ref_text) if match: claim_number = match.group(1) else: match = re.search(r'\b(\d{10,})\b', ref_text) if match: claim_number = match.group(1) print(f"[UnitedDH Claim] step9: Claim number: {claim_number!r} (cell: {ref_text!r})") except Exception as e: print(f"[UnitedDH Claim] step9: Could not read first-row reference number: {e}") try: body_text = self.driver.find_element(By.TAG_NAME, "body").text match = re.search(r'\b(\d{14})\b', body_text) if match: claim_number = match.group(1) print(f"[UnitedDH Claim] step9: Claim number (body scan): {claim_number}") except Exception: pass 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"uniteddh_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"[UnitedDH Claim] step9: PDF saved: {pdf_path}") except Exception as e: print(f"[UnitedDH Claim] step9: PDF capture failed: {e}") return f"ERROR: step9 PDF failed: {e}" self._hide_browser() return { "status": "success", "pdf_path": pdf_path, "claimNumber": claim_number, } except Exception as e: return f"ERROR: step9_save_confirmation_pdf - {e}" # ── Main workflow ────────────────────────────────────────────────────────── def main_workflow(self, url): try: self.config_driver() login_result = self.login(url) print(f"[main_workflow] Login result: {login_result}") if login_result == "OTP_REQUIRED": return {"status": "otp_required", "message": "OTP required after login"} if isinstance(login_result, str) and login_result.startswith("ERROR"): return {"status": "error", "message": login_result} # Step 1: eligibility-style patient search → Selected Patient page step1_result = self.step1_search_patient() print(f"[main_workflow] step1 result: {step1_result}") if isinstance(step1_result, str) and step1_result.startswith("ERROR"): return {"status": "error", "message": step1_result} # Step 2: click Submit Claim on Selected Patient page step2_result = self.step2_click_submit_claim() print(f"[main_workflow] step2 result: {step2_result}") if isinstance(step2_result, str) and step2_result.startswith("ERROR"): return {"status": "error", "message": step2_result} # Step 3: pre-filled Submit Claim page — just Continue step3_result = self.step3_continue_prefilled() print(f"[main_workflow] step3 result: {step3_result}") if isinstance(step3_result, str) and step3_result.startswith("ERROR"): return {"status": "error", "message": step3_result} # Step 4: Select Insurance popup — click Ok step4_result = self.step4_select_insurance_ok() print(f"[main_workflow] step4 result: {step4_result}") if isinstance(step4_result, str) and step4_result.startswith("ERROR"): return {"status": "error", "message": step4_result} # Step 5: Practitioner & Location — click Continue only step5_result = self.step5_practitioner_continue() print(f"[main_workflow] step5 result: {step5_result}") if isinstance(step5_result, str) and step5_result.startswith("ERROR"): return {"status": "error", "message": step5_result} # Step 6: fill CDT codes, tooth, billed amount step6_result = self.step6_fill_claim_form() print(f"[main_workflow] step6 result: {step6_result}") if isinstance(step6_result, str) and step6_result.startswith("ERROR"): return {"status": "error", "message": step6_result} # Step 7: attach files (if any) step7_result = self.step7_attach_files() print(f"[main_workflow] step7 result: {step7_result}") if isinstance(step7_result, str) and step7_result.startswith("ERROR"): return {"status": "error", "message": step7_result} # Step 8: click Submit Claim, then View Status and History step8_result = self.step8_submit_claim() print(f"[main_workflow] step8 result: {step8_result}") if isinstance(step8_result, str) and step8_result.startswith("ERROR"): return {"status": "error", "message": step8_result} # Step 9: capture claim number and PDF step9_result = self.step9_save_confirmation_pdf() print(f"[main_workflow] step9 result: {step9_result}") return step9_result except Exception as e: return {"status": "error", "message": str(e)}