""" Tufts SCO (DentaQuest) Pre-Authorization Worker. Portal: providers.dentaquest.com Steps mirror the claim worker exactly, except: - step3 clicks "Create prior authorization" instead of "Create claim" - step6/7 target pre-auth submit buttons/text - step8 extracts a pre-auth number instead of a claim number """ from selenium.common.exceptions import TimeoutException 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 import time import os import base64 import re from dentaquest_browser_manager import get_browser_manager MEMBERS_URL = "https://providers.dentaquest.com/members" _SERVICE_DIR = os.path.dirname(os.path.abspath(__file__)) _BACKEND_CWD = os.path.normpath(os.path.join(_SERVICE_DIR, "..", "Backend")) class AutomationTuftsSCOPreAuth: 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.dentaquest_username = claim.get("dentaquestUsername", "") self.dentaquest_password = claim.get("dentaquestPassword", "") 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("[TuftsSCO PreAuth login] Forcing logout due to credential change...") browser_manager = get_browser_manager() try: self.driver.get("https://providers.dentaquest.com/") 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']", "//*[contains(@class,'logout') or contains(@class,'signout')]", ]: try: btn = WebDriverWait(self.driver, 3).until(EC.element_to_be_clickable((By.XPATH, selector))) btn.click() print("[TuftsSCO PreAuth login] Clicked logout button") time.sleep(2) break except TimeoutException: continue except Exception as e: print(f"[TuftsSCO PreAuth login] Could not click logout button: {e}") try: self.driver.delete_all_cookies() print("[TuftsSCO PreAuth login] Cleared all cookies") except Exception as e: print(f"[TuftsSCO PreAuth login] Error clearing cookies: {e}") browser_manager.clear_credentials_hash() return True except Exception as e: print(f"[TuftsSCO PreAuth login] Error during forced logout: {e}") return False def _is_maintenance_page(self) -> bool: try: body = self.driver.find_element(By.TAG_NAME, "body").text.lower() markers = ["temporarily unable to service", "maintenance downtime", "capacity problems"] return any(m in body for m in markers) except Exception: return False def login(self, url): wait = WebDriverWait(self.driver, 30) browser_manager = get_browser_manager() try: if self.dentaquest_username and browser_manager.credentials_changed(self.dentaquest_username): self._force_logout() self.driver.get(url) time.sleep(2) try: current_url = self.driver.current_url print(f"[TuftsSCO PreAuth login] Current URL: {current_url}") if "dashboard" in current_url.lower() or "member" in current_url.lower(): try: WebDriverWait(self.driver, 3).until( EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]')) ) print("[TuftsSCO PreAuth login] Already logged in") return "ALREADY_LOGGED_IN" except TimeoutException: pass except Exception as e: print(f"[TuftsSCO PreAuth login] Error checking current state: {e}") self.driver.get(url) time.sleep(3) if self._is_maintenance_page(): return "ERROR: DentaQuest portal is in maintenance mode" current_url = self.driver.current_url.lower() print(f"[TuftsSCO PreAuth login] After navigation URL: {current_url}") if "dashboard" in current_url or "member" in current_url: try: WebDriverWait(self.driver, 3).until( EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]')) ) print("[TuftsSCO PreAuth login] Already on dashboard") return "ALREADY_LOGGED_IN" except TimeoutException: pass # Dismiss "Authentication flow continued in another tab" modal if present try: ok_button = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, "//button[normalize-space(text())='Ok' or normalize-space(text())='OK' " "or normalize-space(text())='Continue']" )) ) ok_button.click() print("[TuftsSCO PreAuth login] Dismissed modal") time.sleep(3) try: WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]')) ) print("[TuftsSCO PreAuth login] Already authenticated after modal dismiss") return "ALREADY_LOGGED_IN" except TimeoutException: pass except TimeoutException: pass # Check for OTP input on page 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') or contains(@name,'otp')]")) ) print("[TuftsSCO PreAuth login] OTP input found on arrival") return "OTP_REQUIRED" except TimeoutException: pass # Fill login credentials try: username_field = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//input[@type='email' or @name='username' or @id='username' or " "@name='Email' or @placeholder='Email' or @placeholder='Username' or @type='text']")) ) username_field.clear() username_field.send_keys(self.dentaquest_username) print(f"[TuftsSCO PreAuth login] Entered username") password_field = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//input[@type='password']")) ) password_field.clear() password_field.send_keys(self.dentaquest_password) print("[TuftsSCO PreAuth login] Entered password") try: remember_me = WebDriverWait(self.driver, 3).until( EC.element_to_be_clickable((By.XPATH, "//label[.//span[contains(text(),'Remember me')] or " "contains(translate(text(),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'remember me')] | " "//input[@type='checkbox' and (" "contains(translate(@name,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'remember') or " "contains(translate(@id,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz'),'remember'))]" )) ) remember_me.click() print("[TuftsSCO PreAuth login] Checked 'Remember me'") except Exception: print("[TuftsSCO PreAuth login] No 'Remember me' found (continuing)") signin_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[@type='submit'] | //input[@type='submit'] | " "//button[contains(text(),'Sign') or contains(text(),'Log')]")) ) signin_btn.click() print("[TuftsSCO PreAuth login] Clicked Sign in") if self.dentaquest_username: browser_manager.save_credentials_hash(self.dentaquest_username) try: WebDriverWait(self.driver, 30).until( EC.presence_of_element_located((By.XPATH, "//input[@type='tel' or contains(@placeholder,'code') or contains(@placeholder,'Code') or " "contains(@aria-label,'Verification') or contains(@aria-label,'verification') or " "contains(@name,'otp') or contains(@name,'code')]" )) ) print("[TuftsSCO PreAuth login] OTP required after sign-in") return "OTP_REQUIRED" except TimeoutException: pass current_url = self.driver.current_url.lower() if "dashboard" in current_url or "member" in current_url: print("[TuftsSCO PreAuth login] Login succeeded without OTP") return "SUCCESS" print(f"[TuftsSCO PreAuth login] Unexpected state — URL: {current_url}") return "SUCCESS" except Exception as e: return f"ERROR: Login failed - {e}" except Exception as e: return f"ERROR: Login exception - {e}" # ── Step 1: Search patient ───────────────────────────────────────────────── def step1_search_patient(self): """Navigate to member search and find the patient by Member ID + DOB.""" wait = WebDriverWait(self.driver, 30) def replace_with_sendkeys(el, value): el.click() time.sleep(0.05) el.send_keys(Keys.CONTROL, "a") el.send_keys(Keys.BACKSPACE) el.send_keys(value) try: print(f"[TuftsSCO PreAuth step1] Current URL: {self.driver.current_url}") print(f"[TuftsSCO PreAuth step1] Searching memberId={self.memberId} dob={self.dateOfBirth}") if self._is_maintenance_page(): return "ERROR: DentaQuest portal is in maintenance mode" time.sleep(2) try: dob_parts = self.dateOfBirth.split("-") dob_year = dob_parts[0] dob_month = dob_parts[1].zfill(2) dob_day = dob_parts[2].zfill(2) print(f"[TuftsSCO PreAuth step1] Parsed DOB: {dob_month}/{dob_day}/{dob_year}") except Exception as e: return f"ERROR: step1 DOB parse failed: {e}" # Select Location from dropdown try: trigger = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="member-search_location_select-btn"]')) ) trigger.click() print("[TuftsSCO PreAuth step1] Clicked location dropdown") time.sleep(0.5) first_option = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, "(//li[@role='option'])[1]")) ) opt_text = first_option.get_attribute("aria-label") or first_option.text.strip() first_option.click() print(f"[TuftsSCO PreAuth step1] Selected location: {opt_text[:60]}") time.sleep(0.3) except TimeoutException: print("[TuftsSCO PreAuth step1] Warning: Location dropdown not found (continuing)") except Exception as e: print(f"[TuftsSCO PreAuth step1] Warning: Location select failed: {e}") # Fill DOB try: dob_container = wait.until(EC.presence_of_element_located( (By.XPATH, "//div[@data-testid='member-search_date-of-birth']") )) month_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']") day_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']") year_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='year' and @contenteditable='true']") replace_with_sendkeys(month_elem, dob_month) time.sleep(0.1) replace_with_sendkeys(day_elem, dob_day) time.sleep(0.1) replace_with_sendkeys(year_elem, dob_year) print(f"[TuftsSCO PreAuth step1] Filled DOB: {dob_month}/{dob_day}/{dob_year}") except Exception as e: print(f"[TuftsSCO PreAuth step1] Warning: Could not fill DOB: {e}") time.sleep(0.3) # Fill Member ID if self.memberId: try: member_id_input = wait.until(EC.presence_of_element_located( (By.XPATH, '//input[@placeholder="Search by member ID"]') )) member_id_input.clear() member_id_input.send_keys(self.memberId) print(f"[TuftsSCO PreAuth step1] Entered Member ID: {self.memberId}") time.sleep(0.2) except Exception as e: print(f"[TuftsSCO PreAuth step1] Warning: Could not fill Member ID: {e}") time.sleep(0.3) # Click Search try: search_btn = wait.until(EC.element_to_be_clickable( (By.XPATH, '//button[@data-testid="member-search_search-button"]') )) search_btn.click() print("[TuftsSCO PreAuth step1] Clicked Search button") except TimeoutException: try: search_btn = self.driver.find_element(By.XPATH, '//button[contains(text(),"Search")]') search_btn.click() print("[TuftsSCO PreAuth step1] Clicked Search button (fallback)") except Exception: ActionChains(self.driver).send_keys(Keys.RETURN).perform() print("[TuftsSCO PreAuth step1] Pressed Enter to search") WebDriverWait(self.driver, 15).until( EC.any_of( EC.presence_of_element_located((By.XPATH, "//tbody//tr")), EC.presence_of_element_located((By.XPATH, '//*[contains(@data-testid,"no-results") or contains(text(),"No results") ' 'or contains(text(),"No member found") or contains(text(),"Nothing was found")]' )), ) ) time.sleep(4) try: no_results = self.driver.find_element(By.XPATH, '//*[contains(@data-testid,"no-results") or contains(text(),"No results") ' 'or contains(text(),"No member found")]' ) if no_results and no_results.is_displayed(): return "ERROR: No patient found with given search criteria" except Exception: pass print("[TuftsSCO PreAuth step1] Search completed") return "SUCCESS" except Exception as e: print(f"[TuftsSCO PreAuth step1] Exception: {e}") return f"ERROR: step1 failed: {e}" # ── Step 2: Open member information page ─────────────────────────────────── def step2_open_member_page(self): """Click patient name link → Member Information page.""" try: try: WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//tbody//tr")) ) time.sleep(2) except TimeoutException: print("[TuftsSCO PreAuth step2] Warning: Results table not found within timeout") detail_url = None for selector in [ "(//table//tbody//tr)[1]//td[1]//a", "(//tbody//tr)[1]//a[contains(@href,'member-details')]", "(//tbody//tr)[1]//a[contains(@href,'member')]", "//a[contains(@href,'member-details')]", ]: try: link_el = WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, selector)) ) href = link_el.get_attribute("href") if href and "member" in href: detail_url = href print(f"[TuftsSCO PreAuth step2] Found detail URL: {href}") break except Exception: continue if not detail_url: return "ERROR: step2 failed: could not find member link" self.driver.get(detail_url) print(f"[TuftsSCO PreAuth step2] Navigating to: {detail_url}") try: WebDriverWait(self.driver, 15).until( lambda d: "member" in d.current_url ) print(f"[TuftsSCO PreAuth step2] Member Information page loaded: {self.driver.current_url}") except TimeoutException: print(f"[TuftsSCO PreAuth step2] Warning — URL: {self.driver.current_url}") # Wait for either Create claim or Create prior authorization button try: WebDriverWait(self.driver, 15).until( EC.any_of( EC.presence_of_element_located((By.XPATH, "//button[@aria-label='Create prior authorization']")), EC.presence_of_element_located((By.XPATH, "//button[@aria-label='Create claim']")), ) ) print("[TuftsSCO PreAuth step2] Action button found on member page") except TimeoutException: print("[TuftsSCO PreAuth step2] Warning: Action button not found within timeout") time.sleep(2) return "SUCCESS" except Exception as e: print(f"[TuftsSCO PreAuth step2] Exception: {e}") return f"ERROR: step2 failed: {e}" # ── Step 3: Click "Create prior authorization" button ───────────────────── def step3_click_create_preauth(self): """Click the 'Create prior authorization' button on the Member Information page.""" try: print(f"[TuftsSCO PreAuth step3] Current URL: {self.driver.current_url}") handles_before = set(self.driver.window_handles) self.driver.execute_script("window.scrollTo(0, 0);") time.sleep(0.5) all_btns = self.driver.find_elements(By.XPATH, "//button") print(f"[TuftsSCO PreAuth step3] Buttons on page: {[b.get_attribute('aria-label') or b.text for b in all_btns]}") btn = None try: btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[.//span[normalize-space(text())='pre-authorization']]" )) ) print("[TuftsSCO PreAuth step3] Found button via pre-authorization") except TimeoutException: pass if btn is None: # Fallbacks for xpath in [ "//button[@aria-label='Create prior authorization' and @data-react-aria-pressable='true']", "//button[contains(normalize-space(.),'prior authorization') or contains(normalize-space(.),'pre-authorization')]", ]: try: btn = WebDriverWait(self.driver, 5).until( EC.element_to_be_clickable((By.XPATH, xpath)) ) print(f"[TuftsSCO PreAuth step3] Found button via fallback") break except TimeoutException: continue if btn is None: return "ERROR: step3 failed: could not find pre-authorization button" print(f"[TuftsSCO PreAuth step3] Found button: displayed={btn.is_displayed()}, enabled={btn.is_enabled()}") self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn) time.sleep(0.5) self.driver.execute_script(""" var el = arguments[0]; el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true})); """, btn) print("[TuftsSCO PreAuth step3] Dispatched pointer+click events") time.sleep(2) handles_after = set(self.driver.window_handles) new_handles = handles_after - handles_before if new_handles: self.driver.switch_to.window(new_handles.pop()) print("[TuftsSCO PreAuth step3] Switched to new tab") print(f"[TuftsSCO PreAuth step3] Post-click URL: {self.driver.current_url}") try: WebDriverWait(self.driver, 20).until( EC.any_of( EC.presence_of_element_located((By.XPATH, "//input[contains(@id,'procedureCode')]")), EC.presence_of_element_located((By.XPATH, "//span[@data-type='month' and @contenteditable='true']")), EC.presence_of_element_located((By.XPATH, "//*[contains(text(),'date of service') or contains(text(),'Date of service') " "or contains(text(),'Procedure code') or contains(text(),'prior auth')]" )), ) ) print("[TuftsSCO PreAuth step3] Pre-auth form loaded") except TimeoutException: page_text = self.driver.execute_script("return document.body.innerText;")[:400] print(f"[TuftsSCO PreAuth step3] Pre-auth form not detected — page: {page_text}") time.sleep(1) return "SUCCESS" except Exception as e: print(f"[TuftsSCO PreAuth step3] Exception: {e}") return f"ERROR: step3 failed: {e}" # ── Step 4: Fill service date and procedure lines ────────────────────────── def _parse_service_date(self): s = str(self.serviceDate or "").strip() if not s: return None, None, None if "-" in s: parts = s.split("-") if len(parts) == 3 and len(parts[0]) == 4: return parts[1].zfill(2), parts[2].zfill(2), parts[0] if len(parts) == 3 and len(parts[2]) == 4: return parts[0].zfill(2), parts[1].zfill(2), parts[2] if "/" in s: parts = s.split("/") if len(parts) == 3: return parts[0].zfill(2), parts[1].zfill(2), parts[2] return None, None, None def _fill_spinbutton(self, label_fragment, value): for sel in [ f"//span[@contenteditable='true' and contains(@aria-label,'{label_fragment}')]", f"//span[@data-type='{label_fragment}' and @contenteditable='true']", ]: try: elem = WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, sel)) ) elem.click() elem.send_keys(Keys.CONTROL, "a") elem.send_keys(Keys.BACKSPACE) elem.send_keys(value) time.sleep(0.1) print(f"[TuftsSCO PreAuth step4] Filled spinbutton '{label_fragment}' = {value!r}") return True except Exception: continue print(f"[TuftsSCO PreAuth step4] Warning: spinbutton '{label_fragment}' not found") return False def _fill_combobox(self, inp, value, label="field"): try: inp.click() inp.send_keys(Keys.CONTROL + "a") inp.send_keys(Keys.DELETE) inp.send_keys(str(value)) time.sleep(1.5) # give portal time to load autocomplete results listbox_id = inp.get_attribute("aria-controls") or "" try: if listbox_id: option = WebDriverWait(self.driver, 6).until( EC.element_to_be_clickable((By.XPATH, f"//*[@id='{listbox_id}']//*[@role='option'][1]" )) ) else: option = WebDriverWait(self.driver, 6).until( EC.element_to_be_clickable((By.XPATH, f"//*[@role='listbox']//*[@role='option' and contains(normalize-space(.),'{value}')]" f" | //*[@role='listbox']//*[@role='option'][1]" )) ) option.click() print(f"[TuftsSCO PreAuth step4] {label}: selected '{value}'") except TimeoutException: inp.send_keys(Keys.TAB) print(f"[TuftsSCO PreAuth step4] {label}: typed '{value}' (no dropdown — pressed TAB)") except Exception as e: print(f"[TuftsSCO PreAuth step4] Warning: could not fill {label}: {e}") def _fill_tooth_field(self, idx, tooth, context=""): """Fill the Tooth input for row idx, verifying the DOM value against what was typed and retrying if it didn't stick. Entering the same procedure code on multiple rows (e.g. one code across several teeth) triggers a duplicate-code warning banner that can re-render the form and silently reset earlier rows' Tooth values — callers should re-run this for every row after the whole form is filled, not just once at type-time.""" tooth = str(tooth).strip() actual = None for attempt in range(3): tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']") if idx >= len(tooth_inputs): return None tooth_inp = tooth_inputs[idx] self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_inp) self.driver.execute_script("arguments[0].focus();", tooth_inp) time.sleep(0.2) tooth_inp.send_keys(Keys.CONTROL + "a") tooth_inp.send_keys(Keys.DELETE) tooth_inp.send_keys(tooth) time.sleep(0.3) actual = (tooth_inp.get_attribute("value") or "").strip() if actual == tooth: print(f"[TuftsSCO PreAuth step4] tooth[{idx}]{context}: confirmed '{tooth}'") return actual print(f"[TuftsSCO PreAuth step4] tooth[{idx}]{context}: attempt {attempt + 1} — " f"expected '{tooth}', field shows {actual!r}, retrying") print(f"[TuftsSCO PreAuth step4] WARNING: tooth[{idx}]{context} did not stick — " f"expected '{tooth}', field shows {actual!r} after 3 attempts") return actual def _fill_text_input(self, inp, value, label="field"): try: inp.click() inp.send_keys(Keys.CONTROL + "a") inp.send_keys(Keys.DELETE) inp.send_keys(str(value)) time.sleep(0.1) print(f"[TuftsSCO PreAuth step4] {label}: typed '{value}'") except Exception as e: print(f"[TuftsSCO PreAuth step4] Warning: could not fill {label}: {e}") def step4_fill_preauth_form(self): """Fill all procedure line fields. Preauth form has no date field — skip it.""" try: active_lines = [ln for ln in self.serviceLines if str(ln.get("procedureCode") or "").strip()] print(f"[TuftsSCO PreAuth step4] {len(active_lines)} service line(s)") for idx, line in enumerate(active_lines): code = str(line.get("procedureCode") or "").strip().upper() tooth = str(line.get("toothNumber") or line.get("tooth") or "").strip() arch = str(line.get("arch") or "").strip() quad = str(line.get("quad") or line.get("quadrant") or "").strip() surface = str(line.get("toothSurface") or line.get("surface") or "").strip().upper() billed = str(line.get("totalBilled") or line.get("billedAmount") or line.get("fee") or "").strip() billed = billed.replace("$", "").strip() print(f"[TuftsSCO PreAuth step4] Line {idx}: code={code} tooth={tooth!r} arch={arch!r} " f"quad={quad!r} surface={surface!r} billed={billed!r}") if idx > 0: try: add_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, "//button[.//span[contains(text(),'Add a procedure')]] | " "//button[contains(normalize-space(text()),'Add a procedure')] | " "//*[contains(text(),'Add a procedure') and @role='button']" )) ) self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn) add_btn.click() print(f"[TuftsSCO PreAuth step4] Clicked 'Add a procedure' for line {idx}") time.sleep(1) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not click 'Add a procedure': {e}") if code: try: proc_inputs = self.driver.find_elements(By.XPATH, "//input[contains(@id,'procedureCode') and contains(@id,'-input')]" ) proc_inp = proc_inputs[idx] if idx < len(proc_inputs) else proc_inputs[-1] self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_inp) self._fill_text_input(proc_inp, code, f"procedureCode[{idx}]") time.sleep(1.5) # wait for warning banner layout shift to settle except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill procedure code: {e}") if tooth: try: self._fill_tooth_field(idx, tooth) time.sleep(0.1) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill tooth: {e}") if arch: try: arch_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Arch']") if idx < len(arch_inputs): self._fill_combobox(arch_inputs[idx], arch, f"arch[{idx}]") time.sleep(0.3) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill arch: {e}") if quad: try: quad_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Quad']") if idx < len(quad_inputs): self._fill_combobox(quad_inputs[idx], quad, f"quad[{idx}]") time.sleep(0.3) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill quad: {e}") if surface: try: surface_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Surface']") if idx < len(surface_inputs): surf_inp = surface_inputs[idx] surf_inp.click() surf_inp.send_keys(Keys.CONTROL + "a") surf_inp.send_keys(Keys.DELETE) surf_inp.send_keys(surface) time.sleep(0.3) surf_inp.send_keys(Keys.ESCAPE) print(f"[TuftsSCO PreAuth step4] surface[{idx}]: typed '{surface}'") time.sleep(0.2) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill surface: {e}") if billed: try: billed_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Enter billed amount']" ) if idx < len(billed_inputs): self._fill_text_input(billed_inputs[idx], billed, f"billedAmount[{idx}]") time.sleep(0.2) except Exception as e: print(f"[TuftsSCO PreAuth step4] Could not fill billed amount: {e}") # Adding a row with a procedure code already used by an earlier row triggers a # duplicate-code warning banner that re-renders the form — this can silently reset # a previously-confirmed Tooth value on an earlier row. Re-check every row now that # no more rows will be added (no more duplicate-code banners can fire after this). print("[TuftsSCO PreAuth step4] Final tooth verification pass...") for idx, line in enumerate(active_lines): tooth = str(line.get("toothNumber") or line.get("tooth") or "").strip() if not tooth: continue tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']") if idx >= len(tooth_inputs): continue actual = (tooth_inputs[idx].get_attribute("value") or "").strip() if actual == tooth: continue print(f"[TuftsSCO PreAuth step4] Final check: tooth[{idx}] regressed to {actual!r}, re-filling '{tooth}'") self._fill_tooth_field(idx, tooth, context=" (final pass)") print("[TuftsSCO PreAuth step4] Done") return "SUCCESS" except Exception as e: print(f"[TuftsSCO PreAuth step4] Exception: {e}") return f"ERROR: step4 failed: {e}" # ── Step 5: Attach files ──────────────────────────────────────────────────── def step5_attach_files(self): """For each claimFile with a filePath, click 'Add a file' and upload it.""" if not self.claimFiles: print("[TuftsSCO PreAuth step5] No files to attach") return "SUCCESS" attached = 0 for cf in self.claimFiles: relative_path = cf.get("filePath") or "" if not relative_path: print(f"[TuftsSCO PreAuth step5] Skipping file with no filePath: {cf}") continue abs_path = os.path.normpath(os.path.join(_BACKEND_CWD, relative_path.lstrip("/"))) if not os.path.isfile(abs_path): print(f"[TuftsSCO PreAuth step5] File not found on disk: {abs_path}") continue print(f"[TuftsSCO PreAuth step5] Attaching: {abs_path}") try: # The react-aria-Input file input is always in the DOM (hidden). # Do NOT click "Add a file" — that opens the OS dialog and blocks WebDriver. file_input = WebDriverWait(self.driver, 10).until( EC.presence_of_element_located((By.XPATH, "//input[@type='file' and contains(@class,'react-aria-Input')]")) ) self.driver.execute_script( "arguments[0].style.display='block'; arguments[0].style.visibility='visible';", file_input ) file_input.send_keys(abs_path) # Wait until the filename appears on the page confirming upload completed filename = os.path.basename(abs_path) try: WebDriverWait(self.driver, 15).until( lambda d: filename.lower() in d.page_source.lower() or filename.rsplit("_", 1)[-1].lower() in d.page_source.lower() ) print(f"[TuftsSCO PreAuth step5] Upload confirmed: {filename}") except Exception: time.sleep(3) print(f"[TuftsSCO PreAuth step5] Upload wait timed out — proceeding: {filename}") print(f"[TuftsSCO PreAuth step5] Attached: {filename}") attached += 1 except Exception as e: print(f"[TuftsSCO PreAuth step5] Could not attach {abs_path}: {e}") print(f"[TuftsSCO PreAuth step5] Attached {attached}/{len(self.claimFiles)} file(s)") return "SUCCESS" # ── Step 6: Click "Next step" ────────────────────────────────────────────── def step6_click_next(self): """Click the 'Next step' button and verify the wizard actually advanced to the acknowledgement page — the URL doesn't change (SPA), so a click that's silently ignored (e.g. button still disabled from a validation error) would otherwise look identical to a successful one in the logs.""" try: print(f"[TuftsSCO PreAuth step6] Current URL: {self.driver.current_url}") advanced = False for attempt in range(3): btn = WebDriverWait(self.driver, 15).until( EC.element_to_be_clickable((By.XPATH, "//button[@data-testid='next-step-btn'] | " "//button[@aria-label='Next step']" )) ) disabled = (btn.get_attribute("disabled") is not None) or \ ((btn.get_attribute("aria-disabled") or "").lower() == "true") if disabled: print(f"[TuftsSCO PreAuth step6] 'Next step' is disabled (attempt {attempt + 1}) — " f"a form field may still be invalid/empty; waiting before retry") time.sleep(1.5) continue self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn) time.sleep(0.5) self.driver.execute_script(""" var el = arguments[0]; el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true})); """, btn) print(f"[TuftsSCO PreAuth step6] Clicked 'Next step' (attempt {attempt + 1})") try: WebDriverWait(self.driver, 6).until( EC.presence_of_element_located((By.XPATH, "//label[contains(.,'submitting this')] | " "//*[contains(@aria-label,'submitting this')]" )) ) advanced = True print("[TuftsSCO PreAuth step6] Confirmed wizard advanced to the acknowledgement step") break except TimeoutException: print(f"[TuftsSCO PreAuth step6] Page did not advance after click (attempt {attempt + 1}), retrying") print(f"[TuftsSCO PreAuth step6] URL after Next: {self.driver.current_url}") if not advanced: return "ERROR: step6 failed: 'Next step' click did not advance the wizard after 3 attempts" return "SUCCESS" except Exception as e: print(f"[TuftsSCO PreAuth step6] Exception: {e}") return f"ERROR: step6 failed: {e}" # ── Step 7: Acknowledge + submit ──────────────────────────────────────────── @staticmethod def _is_checkbox_checked(el): try: if el.is_selected(): return True except Exception: pass state = (el.get_attribute("aria-checked") or el.get_attribute("checked") or "").lower() return state in ("true", "checked") def step7_submit_preauth(self): """On the pre-auth summary page, tick the acknowledgement checkbox then submit.""" try: print(f"[TuftsSCO PreAuth step7] Current URL: {self.driver.current_url}") # A bare "//input[@type='checkbox']" would match the FIRST checkbox in the DOM, # which may not be the acknowledgement box (e.g. a row-selection checkbox in the # service-line table). Try specific, label-anchored selectors before falling back. checkbox = None for xpath in [ "//label[contains(.,'submitting this')]//input[@type='checkbox']", "//*[contains(@aria-label,'submitting this')]", "//label[contains(translate(., 'ACKNOWLEDGE', 'acknowledge'),'acknowledge')]//input[@type='checkbox']", "//*[contains(translate(@aria-label, 'ACKNOWLEDGE', 'acknowledge'),'acknowledge')]", "//input[@type='checkbox']", "//*[@role='checkbox']", ]: try: checkbox = WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, xpath)) ) print(f"[TuftsSCO PreAuth step7] Found acknowledgement checkbox via: {xpath}") break except Exception: continue if checkbox is None: return "ERROR: step7 failed: could not find acknowledgement checkbox" for attempt in range(3): if self._is_checkbox_checked(checkbox): break self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", checkbox) time.sleep(0.3) self.driver.execute_script(""" var el = arguments[0]; el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true})); """, checkbox) time.sleep(0.5) if attempt > 0: print(f"[TuftsSCO PreAuth step7] Acknowledgement checkbox retry {attempt + 1}") if self._is_checkbox_checked(checkbox): print("[TuftsSCO PreAuth step7] Confirmed acknowledgement checkbox is checked") else: print("[TuftsSCO PreAuth step7] WARNING: acknowledgement checkbox did not register as checked after 3 attempts") all_btns = self.driver.find_elements(By.XPATH, "//button") print(f"[TuftsSCO PreAuth step7] Buttons: {[b.get_attribute('aria-label') or b.text[:40] for b in all_btns]}") submit_btn = None for xpath in [ "//button[.//span[normalize-space(text())='pre-authorization']]", "//button[.//span[contains(text(),'Submit prior authorization')]] | //button[@aria-label='Submit prior authorization']", "//button[.//span[contains(text(),'Submit pre-authorization')]] | //button[contains(normalize-space(text()),'Submit pre-authorization')]", "//button[.//span[contains(text(),'Submit claim')]] | //button[@aria-label='Submit claim']", "//button[.//span[contains(text(),'Submit')]] | //button[contains(normalize-space(text()),'Submit')]", ]: try: submit_btn = WebDriverWait(self.driver, 5).until( EC.presence_of_element_located((By.XPATH, xpath)) ) print(f"[TuftsSCO PreAuth step7] Found submit button: {submit_btn.get_attribute('aria-label') or submit_btn.text[:40]!r}") break except Exception: continue if submit_btn is None: return "ERROR: step7 failed: could not find submit button" self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_btn) time.sleep(0.3) self.driver.execute_script(""" var el = arguments[0]; el.dispatchEvent(new PointerEvent('pointerover', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerdown', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new PointerEvent('pointerup', {bubbles:true, cancelable:true, composed:true})); el.dispatchEvent(new MouseEvent('click', {bubbles:true, cancelable:true, composed:true})); """, submit_btn) print("[TuftsSCO PreAuth step7] Clicked submit button") time.sleep(2) print(f"[TuftsSCO PreAuth step7] URL after submit: {self.driver.current_url}") return "SUCCESS" except Exception as e: print(f"[TuftsSCO PreAuth step7] Exception: {e}") return f"ERROR: step7 failed: {e}" # ── Step 8: Extract pre-auth number + save confirmation PDF ─────────────── def step8_save_confirmation_pdf(self): """Wait for the confirmation page, extract the pre-auth number, save page as PDF.""" try: WebDriverWait(self.driver, 60).until( lambda d: "thank" in d.page_source.lower() or "submitted pre-authorization" in d.page_source.lower() ) time.sleep(2) print(f"[TuftsSCO PreAuth step8] Confirmation page URL: {self.driver.current_url}") preauth_number = None try: body_text = self.driver.find_element(By.TAG_NAME, "body").text match = re.search(r'submitted pre-authorization\s+(\d{10,})', body_text, re.IGNORECASE) if match: preauth_number = match.group(1) print(f"[TuftsSCO PreAuth step8] Extracted pre-auth number: {preauth_number}") else: match = re.search(r'\b(\d{12,})\b', body_text) if match: preauth_number = match.group(1) print(f"[TuftsSCO PreAuth step8] Extracted pre-auth number (fallback): {preauth_number}") except Exception as e: print(f"[TuftsSCO PreAuth step8] Could not extract pre-auth number: {e}") shared_downloads = os.path.join(_SERVICE_DIR, "downloads") os.makedirs(shared_downloads, exist_ok=True) safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.") safe_preauth = ("_" + preauth_number[:20]) if preauth_number else "" timestamp = time.strftime("%Y%m%d_%H%M%S") pdf_filename = f"tuftssco_preauth_confirmation_{safe_member}{safe_preauth}_{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"[TuftsSCO PreAuth step8] PDF saved: {pdf_path}") except Exception as e: print(f"[TuftsSCO PreAuth step8] PDF capture failed: {e}") return f"ERROR: step8 PDF failed: {e}" return { "status": "success", "pdf_path": pdf_path, "preAuthNumber": preauth_number, } except Exception as e: print(f"[TuftsSCO PreAuth step8] Exception: {e}") return f"ERROR: step8 failed: {e}"