From 70f36fc13cc79a1c5cc876c53d82844e35829d72 Mon Sep 17 00:00:00 2001 From: ff Date: Sat, 30 May 2026 14:46:51 -0400 Subject: [PATCH] feat: United SCO claim worker rewrite + eligibility/patient-table fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrote UnitedDH claim worker to navigate via eligibility page → Selected Patient → Submit Claim button flow - Updated helpers_uniteddh_claim.py step names to match new 9-step workflow - Changed payer selection in both eligibility and claim workers to type + Enter - Updated patient table column header from 'DOB / Gender' to 'DOB' Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/patients/patient-table.tsx | 2 +- .../SeleniumService/helpers_uniteddh_claim.py | 31 +- .../selenium_UnitedDH_claimSubmitWorker.py | 800 +++++++++++------- ...lenium_UnitedSCO_eligibilityCheckWorker.py | 142 +--- 4 files changed, 541 insertions(+), 434 deletions(-) diff --git a/apps/Frontend/src/components/patients/patient-table.tsx b/apps/Frontend/src/components/patients/patient-table.tsx index 42385195..28d6ee4a 100755 --- a/apps/Frontend/src/components/patients/patient-table.tsx +++ b/apps/Frontend/src/components/patients/patient-table.tsx @@ -972,7 +972,7 @@ export function PatientTable({ {allowCheckbox && Select} Patient - DOB / Gender + DOB {/*Contact*/} Insurance Status diff --git a/apps/SeleniumService/helpers_uniteddh_claim.py b/apps/SeleniumService/helpers_uniteddh_claim.py index 2a723fe2..7c33fc1f 100644 --- a/apps/SeleniumService/helpers_uniteddh_claim.py +++ b/apps/SeleniumService/helpers_uniteddh_claim.py @@ -250,13 +250,14 @@ async def start_uniteddh_claim_run(sid: str, data: dict, url: str): # --- Claim steps --- for step_name, step_fn in [ - ("step1_search_patient", bot.step1_search_patient), - ("step2_open_member_page", bot.step2_open_member_page), - ("step3_click_create_claim", bot.step3_click_create_claim), - ("step4_fill_claim_form", bot.step4_fill_claim_form), - ("step5_attach_files", bot.step5_attach_files), - ("step6_click_next", bot.step6_click_next), - ("step7_submit_claim", bot.step7_submit_claim), + ("step1_search_patient", bot.step1_search_patient), + ("step2_click_submit_claim", bot.step2_click_submit_claim), + ("step3_continue_prefilled", bot.step3_continue_prefilled), + ("step4_select_insurance_ok", bot.step4_select_insurance_ok), + ("step5_practitioner_continue", bot.step5_practitioner_continue), + ("step6_fill_claim_form", bot.step6_fill_claim_form), + ("step7_attach_files", bot.step7_attach_files), + ("step8_submit_claim", bot.step8_submit_claim), ]: result = step_fn() print(f"[UnitedDH Claim] {step_name} result: {result}") @@ -267,14 +268,14 @@ async def start_uniteddh_claim_run(sid: str, data: dict, url: str): asyncio.create_task(_remove_session_later(sid, 30)) return {"status": "error", "message": result} - # --- Step 8: PDF + claim number --- - step8_result = bot.step8_save_confirmation_pdf() - print(f"[UnitedDH Claim] step8 result: {step8_result}") - if isinstance(step8_result, str) and step8_result.startswith("ERROR"): - print(f"[UnitedDH Claim] step8 warning (non-fatal): {step8_result}") - step8_result = {} + # --- Step 9: PDF + claim number --- + step9_result = bot.step9_save_confirmation_pdf() + print(f"[UnitedDH Claim] step9 result: {step9_result}") + if isinstance(step9_result, str) and step9_result.startswith("ERROR"): + print(f"[UnitedDH Claim] step9 warning (non-fatal): {step9_result}") + step9_result = {} - pdf_path = step8_result.get("pdf_path") if isinstance(step8_result, dict) else None + pdf_path = step9_result.get("pdf_path") if isinstance(step9_result, dict) else None pdf_url = None if pdf_path: import os as _os @@ -284,7 +285,7 @@ async def start_uniteddh_claim_run(sid: str, data: dict, url: str): pdf_url = f"http://{url_host}:{port}/downloads/{filename}" print(f"[UnitedDH Claim] pdf_url: {pdf_url}") - claim_number = step8_result.get("claimNumber") if isinstance(step8_result, dict) else None + claim_number = step9_result.get("claimNumber") if isinstance(step9_result, dict) else None result = { "status": "success", diff --git a/apps/SeleniumService/selenium_UnitedDH_claimSubmitWorker.py b/apps/SeleniumService/selenium_UnitedDH_claimSubmitWorker.py index 05b7d44b..53b22765 100644 --- a/apps/SeleniumService/selenium_UnitedDH_claimSubmitWorker.py +++ b/apps/SeleniumService/selenium_UnitedDH_claimSubmitWorker.py @@ -1,6 +1,16 @@ """ United/DentalHub Claim Submission Worker. -Based on the UnitedSCO eligibility check worker (same portal: app.dentalhub.com). +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 @@ -39,11 +49,9 @@ class AutomationUnitedDHClaimSubmit: self.patientName = claim.get("patientName", "") self.remarks = claim.get("remarks", "") - # Credentials (injected by backend) self.uniteddh_username = claim.get("uniteddhUsername", "") self.uniteddh_password = claim.get("uniteddhPassword", "") - # Re-use the UnitedSCO browser manager (same portal) self.download_dir = get_browser_manager().download_dir os.makedirs(self.download_dir, exist_ok=True) @@ -51,7 +59,6 @@ class AutomationUnitedDHClaimSubmit: self.driver = get_browser_manager().get_driver(self.headless) def _force_logout(self): - """Force logout by clearing cookies for the DentalHub domain.""" try: print("[UnitedDH Claim login] Forcing logout due to credential change...") browser_manager = get_browser_manager() @@ -97,7 +104,6 @@ class AutomationUnitedDHClaimSubmit: self.driver.get(url) time.sleep(2) - # Check if already logged in try: current_url = self.driver.current_url print(f"[UnitedDH Claim login] Current URL: {current_url}") @@ -124,7 +130,6 @@ class AutomationUnitedDHClaimSubmit: print("[UnitedDH Claim login] Already on dashboard") return "ALREADY_LOGGED_IN" - # Check for OTP input first try: WebDriverWait(self.driver, 3).until( EC.presence_of_element_located((By.XPATH, @@ -135,7 +140,6 @@ class AutomationUnitedDHClaimSubmit: except TimeoutException: pass - # Click LOGIN button on dentalhub landing page if "app.dentalhub.com" in current_url: try: login_btn = WebDriverWait(self.driver, 5).until( @@ -151,11 +155,9 @@ class AutomationUnitedDHClaimSubmit: current_url = self.driver.current_url print(f"[UnitedDH Claim login] After LOGIN click URL: {current_url}") - # Fill Azure B2C credentials if "b2clogin.com" in current_url or "login" in current_url.lower(): print("[UnitedDH Claim login] On B2C login page - filling credentials") - # Check if already on phone verification page try: send_code_btn = self.driver.find_element(By.XPATH, "//button[@id='sendCode'] | //input[@id='sendCode'] | " @@ -198,7 +200,6 @@ class AutomationUnitedDHClaimSubmit: time.sleep(5) - # Handle MFA method selection page try: continue_btn = self.driver.find_element(By.XPATH, "//button[contains(text(),'Continue')]") phone_elements = self.driver.find_elements(By.XPATH, "//*[contains(text(),'Phone')]") @@ -222,7 +223,6 @@ class AutomationUnitedDHClaimSubmit: except Exception: pass - # Check for "Text Me" / Send Code button (phone OTP) try: send_code_btn = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, @@ -236,7 +236,6 @@ class AutomationUnitedDHClaimSubmit: except TimeoutException: pass - # Check if OTP input appeared try: WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, @@ -247,7 +246,6 @@ class AutomationUnitedDHClaimSubmit: except TimeoutException: pass - # Check if login succeeded 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") @@ -259,7 +257,6 @@ class AutomationUnitedDHClaimSubmit: except Exception as e: return f"ERROR: Login failed - {e}" - # If still on dentalhub, may already be logged in if "app.dentalhub.com" in current_url: return "ALREADY_LOGGED_IN" @@ -271,7 +268,6 @@ class AutomationUnitedDHClaimSubmit: # ── Helpers ──────────────────────────────────────────────────────────────── def _check_for_error_dialog(self): - """Check for and dismiss common error dialogs. Returns error message string or None.""" 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"), @@ -292,7 +288,7 @@ class AutomationUnitedDHClaimSubmit: except Exception: dialog_text = dialog_elem.text.strip()[:200] - print(f"[UnitedDH Claim step1] Error dialog detected: {dialog_text}") + print(f"[UnitedDH Claim] Error dialog detected: {dialog_text}") try: dismiss_btn = self.driver.find_element(By.XPATH, @@ -300,7 +296,7 @@ class AutomationUnitedDHClaimSubmit: "//div[contains(@class,'modal')]//button[contains(text(),'Ok') or contains(text(),'OK') or contains(text(),'Close')]" ) dismiss_btn.click() - print("[UnitedDH Claim step1] Dismissed error dialog") + print("[UnitedDH Claim] Dismissed error dialog") time.sleep(1) except Exception: try: @@ -317,7 +313,6 @@ class AutomationUnitedDHClaimSubmit: return None def _format_dob(self, dob_str): - """Convert DOB from YYYY-MM-DD to MM/DD/YYYY format.""" if dob_str and "-" in dob_str: dob_parts = dob_str.split("-") if len(dob_parts) == 3: @@ -325,12 +320,10 @@ class AutomationUnitedDHClaimSubmit: return dob_str def _get_existing_downloads(self): - """Get set of existing PDF files in download dir before clicking.""" import glob return set(glob.glob(os.path.join(self.download_dir, "*.pdf"))) def _wait_for_new_download(self, existing_files, timeout=15): - """Wait for a new PDF file to appear in the download dir.""" import glob for _ in range(timeout * 2): time.sleep(0.5) @@ -343,7 +336,6 @@ class AutomationUnitedDHClaimSubmit: return None def _hide_browser(self): - """Hide the browser window after task completion.""" try: try: self.driver.get("about:blank") @@ -373,7 +365,6 @@ class AutomationUnitedDHClaimSubmit: print(f"[UnitedDH Claim] Could not hide browser: {e}") def _capture_pdf(self, identifier): - """Capture the current page as PDF using Chrome DevTools Protocol.""" try: pdf_options = { "landscape": False, @@ -403,44 +394,41 @@ class AutomationUnitedDHClaimSubmit: def step1_search_patient(self): """ - 1. Navigate directly to the Submit Claim page. - 2. Fill Subscriber ID, Date of Birth, Procedure Date, select Payer - (first UnitedHealthcare Massachusetts result). - 3. Click Continue. - """ - try: - print(f"[UnitedDH Claim] step1: memberId={self.memberId}, dob={self.dateOfBirth}, serviceDate={self.serviceDate}") + 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. - # Navigate directly to the claim submission page - self.driver.get("https://app.dentalhub.com/app/claims-auths/claim") + 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 claim form to load (Subscriber ID field) --- + # Wait for Patient Information form try: WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.ID, "subscriberId_Front")) + EC.presence_of_element_located((By.ID, "firstName_Back")) ) - print("[UnitedDH Claim] step1: Claim form loaded (subscriberId_Front found)") + print("[UnitedDH Claim] step1: Patient Information form loaded") except TimeoutException: - # Try alternate field names - try: - WebDriverWait(self.driver, 5).until( - EC.presence_of_element_located((By.XPATH, - "//input[contains(@id,'subscriberId') or contains(@id,'memberId')]" - )) - ) - print("[UnitedDH Claim] step1: Claim form loaded (alternate subscriber field)") - except TimeoutException: - return "ERROR: step1 - Claim form not found after clicking Submit Claim" + print("[UnitedDH Claim] step1: Patient Information form not found") + return "ERROR: step1 - Patient Information form not found" - # --- Fill Subscriber ID --- + # 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 @@ -450,58 +438,63 @@ class AutomationUnitedDHClaimSubmit: if sid_input.is_displayed(): sid_input.clear() sid_input.send_keys(self.memberId) - print(f"[UnitedDH Claim] step1: Subscriber ID entered: {self.memberId} (field='{sid_input.get_attribute('id')}')") + 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 --- - dob_formatted = self._format_dob(self.dateOfBirth) + # Fill Date of Birth try: - dob_input = WebDriverWait(self.driver, 5).until( - EC.presence_of_element_located((By.XPATH, - "//input[@id='dateOfBirth_Back' or @id='dateOfBirth_Front']" - " | //input[contains(@placeholder,'Date of Birth') or contains(@placeholder,'DOB')]" - )) - ) + 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" - # --- Fill Procedure Date (portal pre-fills today's date — must overwrite) --- - procedure_date_formatted = self._format_dob(self.serviceDate) # YYYY-MM-DD -> MM/DD/YYYY - try: - proc_date_input = WebDriverWait(self.driver, 5).until( - EC.element_to_be_clickable((By.XPATH, - "//input[@id='procedureDate_Back' or @id='procedureDate_Front']" - " | //input[contains(@placeholder,'Procedure Date') or contains(@placeholder,'Service Date')]" - )) - ) - proc_date_input.click() - proc_date_input.send_keys(Keys.CONTROL + "a") - proc_date_input.send_keys(Keys.DELETE) - proc_date_input.send_keys(procedure_date_formatted) - print(f"[UnitedDH Claim] step1: Procedure Date entered: {procedure_date_formatted}") - except Exception as e: - print(f"[UnitedDH Claim] step1: Error entering Procedure Date: {e}") - time.sleep(1) - # --- Select Payer: first UnitedHealthcare Massachusetts result --- + # 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') or contains(@placeholder,'Payer')]]", + "//ng-select[.//input[contains(@placeholder,'Search by Payers')]]", ] payer_ng_select = None for sel in payer_selectors: @@ -518,48 +511,16 @@ class AutomationUnitedDHClaimSubmit: time.sleep(0.5) payer_ng_select.click() time.sleep(1) - - try: - search_input = payer_ng_select.find_element(By.XPATH, - ".//input[@type='text' or @role='combobox']") - search_input.clear() - search_input.send_keys("UnitedHealthcare Massachusetts") - print("[UnitedDH Claim] step1: Typed payer search text") - time.sleep(2) - except Exception: - try: - from selenium.webdriver.common.action_chains import ActionChains - ActionChains(self.driver).send_keys("UnitedHealthcare Mass").perform() - time.sleep(2) - except Exception: - pass - - # Pick the first visible matching option - payer_options = self.driver.find_elements(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option')]") - for opt in payer_options: - if opt.is_displayed(): - opt_text = opt.text.strip() - if "UnitedHealthcare" in opt_text and "Massachusetts" in opt_text: - opt.click() - print(f"[UnitedDH Claim] step1: Selected Payer: {opt_text}") - payer_selected = True - break - if not payer_selected: - # Fallback: first visible option at all - for opt in payer_options: - if opt.is_displayed(): - opt.click() - print(f"[UnitedDH Claim] step1: Selected first visible Payer: {opt.text.strip()}") - payer_selected = True - break - - try: - from selenium.webdriver.common.action_chains import ActionChains - ActionChains(self.driver).send_keys(Keys.ESCAPE).perform() - except Exception: - pass + 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: @@ -570,14 +531,14 @@ class AutomationUnitedDHClaimSubmit: time.sleep(1) - # --- Click Continue --- + # 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") - time.sleep(4) + print("[UnitedDH Claim] step1: Clicked Continue (Patient Info)") + time.sleep(3) error_result = self._check_for_error_dialog() if error_result: @@ -585,168 +546,337 @@ class AutomationUnitedDHClaimSubmit: except Exception as e: return f"ERROR: step1 - Could not click Continue: {e}" - print("[UnitedDH Claim] step1: Patient search completed") + # 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_open_member_page(self): + def step2_click_submit_claim(self): """ - After step1 clicks Continue, the portal shows a "Select Insurance" popup. - Click Ok on it, then wait for the patient info / office location page. + On the Selected Patient results page, click the Submit Claim button + (id="btnSubmitClaim"). """ try: - print("[UnitedDH Claim] step2: waiting for 'Select Insurance' popup") + print("[UnitedDH Claim] step2: Looking for Submit Claim button (id='btnSubmitClaim')...") time.sleep(2) - # Click the Ok button on the "Select Insurance" modal - 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')]" - )) - ) - ok_btn.click() - print("[UnitedDH Claim] step2: Clicked Ok on Select Insurance popup") - time.sleep(2) - except TimeoutException: - print("[UnitedDH Claim] step2: Select Insurance popup not found — proceeding") - - # Wait for patient info / office location page (Continue button visible) - try: - WebDriverWait(self.driver, 15).until( - EC.element_to_be_clickable((By.XPATH, - "//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Continue')]" - )) - ) - print("[UnitedDH Claim] step2: Patient info page loaded (Continue button found)") - except TimeoutException: - print("[UnitedDH Claim] step2: Continue button not found — proceeding anyway") + 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_open_member_page - {e}" + return f"ERROR: step2_click_submit_claim - {e}" - def step3_click_create_claim(self): + def step3_continue_prefilled(self): """ - Click Continue on the Practitioner & Location page. - The location is auto-filled by Angular asynchronously — wait for it to be - populated before clicking Continue, otherwise the field may submit blank. + 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: waiting for Practitioner & Location page") + print("[UnitedDH Claim] step3: Submit Claim page — selecting Payer then clicking Continue...") - # Wait for the Continue button — primary indicator that the page has loaded - continue_btn = WebDriverWait(self.driver, 20).until( - EC.element_to_be_clickable((By.XPATH, - "//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'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" )) ) - # --- Treatment Location --- - print("[UnitedDH Claim] step3: Selecting Treatment Location...") - location_selected = False + # Select Payer: type + Enter + payer_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:'end'});", location_ng) - arrow = location_ng.find_element(By.XPATH, ".//span[contains(@class,'ng-arrow-wrapper')]") - ActionChains(self.driver).move_to_element(arrow).click().perform() - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.XPATH, "//ng-dropdown-panel")) - ) - option_text = self.driver.find_element(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option') and not(contains(@class,'disabled'))]" - ).text.strip() - ActionChains(self.driver).send_keys(Keys.ARROW_DOWN).send_keys(Keys.ENTER).perform() - print(f"[UnitedDH Claim] step3: Selected Treatment Location: {option_text}") - location_selected = True + 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: Treatment Location selection failed: {e}") + print(f"[UnitedDH Claim] step3: Payer selection error: {e}") - if not location_selected: - print("[UnitedDH Claim] step3: WARNING: Could not select Treatment Location — continuing anyway") + if not payer_selected: + print("[UnitedDH Claim] step3: WARNING - Could not select Payer") - # --- Billing Entity --- - print("[UnitedDH Claim] step3: 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:'end'});", billing_ng) - arrow = billing_ng.find_element(By.XPATH, ".//span[contains(@class,'ng-arrow-wrapper')]") - ActionChains(self.driver).move_to_element(arrow).click().perform() - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.XPATH, "//ng-dropdown-panel")) - ) - option_text = self.driver.find_element(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option') and not(contains(@class,'disabled'))]" - ).text.strip() - ActionChains(self.driver).send_keys(Keys.ARROW_DOWN).send_keys(Keys.ENTER).perform() - print(f"[UnitedDH Claim] step3: Selected Billing Entity: {option_text}") - billing_selected = True - except Exception as e: - print(f"[UnitedDH Claim] step3: Billing Entity selection failed: {e}") + time.sleep(1) - if not billing_selected: - print("[UnitedDH Claim] step3: WARNING: Could not select Billing Entity — continuing anyway") - - # Re-find Continue button after dropdown interactions to avoid stale element reference - continue_btn = WebDriverWait(self.driver, 10).until( - EC.element_to_be_clickable((By.XPATH, - "//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Continue')]" - )) + 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 — waiting for Code Entry page") - time.sleep(3) + print("[UnitedDH Claim] step3: Clicked Continue") + time.sleep(4) - # Confirm we're on the Code Entry page - try: - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.ID, "procedureCode")) - ) - print("[UnitedDH Claim] step3: Code Entry page loaded (procedureCode found)") - except TimeoutException: - print("[UnitedDH Claim] step3: procedureCode input not found — proceeding anyway") + 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_click_create_claim - {e}" + return f"ERROR: step3_continue_prefilled - {e}" - def step4_fill_claim_form(self): + 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. (For lines after the first: click btnAddItem to start a new row) + 1. Click btnAddItem to open/activate the row 2. Type CDT code into id="procedureCode" 3. Click id="btnAddItem" — billed amount input appears - 4. Clear id="billedAmount" and enter the billed amount - 5. Click the Add button to confirm the row + 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] step4: {len(active_lines)} service line(s)") + print(f"[UnitedDH Claim] step6: {len(active_lines)} service line(s)") if not active_lines: - print("[UnitedDH Claim] step4: No service lines — skipping") + print("[UnitedDH Claim] step6: No service lines — skipping") return "OK" for idx, line in enumerate(active_lines): @@ -758,21 +888,21 @@ class AutomationUnitedDHClaimSubmit: ).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] step4: line {idx}: code={code}, billed={billed}, tooth={tooth}, surface={surface}") + print(f"[UnitedDH Claim] step6: line {idx}: code={code}, billed={billed}, tooth={tooth}, surface={surface}") - # For ALL rows, click btnAddItem to open/activate the procedure row + # 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] step4: clicked btnAddItem to open row {idx}") + print(f"[UnitedDH Claim] step6: clicked btnAddItem to open row {idx}") time.sleep(1) except Exception as e: - print(f"[UnitedDH Claim] step4: could not click btnAddItem to open row {idx}: {e}") + print(f"[UnitedDH Claim] step6: could not click btnAddItem to open row {idx}: {e}") - # Type CDT code in procedureCode input + # Type CDT code try: proc_input = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.ID, "procedureCode")) @@ -782,10 +912,10 @@ class AutomationUnitedDHClaimSubmit: proc_input.send_keys(Keys.CONTROL + "a") proc_input.send_keys(Keys.DELETE) proc_input.send_keys(code) - print(f"[UnitedDH Claim] step4: typed procedure code: {code}") + print(f"[UnitedDH Claim] step6: typed procedure code: {code}") time.sleep(0.5) except Exception as e: - print(f"[UnitedDH Claim] step4: could not type procedure code for row {idx}: {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 @@ -795,10 +925,10 @@ class AutomationUnitedDHClaimSubmit: ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn) add_btn.click() - print(f"[UnitedDH Claim] step4: clicked btnAddItem to reveal billedAmount for row {idx}") + 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] step4: could not click btnAddItem for billed amount row {idx}: {e}") + print(f"[UnitedDH Claim] step6: could not click btnAddItem for billed amount row {idx}: {e}") continue # Fill tooth number @@ -812,18 +942,16 @@ class AutomationUnitedDHClaimSubmit: tooth_input.send_keys(Keys.CONTROL + "a") tooth_input.send_keys(Keys.DELETE) tooth_input.send_keys(tooth) - print(f"[UnitedDH Claim] step4: entered tooth number: {tooth} for row {idx}") + print(f"[UnitedDH Claim] step6: entered tooth number: {tooth}") time.sleep(0.3) except Exception as e: - print(f"[UnitedDH Claim] step4: could not fill tooth number for row {idx}: {e}") + print(f"[UnitedDH Claim] step6: could not fill tooth number for row {idx}: {e}") - # Click surface boxes (B, D, F, L, M, O, etc.) — only present for filling codes + # Click surface boxes if surface: try: - # Check surface box group is present before trying to click surface_boxes = self.driver.find_elements(By.XPATH, - "//div[contains(@class,'claim-add-item-group__box')]" - ) + "//div[contains(@class,'claim-add-item-group__box')]") if surface_boxes: for letter in surface: if not letter.strip(): @@ -836,14 +964,14 @@ class AutomationUnitedDHClaimSubmit: ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", box) box.click() - print(f"[UnitedDH Claim] step4: clicked surface '{letter}' for row {idx}") + print(f"[UnitedDH Claim] step6: clicked surface '{letter}'") time.sleep(0.2) except Exception: - print(f"[UnitedDH Claim] step4: surface '{letter}' not found or disabled for row {idx}") + print(f"[UnitedDH Claim] step6: surface '{letter}' not found or disabled") else: - print(f"[UnitedDH Claim] step4: no surface boxes on page for row {idx} — skipping") + print(f"[UnitedDH Claim] step6: no surface boxes on page — skipping") except Exception as e: - print(f"[UnitedDH Claim] step4: surface click error for row {idx}: {e}") + print(f"[UnitedDH Claim] step6: surface click error for row {idx}: {e}") # Fill billed amount if billed: @@ -856,10 +984,10 @@ class AutomationUnitedDHClaimSubmit: billed_input.send_keys(Keys.CONTROL + "a") billed_input.send_keys(Keys.DELETE) billed_input.send_keys(billed) - print(f"[UnitedDH Claim] step4: entered billed amount: {billed}") + print(f"[UnitedDH Claim] step6: entered billed amount: {billed}") time.sleep(0.5) except Exception as e: - print(f"[UnitedDH Claim] step4: could not fill billed amount for row {idx}: {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: @@ -872,15 +1000,14 @@ class AutomationUnitedDHClaimSubmit: ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", span_add) span_add.click() - print(f"[UnitedDH Claim] step4: clicked span Add — row {idx} confirmed") + print(f"[UnitedDH Claim] step6: clicked span Add — row {idx} confirmed") time.sleep(1) except Exception as e: - print(f"[UnitedDH Claim] step4: could not click span Add for row {idx}: {e}") + print(f"[UnitedDH Claim] step6: could not click span Add for row {idx}: {e}") - # --- Other coverage section: click "No" (second radio button) --- + # Other coverage: click "No" (second radio button) try: - print("[UnitedDH Claim] step4: selecting 'No' for Other coverage") - # The "No" option is the second radio button on the page + 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']") ) @@ -888,20 +1015,20 @@ class AutomationUnitedDHClaimSubmit: no_radio = radio_buttons[1] self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", no_radio) no_radio.click() - print("[UnitedDH Claim] step4: Clicked 'No' (2nd radio) for Other coverage") + print("[UnitedDH Claim] step6: Clicked 'No' (2nd radio) for Other coverage") else: - print(f"[UnitedDH Claim] step4: Only {len(radio_buttons)} radio button(s) found — skipping") + 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] step4: Could not click 'No' for Other coverage (non-fatal): {e}") + print(f"[UnitedDH Claim] step6: Could not click 'No' for Other coverage (non-fatal): {e}") - print("[UnitedDH Claim] step4: Done filling claim form") + print("[UnitedDH Claim] step6: Done filling claim form") return "OK" except Exception as e: - return f"ERROR: step4_fill_claim_form - {e}" + return f"ERROR: step6_fill_claim_form - {e}" - def step5_attach_files(self): + def step7_attach_files(self): """ If there are claim files: 1. Click the fa-caret-up dropdown icon to reveal the Add Document button @@ -910,10 +1037,9 @@ class AutomationUnitedDHClaimSubmit: """ try: if not self.claimFiles: - print("[UnitedDH Claim] step5: No files to attach") + print("[UnitedDH Claim] step7: No files to attach") return "OK" - # Open the Attached Documents section by clicking the caret-up icon try: caret = WebDriverWait(self.driver, 8).until( EC.element_to_be_clickable((By.XPATH, @@ -924,26 +1050,25 @@ class AutomationUnitedDHClaimSubmit: ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", caret) caret.click() - print("[UnitedDH Claim] step5: Clicked caret-up to expand Attached Documents") + print("[UnitedDH Claim] step7: Clicked caret-up to expand Attached Documents") time.sleep(1) except Exception as e: - print(f"[UnitedDH Claim] step5: Could not click caret (section may already be open): {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] step5: Skipping file with no filePath: {cf}") + 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] step5: File not found on disk: {abs_path}") + print(f"[UnitedDH Claim] step7: File not found on disk: {abs_path}") continue - print(f"[UnitedDH Claim] step5: Attaching: {abs_path}") + print(f"[UnitedDH Claim] step7: Attaching: {abs_path}") try: - # Click the Add Document button upload_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.ID, "upload-document")) ) @@ -951,37 +1076,30 @@ class AutomationUnitedDHClaimSubmit: upload_btn.click() time.sleep(1) - # Find the file input (may be hidden) and send the path 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] step5: Attached: {os.path.basename(abs_path)}") + print(f"[UnitedDH Claim] step7: Attached: {os.path.basename(abs_path)}") attached += 1 except Exception as e: - print(f"[UnitedDH Claim] step5: Could not attach {abs_path}: {e}") + print(f"[UnitedDH Claim] step7: Could not attach {abs_path}: {e}") - print(f"[UnitedDH Claim] step5: Attached {attached}/{len(self.claimFiles)} file(s)") + print(f"[UnitedDH Claim] step7: Attached {attached}/{len(self.claimFiles)} file(s)") return "OK" except Exception as e: - return f"ERROR: step5_attach_files - {e}" + return f"ERROR: step7_attach_files - {e}" - def step6_click_next(self): - """No separate Next step on DentalHub claim form — submission goes directly from Code Entry.""" - print("[UnitedDH Claim] step6: no-op (DentalHub has no Next step before Submit)") - return "OK" - - def step7_submit_claim(self): + def step8_submit_claim(self): """ - Click Submit Claim, then handle the post-submit popup: - - Top button: "Submit another claim" - - Bottom button: "View Status and History" ← click this one + Click Submit Claim on the Code Entry page, then click + "View Status and History" on the post-submit popup. """ try: - print(f"[UnitedDH Claim] step7: submitting claim — URL: {self.driver.current_url}") + 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, @@ -993,10 +1111,9 @@ class AutomationUnitedDHClaimSubmit: self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_btn) time.sleep(0.5) submit_btn.click() - print("[UnitedDH Claim] step7: Clicked Submit Claim — waiting for post-submit popup") + print("[UnitedDH Claim] step8: Clicked Submit Claim — waiting for post-submit popup") time.sleep(3) - # Click "View Status and History" (the bottom button in the popup) try: view_btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, @@ -1005,46 +1122,42 @@ class AutomationUnitedDHClaimSubmit: )) ) view_btn.click() - print("[UnitedDH Claim] step7: Clicked 'View Status and History'") + print("[UnitedDH Claim] step8: Clicked 'View Status and History'") time.sleep(3) except TimeoutException: - print("[UnitedDH Claim] step7: Post-submit popup not found — proceeding to step8") + print("[UnitedDH Claim] step8: Post-submit popup not found — proceeding to step9") - print(f"[UnitedDH Claim] step7: URL after popup: {self.driver.current_url}") + print(f"[UnitedDH Claim] step8: URL after popup: {self.driver.current_url}") return "OK" except Exception as e: - return f"ERROR: step7_submit_claim - {e}" + return f"ERROR: step8_submit_claim - {e}" - def step8_save_confirmation_pdf(self): + 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] step8: waiting for Status & History page") + print("[UnitedDH Claim] step9: waiting for Status & History page") - # Wait for Status & History page to load 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] step8: Status & History URL: {self.driver.current_url}") + print(f"[UnitedDH Claim] step9: Status & History URL: {self.driver.current_url}") - # Refresh so the just-submitted claim appears at the top self.driver.refresh() - print("[UnitedDH Claim] step8: Page refreshed — waiting for table to reload") + 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) - # Extract claim number from the first data row, Reference Number column claim_number = None try: - # The first in the first data row that looks like a 14-digit reference number first_ref = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "(//table//tr[not(th)]/td[2] | " @@ -1053,29 +1166,25 @@ class AutomationUnitedDHClaimSubmit: )) ) ref_text = first_ref.text.strip() - # Reference numbers are 14-digit integers like 20260524181895 match = re.search(r'\b(\d{14})\b', ref_text) if match: claim_number = match.group(1) else: - # Fallback: any 10+ digit number in the cell match = re.search(r'\b(\d{10,})\b', ref_text) if match: claim_number = match.group(1) - print(f"[UnitedDH Claim] step8: Claim number from first row: {claim_number!r} (cell text: {ref_text!r})") + print(f"[UnitedDH Claim] step9: Claim number: {claim_number!r} (cell: {ref_text!r})") except Exception as e: - print(f"[UnitedDH Claim] step8: Could not read first-row reference number: {e}") - # Last-resort: scan all visible text for a 14-digit number + 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] step8: Claim number (body scan): {claim_number}") + print(f"[UnitedDH Claim] step9: Claim number (body scan): {claim_number}") except Exception: pass - # Save page as PDF 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 "-_.") @@ -1097,10 +1206,12 @@ class AutomationUnitedDHClaimSubmit: pdf_bytes = base64.b64decode(pdf_data["data"]) with open(pdf_path, "wb") as f: f.write(pdf_bytes) - print(f"[UnitedDH Claim] step8: PDF saved: {pdf_path}") + print(f"[UnitedDH Claim] step9: PDF saved: {pdf_path}") except Exception as e: - print(f"[UnitedDH Claim] step8: PDF capture failed: {e}") - return f"ERROR: step8 PDF failed: {e}" + print(f"[UnitedDH Claim] step9: PDF capture failed: {e}") + return f"ERROR: step9 PDF failed: {e}" + + self._hide_browser() return { "status": "success", @@ -1109,4 +1220,75 @@ class AutomationUnitedDHClaimSubmit: } except Exception as e: - return f"ERROR: step8_save_confirmation_pdf - {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)} diff --git a/apps/SeleniumService/selenium_UnitedSCO_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_UnitedSCO_eligibilityCheckWorker.py index a7fcd449..c6a789ef 100644 --- a/apps/SeleniumService/selenium_UnitedSCO_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_UnitedSCO_eligibilityCheckWorker.py @@ -524,7 +524,7 @@ class AutomationUnitedSCOEligibilityCheck: # Step 1.2: Select Payer - UnitedHealthcare Massachusetts print("[UnitedSCO step1] Selecting Payer...") - + # First dismiss any blocking dialogs (e.g. Chrome password save) try: self.driver.execute_script(""" @@ -534,12 +534,10 @@ class AutomationUnitedSCOEligibilityCheck: """) except Exception: pass - + payer_selected = False - - # Strategy 1: Click the ng-select, type to search, and select the option + try: - # Find the Payer ng-select by multiple selectors payer_selectors = [ "//label[contains(text(),'Payer')]/following-sibling::ng-select", "//label[contains(text(),'Payer')]/..//ng-select", @@ -554,101 +552,27 @@ class AutomationUnitedSCOEligibilityCheck: break except Exception: continue - + if payer_ng_select: - # Scroll to it and click to open self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", payer_ng_select) time.sleep(0.5) payer_ng_select.click() time.sleep(1) - - # Type into the search input inside ng-select to filter options - try: - 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("[UnitedSCO step1] Typed payer search text") - time.sleep(2) - except Exception: - # If no search input, try sending keys directly to ng-select - try: - ActionChains(self.driver).send_keys("UnitedHealthcare Mass").perform() - print("[UnitedSCO step1] Typed payer search via ActionChains") - time.sleep(2) - except Exception: - pass - - # Find and click the matching option - payer_options = self.driver.find_elements(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option')]" - ) - for opt in payer_options: - opt_text = opt.text.strip() - if "UnitedHealthcare Massachusetts" in opt_text: - opt.click() - print(f"[UnitedSCO step1] Selected Payer: {opt_text}") - payer_selected = True - break - - if not payer_selected and payer_options: - # Select first visible option if it contains "United" - for opt in payer_options: - opt_text = opt.text.strip() - if "United" in opt_text and opt.is_displayed(): - opt.click() - print(f"[UnitedSCO step1] Selected first matching Payer: {opt_text}") - payer_selected = True - break - - # Close dropdown - ActionChains(self.driver).send_keys(Keys.ESCAPE).perform() + 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("[UnitedSCO step1] Typed payer search text") + time.sleep(2) + search_input.send_keys(Keys.ENTER) + print("[UnitedSCO step1] Pressed Enter to select Payer") time.sleep(0.5) + payer_selected = True else: print("[UnitedSCO step1] Could not find Payer ng-select element") - except Exception as e: - print(f"[UnitedSCO step1] Payer selection strategy 1 error: {e}") - try: - ActionChains(self.driver).send_keys(Keys.ESCAPE).perform() - except Exception: - pass - - # Strategy 2: JavaScript direct selection if strategy 1 failed - if not payer_selected: - try: - # Try clicking via JavaScript - clicked = self.driver.execute_script(""" - // Find ng-select near the Payer label - var labels = document.querySelectorAll('label'); - for (var i = 0; i < labels.length; i++) { - if (labels[i].textContent.includes('Payer')) { - var parent = labels[i].parentElement; - var ngSelect = parent.querySelector('ng-select') || labels[i].nextElementSibling; - if (ngSelect) { - ngSelect.click(); - return true; - } - } - } - return false; - """) - if clicked: - time.sleep(1) - ActionChains(self.driver).send_keys("UnitedHealthcare Mass").perform() - time.sleep(2) - payer_options = self.driver.find_elements(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option')]" - ) - for opt in payer_options: - if "UnitedHealthcare" in opt.text and "Massachusetts" in opt.text: - opt.click() - print(f"[UnitedSCO step1] Selected Payer via JS: {opt.text.strip()}") - payer_selected = True - break - ActionChains(self.driver).send_keys(Keys.ESCAPE).perform() - except Exception as e: - print(f"[UnitedSCO step1] Payer selection strategy 2 error: {e}") - + print(f"[UnitedSCO step1] Payer selection error: {e}") + if not payer_selected: print("[UnitedSCO step1] WARNING: Could not select Payer - form may fail") @@ -699,6 +623,10 @@ class AutomationUnitedSCOEligibilityCheck: print("[UnitedSCO step1] Select Insurance modal closed") except TimeoutException: print("[UnitedSCO step1] Modal staleness timeout — continuing anyway") + # Wait for the Provider & Location page to begin rendering after modal closes + WebDriverWait(self.driver, 5).until( + EC.presence_of_element_located((By.XPATH, "//label[@for='treatmentLocation'] | //label[@for='paymentGroupId']")) + ) except TimeoutException: print("[UnitedSCO step1] Select Insurance popup not found — proceeding") @@ -721,17 +649,15 @@ class AutomationUnitedSCOEligibilityCheck: "//label[@for='treatmentLocation']/..//ng-select" )) ) - self.driver.execute_script("arguments[0].scrollIntoView({block:'end'});", location_ng) - arrow = location_ng.find_element(By.XPATH, ".//span[contains(@class,'ng-arrow-wrapper')]") - ActionChains(self.driver).move_to_element(arrow).click().perform() - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.XPATH, "//ng-dropdown-panel")) - ) - first_option = self.driver.find_element(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option') and not(contains(@class,'disabled'))]" + # Center in viewport so panel opens downward instead of upward + 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() - ActionChains(self.driver).send_keys(Keys.ARROW_DOWN).send_keys(Keys.ENTER).perform() + first_option.click() print(f"[UnitedSCO step1] Selected Treatment Location: {option_text}") location_selected = True except Exception as e: @@ -750,17 +676,15 @@ class AutomationUnitedSCOEligibilityCheck: "//label[@for='paymentGroupId']/..//ng-select" )) ) - self.driver.execute_script("arguments[0].scrollIntoView({block:'end'});", billing_ng) - arrow = billing_ng.find_element(By.XPATH, ".//span[contains(@class,'ng-arrow-wrapper')]") - ActionChains(self.driver).move_to_element(arrow).click().perform() - WebDriverWait(self.driver, 10).until( - EC.presence_of_element_located((By.XPATH, "//ng-dropdown-panel")) - ) - first_option = self.driver.find_element(By.XPATH, - "//ng-dropdown-panel//div[contains(@class,'ng-option') and not(contains(@class,'disabled'))]" + # Center in viewport so panel opens downward instead of upward + 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() - ActionChains(self.driver).send_keys(Keys.ARROW_DOWN).send_keys(Keys.ENTER).perform() + first_option.click() print(f"[UnitedSCO step1] Selected Billing Entity: {option_text}") billing_selected = True except Exception as e: