from selenium.common.exceptions import ( TimeoutException, StaleElementReferenceException, ElementClickInterceptedException, ) from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys from selenium.webdriver.support.ui import WebDriverWait, Select from selenium.webdriver.support import expected_conditions as EC import base64 import os import re import time from selenium_MetLife_eligibilityCheckWorker import AutomationMetLifeEligibilityCheck, METLIFE_HOME_URL # Home page card link: Submit Claims SUBMIT_CLAIMS_LINK_XPATH = "//a[@id='document-forms-link']" # Submit Claims page: CLAIMCONNECT_BUTTON_XPATH = "//button[@id='claims-submission-button']" # "Leaving MetLife Website" modal: LEAVING_CONTINUE_BUTTON_XPATH = "//button[@id='confirmation-modal-primary-button']" # DentalXChange dashboard (register.dentalxchange.com/reg/dashboard), Claims card: # Single Claim… — the click bubbles to its link SINGLE_CLAIM_LINK_XPATH = "//span[contains(@class,'quickLinkContent')][span[normalize-space()='Single Claim']]" # DentalXChange "Start New Claim" (Wicket app — ids like id11/id12 change, so match on # placeholder text / labels / names). Dropdowns are Select2 v3. def _select2_container_xpath(placeholder): return ( "//div[contains(@class,'select2-container')]" f"[.//span[contains(@class,'select2-chosen')][normalize-space()='{placeholder}']]" ) BILLING_SELECT_XPATH = _select2_container_xpath("Select Billing Provider") RENDERING_SELECT_XPATH = _select2_container_xpath("Select Rendering Provider") PAYER_SELECT_XPATH = _select2_container_xpath("Type Payer Name") # Open Select2 dropdown (only one is open at a time) SELECT2_DROP_XPATH = "//div[@id='select2-drop' and not(contains(@style,'display: none'))]" SELECT2_RESULT_XPATH = SELECT2_DROP_XPATH + "//li[contains(@class,'select2-result-selectable')]" SELECT2_SEARCH_INPUT_XPATH = SELECT2_DROP_XPATH + "//input[contains(@class,'select2-input')]" MEMBER_ID_INPUT_XPATH = "//*[contains(normalize-space(text()),'Member ID or SSN')]/following::input[@type='text'][1]" LAST_NAME_INPUT_XPATH = "//input[contains(@name,'enterPatientContainer:patient.address.lastName')]" NEXT_BUTTON_XPATH = "//button[@name='buttons:next']" # MetLife payer in the DentalXChange payer list ("Metlife - 65978"; not "Metlife Tricare - 89070") METLIFE_PAYER_SEARCH = "metlife" METLIFE_PAYER_ID = "65978" # "Select Patient" page: one row per covered person, Name cell is a link # (Name | Relationship | Date of Birth | Group/Policy # | Status) PATIENT_ROW_XPATH = "//table//tr[td and .//a]" # "Eligible Patient Review" page: Create Claim inside the action button CREATE_CLAIM_BUTTON_XPATH = "//*[self::button or self::a][normalize-space(.)='Create Claim']" # "Claim Detail" page — Signature on file radios (each click fires a Wicket ajax call) RELEASE_YES_RADIO_ID = "releaseYesButton" ASSIGNMENT_YES_RADIO_ID = "assignmentYesButton" # "Claim Item Detail" page — one panel per procedure line (3 shown by default). # Field names are stable apart from the line index; every field posts a Wicket ajax # call on change/blur, which can re-render the line, so always re-find by name. ITEM_PREFIX = "view:formPanel:claimForm:claimItemListContainer:claimItemList:{i}:claimItemPanel:claimItemForm:" ITEM_DATE_NAME = ITEM_PREFIX + "startDateInput" ITEM_PROC_NAME = ITEM_PREFIX + "procCode" ITEM_FEE_NAME = ITEM_PREFIX + "feeInput" ITEM_TOOTH_NAME = ITEM_PREFIX + "claimToothListViewContainer:claimToothListView:0:tooth" ADD_CLAIM_ITEM_BUTTON_XPATH = "//button[@name='view:formPanel:claimForm:add']" FINISH_BUTTON_XPATH = "//button[@name='buttons:finish']" # The 5 Surfaces dropdowns sit right after the Tooth box; options are # Buccal, Distal, Facial, Incisal, Lingual, Mesial, Occlusal (first letter = code) MAX_SURFACES = 5 # Review page after Finish: (ajax; # id changes). While any ajax call runs, Wicket shows the full-page #ajax-indicator-veil. SUBMIT_CLAIM_BUTTON_XPATH = "//button[normalize-space(.)='Submit Claim']" AJAX_VEIL_ID = "ajax-indicator-veil" # Confirmation page: "Submitted to MetLife" banner + BACK_TO_CLAIM_BUTTON_XPATH = "//button[normalize-space(.)='Back to Claim']" # agent.py serves this folder at /downloads so the backend can fetch the PDF SHARED_DOWNLOADS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "downloads") # True when no Wicket ajax request is in flight (Wicket 1.4/1.5 channel manager) WICKET_IDLE_JS = r""" try { if (typeof Wicket === 'undefined' || !Wicket.channelManager) return true; var ch = Wicket.channelManager.channels || {}; for (var k in ch) { if (ch[k] && ch[k].busy) return false; } return true; } catch (e) { return true; } """ class AutomationMetLifeClaimSubmit(AutomationMetLifeEligibilityCheck): """ MetLife Dental claim submission on dentalprovider.metlife.com. Login (persistent Chrome profile, PingOne + email MFA) is inherited from the eligibility worker so both flows share one session. """ def __init__(self, data): claim = data.get("claim", {}) if isinstance(data, dict) else {} super().__init__({"data": claim}) self.claim = claim self.serviceDate = claim.get("serviceDate", "") self.serviceLines = claim.get("serviceLines", []) or [] self.claimFiles = claim.get("claimFiles", []) or [] self.providerName = claim.get("providerName", "") self.remarks = claim.get("remarks", "") # Claim payload carries the patient name as one field if not (self.firstName or self.lastName): parts = (claim.get("patientName", "") or "").split() if parts: self.firstName = parts[0] self.lastName = " ".join(parts[1:]) def _click(self, xpath, label, timeout=20): """Click the first visible element matching `xpath`.""" WebDriverWait(self.driver, timeout).until( lambda d: any(e.is_displayed() for e in d.find_elements(By.XPATH, xpath)) ) el = next(e for e in self.driver.find_elements(By.XPATH, xpath) if e.is_displayed()) try: el.click() except Exception: # href="javascript:;" / overlay — the React onClick does the navigation self.driver.execute_script("arguments[0].click();", el) print(f"[MetLife Claim] Clicked '{label}'") def _click_and_follow(self, xpath, label, timeout=20): """ Click the first visible element matching `xpath`, then wait for the page to change. If the click opened a new tab, switch to it. """ start_url = self.driver.current_url start_handles = set(self.driver.window_handles) self._click(xpath, label, timeout) # The click may route inside the SPA or open a new tab try: WebDriverWait(self.driver, 15).until( lambda d: d.current_url != start_url or len(d.window_handles) > len(start_handles) ) except TimeoutException: print(f"[MetLife Claim] No navigation after '{label}'; still on {self.driver.current_url}") new_handles = [h for h in self.driver.window_handles if h not in start_handles] if new_handles: self.driver.switch_to.window(new_handles[-1]) print(f"[MetLife Claim] '{label}' opened a new tab — switched to it") time.sleep(2) print(f"[MetLife Claim] After '{label}': {self.driver.current_url}") def step1_open_submit_claims(self): """From the home page, click "Submit Claims".""" try: if "/home" not in self.driver.current_url: self.driver.get(METLIFE_HOME_URL) self._click_and_follow(SUBMIT_CLAIMS_LINK_XPATH, "Submit Claims") return "Success" except TimeoutException: return "ERROR: 'Submit Claims' link not found on MetLife home page" except Exception as e: return f"ERROR: Could not open Submit Claims: {e}" def step2_open_claimconnect(self): """ On the Submit Claims page, click "ClaimConnect", then "Continue" on the "Leaving MetLife Website" modal. """ try: self._click(CLAIMCONNECT_BUTTON_XPATH, "ClaimConnect") except TimeoutException: return "ERROR: 'ClaimConnect' button not found on MetLife Submit Claims page" try: self._click_and_follow(LEAVING_CONTINUE_BUTTON_XPATH, "Continue (Leaving MetLife)", timeout=10) return "Success" except TimeoutException: return "ERROR: 'Leaving MetLife Website' Continue button not found" except Exception as e: return f"ERROR: Could not open ClaimConnect: {e}" def step3_open_single_claim(self): """On the DentalXChange dashboard (new tab), click "Single Claim".""" try: self._click_and_follow(SINGLE_CLAIM_LINK_XPATH, "Single Claim", timeout=30) return "Success" except TimeoutException: return f"ERROR: 'Single Claim' link not found on DentalXChange dashboard ({self.driver.current_url})" except Exception as e: return f"ERROR: Could not open Single Claim: {e}" # ── DentalXChange Select2 helpers ───────────────────────────────────────── def _select2_open(self, container_xpath, label, timeout=20): container = WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((By.XPATH, container_xpath)) ) choice = container.find_element(By.XPATH, ".//a[contains(@class,'select2-choice')]") try: choice.click() except Exception: self.driver.execute_script( "arguments[0].dispatchEvent(new MouseEvent('mousedown', {bubbles: true}));", choice ) WebDriverWait(self.driver, 10).until(EC.visibility_of_element_located((By.XPATH, SELECT2_DROP_XPATH))) print(f"[MetLife Claim] Opened '{label}' dropdown") def _select2_pick(self, label, match=None, timeout=15): """Click the first open-dropdown result (or the first whose text contains `match`).""" def find_result(d): for li in d.find_elements(By.XPATH, SELECT2_RESULT_XPATH): if li.is_displayed() and (match is None or match.lower() in li.text.lower()): return li return False li = WebDriverWait(self.driver, timeout).until(find_result) text = li.text.strip() li.click() print(f"[MetLife Claim] {label}: picked '{text}'") # Wicket re-renders parts of the form after a selection time.sleep(1.5) return text def _select2_choose(self, container_xpath, label, search=None, match=None): self._select2_open(container_xpath, label) if search: box = WebDriverWait(self.driver, 10).until( EC.visibility_of_element_located((By.XPATH, SELECT2_SEARCH_INPUT_XPATH)) ) box.send_keys(search) return self._select2_pick(label, match) def _fill_text(self, xpath, value, label): el = WebDriverWait(self.driver, 15).until(EC.visibility_of_element_located((By.XPATH, xpath))) el.clear() el.send_keys(value) el.send_keys(Keys.TAB) print(f"[MetLife Claim] Filled {label}") time.sleep(0.5) def _click_next(self, page, xpath=NEXT_BUTTON_XPATH, label="Next"): """ Click a wizard button ("Next" by default) and wait for the next page. Returns "Success", or "ERROR: …" with the page's validation messages. """ # The last field's ajax call (e.g. Fee) may still be in flight and overlay the page self._wait_wicket_idle() try: self._wait_veil_gone(timeout=30) except TimeoutException: print(f"[MetLife Claim] Ajax veil still showing before '{label}'") next_btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, xpath)) ) self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", next_btn) time.sleep(0.5) try: next_btn.click() except ElementClickInterceptedException: # Something overlays the button (e.g. date picker / page footer) — the # native click never landed, so a script click submits exactly once print(f"[MetLife Claim] '{label}' click intercepted — clicking via script") self.driver.execute_script("arguments[0].click();", next_btn) print(f"[MetLife Claim] Clicked '{label}' on {page}") try: WebDriverWait(self.driver, 30).until(EC.staleness_of(next_btn)) except TimeoutException: # Still on the page — surface any validation message (e.g. "Member ID is required.") errors = [e.text.strip() for e in self.driver.find_elements( By.XPATH, "//span[contains(@class,'feedbacklabel') and contains(@class,'ERROR')]" ) if e.text.strip()] if errors: return f"ERROR: DentalXChange {page}: {'; '.join(errors)}" time.sleep(2) print(f"[MetLife Claim] After {page}: {self.driver.current_url}") return "Success" def step4_start_new_claim(self): """ Start New Claim page: Billing + Rendering provider (first entry), Member ID, patient last name, payer "Metlife - 65978", then Next. """ member_id = re.sub(r"[\s-]", "", self.memberId or "") if not member_id: return "ERROR: Member ID is required for MetLife claim" try: self._select2_choose(BILLING_SELECT_XPATH, "Billing provider") self._select2_choose(RENDERING_SELECT_XPATH, "Rendering provider") self._fill_text(MEMBER_ID_INPUT_XPATH, member_id, "Member ID") if self.lastName: self._fill_text(LAST_NAME_INPUT_XPATH, self.lastName, "Last Name") self._select2_choose(PAYER_SELECT_XPATH, "Payer", search=METLIFE_PAYER_SEARCH, match=METLIFE_PAYER_ID) return self._click_next("Start New Claim") except TimeoutException as e: return f"ERROR: DentalXChange Start New Claim — element not found ({e.msg or 'timeout'})" except Exception as e: return f"ERROR: DentalXChange Start New Claim failed: {e}" @staticmethod def _mmddyyyy(value): """YYYY-MM-DD[...] or MM/DD/YYYY as MM/DD/YYYY ("" if unparseable).""" v = str(value or "").strip() m = re.match(r"(\d{4})-(\d{1,2})-(\d{1,2})", v) if m: return f"{m.group(2).zfill(2)}/{m.group(3).zfill(2)}/{m.group(1)}" m = re.match(r"(\d{1,2})/(\d{1,2})/(\d{4})", v) if m: return f"{m.group(1).zfill(2)}/{m.group(2).zfill(2)}/{m.group(3)}" return "" def _expected_dob(self): return self._mmddyyyy(self.dateOfBirth) def step5_select_patient(self): """ "Select Patient" page: click the covered person matching the claim's patient (DOB first, then name). Then "Create Claim" on "Eligible Patient Review". """ try: # Either the patient list, or straight to the review page (single match) WebDriverWait(self.driver, 30).until( lambda d: d.find_elements(By.XPATH, PATIENT_ROW_XPATH) or d.find_elements(By.XPATH, CREATE_CLAIM_BUTTON_XPATH) ) rows = [r for r in self.driver.find_elements(By.XPATH, PATIENT_ROW_XPATH) if r.is_displayed()] if rows and not self.driver.find_elements(By.XPATH, CREATE_CLAIM_BUTTON_XPATH): expected_dob = self._expected_dob() first = (self.firstName or "").strip().upper() last = (self.lastName or "").strip().upper() parsed = [] for r in rows: cells = [c.text.strip() for c in r.find_elements(By.TAG_NAME, "td")] name = cells[0].upper() if cells else "" dob = next((c for c in cells if re.fullmatch(r"\d{2}/\d{2}/\d{4}", c)), "") parsed.append((r, name, dob)) print(f"[MetLife Claim step5] Row: {cells}") def name_match(name): return (not first or first in name) and (not last or last in name) matches = [p for p in parsed if expected_dob and p[2] == expected_dob] if len(matches) > 1: matches = [p for p in matches if name_match(p[1])] or matches if not matches: matches = [p for p in parsed if name_match(p[1])] if not matches: listed = ", ".join(f"{p[1]} ({p[2]})" for p in parsed) return (f"ERROR: Patient {first} {last} ({expected_dob}) not listed by MetLife. " f"Covered: {listed}") row, name, dob = matches[0] link = row.find_element(By.XPATH, ".//a") try: link.click() except Exception: self.driver.execute_script("arguments[0].click();", link) print(f"[MetLife Claim step5] Selected patient {name} ({dob})") create_btn = WebDriverWait(self.driver, 30).until( EC.element_to_be_clickable((By.XPATH, CREATE_CLAIM_BUTTON_XPATH)) ) create_btn.click() print("[MetLife Claim step5] Clicked 'Create Claim'") try: WebDriverWait(self.driver, 30).until(EC.staleness_of(create_btn)) except TimeoutException: print("[MetLife Claim step5] Page did not change after 'Create Claim'") time.sleep(2) print(f"[MetLife Claim step5] After Create Claim: {self.driver.current_url}") return "Success" except TimeoutException: return f"ERROR: DentalXChange Select Patient / Create Claim not found ({self.driver.current_url})" except Exception as e: return f"ERROR: DentalXChange Select Patient failed: {e}" def _check_radio(self, radio_id, label): """Select a radio by id and confirm it stuck (Wicket ajax may re-render it).""" for attempt in range(3): radio = WebDriverWait(self.driver, 20).until( EC.presence_of_element_located((By.ID, radio_id)) ) if radio.is_selected(): print(f"[MetLife Claim] {label}: Yes") return True try: radio.click() except Exception: self.driver.execute_script("arguments[0].click();", radio) time.sleep(1.5) # let the ajax round-trip finish radio = self.driver.find_element(By.ID, radio_id) if radio.is_selected(): print(f"[MetLife Claim] {label}: Yes") return True return False def step6_claim_detail(self): """ Claim Detail page: Signature on file — Release of information Yes, Assignment of payment Yes — then Next. """ try: if not self._check_radio(RELEASE_YES_RADIO_ID, "Release of information by provider"): return "ERROR: Could not select 'Yes' for Release of information by provider" if not self._check_radio(ASSIGNMENT_YES_RADIO_ID, "Assignment of payment to provider"): return "ERROR: Could not select 'Yes' for Assignment of payment to provider" return self._click_next("Claim Detail") except TimeoutException: return f"ERROR: DentalXChange Claim Detail page not found ({self.driver.current_url})" except Exception as e: return f"ERROR: DentalXChange Claim Detail failed: {e}" # ── Claim Item Detail ───────────────────────────────────────────────────── def _by_name(self, name, timeout=15): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located((By.NAME, name)) ) def _wait_wicket_idle(self, timeout=15): """Wait for in-flight Wicket ajax calls (they re-render the line) to finish.""" time.sleep(0.3) try: WebDriverWait(self.driver, timeout).until(lambda d: d.execute_script(WICKET_IDLE_JS)) except TimeoutException: print("[MetLife Claim step7] Wicket ajax still busy — continuing") time.sleep(0.3) @staticmethod def _same_value(actual, expected): """Field value matches what was typed (fees compared as numbers, "$90.00" == "90").""" try: return abs(float(re.sub(r"[$,\s]", "", actual)) - float(expected)) < 0.005 except ValueError: return re.sub(r"[^A-Z0-9]", "", actual.upper()) == re.sub(r"[^A-Z0-9]", "", expected.upper()) def _set_item_field(self, name, value, label): """ Type into a line field and tab out so its Wicket onchange/onblur fires. The previous field's ajax call can re-render the line mid-typing, so wait for it, retry on a stale element, and verify the value stuck. """ for attempt in range(1, 4): self._wait_wicket_idle() try: el = self._by_name(name) self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", el) try: el.click() el.send_keys(Keys.CONTROL, "a") el.send_keys(Keys.BACKSPACE) el.send_keys(value) el.send_keys(Keys.TAB) except StaleElementReferenceException: raise except Exception as type_err: # Not interactable — set it directly and fire the inline onchange/onblur print(f"[MetLife Claim step7] {label}: typing failed ({type_err}); setting via JS") self.driver.execute_script( "arguments[0].value = arguments[1];" "arguments[0].dispatchEvent(new Event('change', {bubbles: true}));" "arguments[0].dispatchEvent(new Event('blur'));" "if (arguments[0].onchange) arguments[0].onchange();" "if (arguments[0].onblur) arguments[0].onblur();", el, value, ) self._wait_wicket_idle() actual = (self._by_name(name).get_attribute("value") or "").strip() if self._same_value(actual, value): print(f"[MetLife Claim step7] {label} = {value}") return print(f"[MetLife Claim step7] {label}: expected {value!r}, field shows {actual!r} (attempt {attempt})") except StaleElementReferenceException: print(f"[MetLife Claim step7] {label}: field re-rendered (attempt {attempt}) — retrying") raise RuntimeError(f"{label}: could not set {value!r}") def _set_item_surfaces(self, i, surfaces): tooth = self._by_name(ITEM_TOOTH_NAME.format(i=i)) selects = tooth.find_elements(By.XPATH, f"./following::select[position()<={MAX_SURFACES}]") for n, code in enumerate(surfaces[:MAX_SURFACES]): if n >= len(selects): return f"ERROR: Line {i + 1}: only {len(selects)} surface dropdowns found" sel = Select(selects[n]) option = next((o for o in sel.options if (o.get_attribute("value") or "").upper() == code or (o.text or "").strip().upper().startswith(code)), None) if option is None: return f"ERROR: Line {i + 1}: surface '{code}' not in dropdown" sel.select_by_visible_text(option.text) print(f"[MetLife Claim step7] Line {i + 1} surface{n + 1} = {option.text.strip()}") time.sleep(0.5) # Re-find after a possible re-render tooth = self._by_name(ITEM_TOOTH_NAME.format(i=i)) selects = tooth.find_elements(By.XPATH, f"./following::select[position()<={MAX_SURFACES}]") return None def _ensure_item(self, i): """Click "Add Claim Item" until line index `i` exists.""" for _ in range(3): if self.driver.find_elements(By.NAME, ITEM_DATE_NAME.format(i=i)): return True btn = WebDriverWait(self.driver, 10).until( EC.element_to_be_clickable((By.XPATH, ADD_CLAIM_ITEM_BUTTON_XPATH)) ) btn.click() print(f"[MetLife Claim step7] Clicked 'Add Claim Item' for line {i + 1}") try: self._by_name(ITEM_DATE_NAME.format(i=i), timeout=10) return True except TimeoutException: continue return False def step7_fill_claim_items(self): """Claim Item Detail page: one line per procedure code, then Finish.""" lines = [l for l in self.serviceLines if str(l.get("procedureCode") or "").strip()] if not lines: return "ERROR: No procedure codes on the claim" try: self._by_name(ITEM_DATE_NAME.format(i=0), timeout=30) for i, line in enumerate(lines): code = str(line.get("procedureCode") or "").strip().upper() date = self._mmddyyyy(line.get("procedureDate")) or self._mmddyyyy(self.serviceDate) # Tooth box takes 2 characters (1-32, A-T); drop "#", spaces, etc. tooth = re.sub(r"[^A-Za-z0-9]", "", str(line.get("toothNumber") or "")).upper() surfaces = [c for c in str(line.get("toothSurface") or "").upper() if c.isalpha()] billed = line.get("totalBilled") or line.get("billedAmount") or line.get("fee") try: fee = f"{float(billed):.2f}" if billed not in (None, "") else "" except (TypeError, ValueError): fee = "" print(f"[MetLife Claim step7] Line {i + 1}: code={code} date={date} " f"tooth={tooth!r} surf={surfaces} fee={fee}") if not date: return f"ERROR: Line {i + 1} ({code}): no service date" if not self._ensure_item(i): return f"ERROR: Could not add claim item line {i + 1}" self._set_item_field(ITEM_DATE_NAME.format(i=i), date, f"Line {i + 1} date") self._set_item_field(ITEM_PROC_NAME.format(i=i), code, f"Line {i + 1} procedure") if tooth: self._set_item_field(ITEM_TOOTH_NAME.format(i=i), tooth, f"Line {i + 1} tooth") if surfaces: err = self._set_item_surfaces(i, surfaces) if err: return err if fee: self._set_item_field(ITEM_FEE_NAME.format(i=i), fee, f"Line {i + 1} fee") return self._click_next("Claim Item Detail", FINISH_BUTTON_XPATH, "Finish") except TimeoutException: return f"ERROR: DentalXChange Claim Item Detail field not found ({self.driver.current_url})" except Exception as e: return f"ERROR: DentalXChange Claim Item Detail failed: {e}" def _wait_veil_gone(self, timeout=60): """Wait until Wicket's full-page ajax veil is hidden (it blocks clicks).""" def gone(d): veils = d.find_elements(By.ID, AJAX_VEIL_ID) return not veils or not veils[0].is_displayed() WebDriverWait(self.driver, timeout).until(gone) def step8_submit_claim(self): """Review page: click "Submit Claim" once and wait for the portal's response.""" try: self._wait_wicket_idle() self._wait_veil_gone() btn = WebDriverWait(self.driver, 30).until( EC.element_to_be_clickable((By.XPATH, SUBMIT_CLAIM_BUTTON_XPATH)) ) self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", btn) time.sleep(0.5) try: btn.click() except ElementClickInterceptedException: # The native click never landed, so a script click submits exactly once print("[MetLife Claim step8] 'Submit Claim' click intercepted — clicking via script") self.driver.execute_script("arguments[0].click();", btn) print("[MetLife Claim step8] Clicked 'Submit Claim'") # Submission is an ajax call: wait for the veil to clear and the button to go away time.sleep(1) self._wait_veil_gone(timeout=90) self._wait_wicket_idle(timeout=60) try: WebDriverWait(self.driver, 30).until( lambda d: not any(b.is_displayed() for b in d.find_elements(By.XPATH, SUBMIT_CLAIM_BUTTON_XPATH)) ) except TimeoutException: errors = [e.text.strip() for e in self.driver.find_elements( By.XPATH, "//span[contains(@class,'feedbacklabel') and contains(@class,'ERROR')]" " | //*[contains(@class,'feedbackPanelERROR') or contains(@class,'alert-error')]" ) if e.text.strip()] if errors: return f"ERROR: DentalXChange Submit Claim: {'; '.join(errors)}" return "ERROR: 'Submit Claim' did not complete — check the browser window before resubmitting" time.sleep(2) print(f"[MetLife Claim step8] After Submit Claim: {self.driver.current_url}") return "Success" except TimeoutException: return f"ERROR: DentalXChange review page — 'Submit Claim' not found ({self.driver.current_url})" except Exception as e: return f"ERROR: DentalXChange Submit Claim failed: {e}" def _extract_claim_id(self): """Claim ID from the claim page ("Claim ID 736537870"), else from the URL path.""" try: text = self.driver.execute_script("return document.body.innerText;") or "" except Exception: text = "" m = re.search(r"Claim\s*ID\s*[:#]?\s*(\d{6,})", text, re.IGNORECASE) if m: return m.group(1) # Claim pages live at …/claim/?- m = re.search(r"/(\d{6,})(?:\?|$)", self.driver.current_url) return m.group(1) if m else None def _save_page_pdf(self, claim_id): os.makedirs(SHARED_DOWNLOADS_DIR, exist_ok=True) safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.") safe_claim = f"_{claim_id}" if claim_id else "" timestamp = time.strftime("%Y%m%d_%H%M%S") pdf_path = os.path.join(SHARED_DOWNLOADS_DIR, f"metlife_claim_{safe_member}{safe_claim}_{timestamp}.pdf") 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, }) with open(pdf_path, "wb") as f: f.write(base64.b64decode(pdf_data["data"])) print(f"[MetLife Claim step9] PDF saved: {pdf_path}") return pdf_path def step9_save_claim(self): """ Confirmation page ("Submitted to MetLife"): click "Back to Claim", read the Claim ID and save the claim page as a PDF. Returns {"claimNumber", "pdf_path"} or "ERROR: …". """ try: self._wait_veil_gone(timeout=30) try: WebDriverWait(self.driver, 30).until( lambda d: "Submitted to MetLife" in (d.execute_script("return document.body.innerText;") or "") ) print("[MetLife Claim step9] 'Submitted to MetLife' confirmed") except TimeoutException: print("[MetLife Claim step9] 'Submitted to MetLife' banner not seen — continuing") old_page = self.driver.find_element(By.TAG_NAME, "body") self._click(BACK_TO_CLAIM_BUTTON_XPATH, "Back to Claim", timeout=30) # Plain navigation (window.location.href) try: WebDriverWait(self.driver, 30).until(EC.staleness_of(old_page)) except TimeoutException: print("[MetLife Claim step9] Page did not reload after 'Back to Claim'") WebDriverWait(self.driver, 30).until( lambda d: d.execute_script("return document.readyState") == "complete" ) self._wait_wicket_idle() time.sleep(1.5) print(f"[MetLife Claim step9] Claim page: {self.driver.current_url}") claim_id = self._extract_claim_id() print(f"[MetLife Claim step9] Claim ID: {claim_id}") try: pdf_path = self._save_page_pdf(claim_id) except Exception as e: # The claim is already submitted — don't fail the job over the PDF print(f"[MetLife Claim step9] PDF capture failed (non-fatal): {e}") pdf_path = None return {"claimNumber": claim_id, "pdf_path": pdf_path} except TimeoutException: return f"ERROR: 'Back to Claim' not found after submit ({self.driver.current_url})" except Exception as e: return f"ERROR: Reading submitted claim failed: {e}"