Open the toothSurfaces MUI listbox (retrying if it doesn't appear), check each surface letter by option text via JS and verify it ends up selected, then close the popover with Escape or a backdrop click so its overlay no longer blocks the fee field. Waits for the tooth-number listbox to close first so its options aren't misread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
890 lines
42 KiB
Python
890 lines
42 KiB
Python
from selenium.common.exceptions import TimeoutException
|
|
from selenium.webdriver.common.by import By
|
|
from selenium.webdriver.common.keys import Keys
|
|
from selenium.webdriver.support.ui import WebDriverWait
|
|
from selenium.webdriver.support import expected_conditions as EC
|
|
import time
|
|
import os
|
|
import re
|
|
import base64
|
|
|
|
from deltains_browser_manager import get_browser_manager
|
|
|
|
LOGIN_URL = "https://www.deltadentalins.com/ciam/login?TARGET=%2Fprovider-tools%2Fv2"
|
|
PROVIDER_TOOLS_URL = "https://www.deltadentalins.com/provider-tools/v2"
|
|
|
|
# Absolute path to the Backend app root (two levels up from this file's SeleniumService dir)
|
|
_SERVICE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
_BACKEND_CWD = os.path.normpath(os.path.join(_SERVICE_DIR, "..", "Backend"))
|
|
|
|
|
|
class AutomationDeltaInsClaimSubmit:
|
|
def __init__(self, data):
|
|
self.headless = False
|
|
self.driver = None
|
|
|
|
raw = data if isinstance(data, dict) else {}
|
|
claim = raw.get("claim", {}) or {}
|
|
|
|
patient_name = (claim.get("patientName") or "").strip()
|
|
parts = patient_name.split()
|
|
first_name = parts[0] if parts else ""
|
|
last_name = " ".join(parts[1:]) if len(parts) > 1 else ""
|
|
|
|
self.deltains_username = claim.get("deltainsUsername", "") or raw.get("deltainsUsername", "")
|
|
self.deltains_password = claim.get("deltainsPassword", "") or raw.get("deltainsPassword", "")
|
|
self.memberId = claim.get("memberId", "")
|
|
self.dateOfBirth = claim.get("dateOfBirth", "")
|
|
self.serviceDate = claim.get("serviceDate", "")
|
|
self.firstName = claim.get("firstName", "") or first_name
|
|
self.lastName = claim.get("lastName", "") or last_name
|
|
self.serviceLines = claim.get("serviceLines", []) or []
|
|
self.claimFiles = claim.get("claimFiles", []) or []
|
|
self.providerName = claim.get("providerName", "") or raw.get("providerName", "")
|
|
|
|
self.download_dir = get_browser_manager().download_dir
|
|
os.makedirs(self.download_dir, exist_ok=True)
|
|
|
|
print(f"[Deltains Claim] Init — member={self.memberId}, "
|
|
f"patient={self.firstName} {self.lastName}, "
|
|
f"lines={len(self.serviceLines)}")
|
|
|
|
def config_driver(self):
|
|
self.driver = get_browser_manager().get_driver(self.headless)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Login — copied from AutomationDeltaInsEligibilityCheck.login() #
|
|
# (Okta-based, already proven working for the eligibility flow) #
|
|
# ------------------------------------------------------------------ #
|
|
def _dismiss_cookie_banner(self):
|
|
try:
|
|
accept_btn = WebDriverWait(self.driver, 5).until(
|
|
EC.element_to_be_clickable((By.ID, "onetrust-accept-btn-handler"))
|
|
)
|
|
accept_btn.click()
|
|
print("[Deltains Claim login] Dismissed cookie consent banner")
|
|
time.sleep(1)
|
|
except TimeoutException:
|
|
print("[Deltains Claim login] No cookie consent banner found")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim login] Error dismissing cookie banner: {e}")
|
|
|
|
def _force_logout(self):
|
|
try:
|
|
print("[Deltains Claim login] Forcing logout due to credential change...")
|
|
browser_manager = get_browser_manager()
|
|
try:
|
|
self.driver.delete_all_cookies()
|
|
print("[Deltains Claim login] Cleared all cookies")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim login] Error clearing cookies: {e}")
|
|
browser_manager.clear_credentials_hash()
|
|
print("[Deltains Claim login] Logout complete")
|
|
return True
|
|
except Exception as e:
|
|
print(f"[Deltains Claim login] Error during forced logout: {e}")
|
|
return False
|
|
|
|
def login(self, url):
|
|
"""
|
|
Multi-step login flow for DeltaIns (Okta-based) — identical to the
|
|
eligibility worker's login():
|
|
1. Enter username (name='identifier') -> click Next
|
|
2. Enter password (type='password') -> click Submit
|
|
3. Handle MFA: click 'Send me an email' -> wait for OTP
|
|
Returns: ALREADY_LOGGED_IN, SUCCESS, OTP_REQUIRED, or ERROR:...
|
|
"""
|
|
wait = WebDriverWait(self.driver, 30)
|
|
browser_manager = get_browser_manager()
|
|
|
|
try:
|
|
if self.deltains_username and browser_manager.credentials_changed(self.deltains_username):
|
|
self._force_logout()
|
|
self.driver.get(url)
|
|
time.sleep(3)
|
|
|
|
try:
|
|
current_url = self.driver.current_url
|
|
print(f"[Deltains Claim login] Current URL: {current_url}")
|
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
|
print("[Deltains Claim login] Already on provider tools page - logged in")
|
|
return "ALREADY_LOGGED_IN"
|
|
except Exception as e:
|
|
print(f"[Deltains Claim login] Error checking current state: {e}")
|
|
|
|
print("[Deltains Claim login] Trying provider-tools URL to check session...")
|
|
self.driver.get(PROVIDER_TOOLS_URL)
|
|
time.sleep(5)
|
|
|
|
current_url = self.driver.current_url
|
|
print(f"[Deltains Claim login] After provider-tools nav URL: {current_url}")
|
|
|
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
|
print("[Deltains Claim login] Session still valid - already logged in")
|
|
return "ALREADY_LOGGED_IN"
|
|
|
|
print("[Deltains Claim login] Session not valid, navigating to login page...")
|
|
self.driver.get(url)
|
|
time.sleep(3)
|
|
|
|
current_url = self.driver.current_url
|
|
print(f"[Deltains Claim login] After login nav URL: {current_url}")
|
|
|
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
|
print("[Deltains Claim login] Already logged in - on provider tools")
|
|
return "ALREADY_LOGGED_IN"
|
|
|
|
self._dismiss_cookie_banner()
|
|
|
|
print("[Deltains Claim login] Looking for username field...")
|
|
username_entered = False
|
|
for sel in [
|
|
(By.NAME, "identifier"),
|
|
(By.ID, "okta-signin-username"),
|
|
(By.XPATH, "//input[@type='text' and @autocomplete='username']"),
|
|
(By.XPATH, "//input[@type='text']"),
|
|
]:
|
|
try:
|
|
field = WebDriverWait(self.driver, 8).until(EC.presence_of_element_located(sel))
|
|
if field.is_displayed():
|
|
field.clear()
|
|
field.send_keys(self.deltains_username)
|
|
username_entered = True
|
|
print(f"[Deltains Claim login] Username entered via {sel}")
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if not username_entered:
|
|
return "ERROR: Could not find username field"
|
|
|
|
time.sleep(1)
|
|
for sel in [
|
|
(By.XPATH, "//input[@type='submit' and @value='Next']"),
|
|
(By.XPATH, "//input[@type='submit']"),
|
|
(By.XPATH, "//button[@type='submit']"),
|
|
]:
|
|
try:
|
|
btn = self.driver.find_element(*sel)
|
|
if btn.is_displayed():
|
|
btn.click()
|
|
print(f"[Deltains Claim login] Clicked Next via {sel}")
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
time.sleep(4)
|
|
|
|
current_url = self.driver.current_url
|
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
|
return "ALREADY_LOGGED_IN"
|
|
|
|
print("[Deltains Claim login] Looking for password field...")
|
|
pw_entered = False
|
|
for sel in [
|
|
(By.XPATH, "//input[@type='password']"),
|
|
(By.ID, "okta-signin-password"),
|
|
(By.NAME, "password"),
|
|
]:
|
|
try:
|
|
field = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(sel))
|
|
if field.is_displayed():
|
|
field.clear()
|
|
field.send_keys(self.deltains_password)
|
|
pw_entered = True
|
|
print(f"[Deltains Claim login] Password entered via {sel}")
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if not pw_entered:
|
|
current_url = self.driver.current_url
|
|
if "provider-tools" in current_url and "login" not in current_url.lower():
|
|
return "ALREADY_LOGGED_IN"
|
|
return "ERROR: Password field not found"
|
|
|
|
time.sleep(1)
|
|
for sel in [
|
|
(By.ID, "okta-signin-submit"),
|
|
(By.XPATH, "//input[@type='submit']"),
|
|
(By.XPATH, "//button[@type='submit']"),
|
|
]:
|
|
try:
|
|
btn = self.driver.find_element(*sel)
|
|
if btn.is_displayed():
|
|
btn.click()
|
|
print(f"[Deltains Claim login] Clicked Sign In via {sel}")
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if self.deltains_username:
|
|
browser_manager.save_credentials_hash(self.deltains_username)
|
|
|
|
time.sleep(6)
|
|
|
|
current_url = self.driver.current_url
|
|
print(f"[Deltains Claim login] After password submit URL: {current_url}")
|
|
|
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
|
print("[Deltains Claim login] Login successful - on provider tools")
|
|
return "SUCCESS"
|
|
|
|
print("[Deltains Claim login] Handling MFA...")
|
|
|
|
try:
|
|
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
if "security method" in body_text.lower() or "select from the following" in body_text.lower():
|
|
print("[Deltains Claim login] MFA method selection page detected")
|
|
email_select = None
|
|
for sel in [
|
|
(By.CSS_SELECTOR, "div[data-se='okta_email'] a.select-factor"),
|
|
(By.XPATH, "//div[@data-se='okta_email']//a[contains(@class,'select-factor')]"),
|
|
(By.XPATH, "//a[contains(@aria-label,'Select Email')]"),
|
|
(By.XPATH, "//div[@data-se='okta_email']//a[@data-se='button']"),
|
|
(By.CSS_SELECTOR, "a.select-factor.link-button"),
|
|
]:
|
|
try:
|
|
btn = self.driver.find_element(*sel)
|
|
if btn.is_displayed():
|
|
email_select = btn
|
|
print(f"[Deltains Claim login] Found Email Select via {sel}")
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if email_select:
|
|
email_select.click()
|
|
print("[Deltains Claim login] Clicked 'Select' for Email MFA")
|
|
time.sleep(5)
|
|
else:
|
|
print("[Deltains Claim login] Could not find Email Select button")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim login] Error checking MFA method selection: {e}")
|
|
|
|
try:
|
|
send_btn = WebDriverWait(self.driver, 8).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//input[@type='submit' and @value='Send me an email'] | "
|
|
"//input[@value='Send me an email'] | "
|
|
"//button[contains(text(),'Send me an email')]"))
|
|
)
|
|
send_btn.click()
|
|
print("[Deltains Claim login] Clicked 'Send me an email'")
|
|
time.sleep(5)
|
|
except TimeoutException:
|
|
print("[Deltains Claim login] No 'Send me an email' button, checking for OTP input...")
|
|
|
|
try:
|
|
otp_input = WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//input[@name='credentials.passcode' and @type='text'] | "
|
|
"//input[contains(@name,'passcode')]"))
|
|
)
|
|
print("[Deltains Claim login] OTP input found -> OTP_REQUIRED")
|
|
return "OTP_REQUIRED"
|
|
except TimeoutException:
|
|
pass
|
|
|
|
current_url = self.driver.current_url
|
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
|
return "SUCCESS"
|
|
|
|
try:
|
|
error_elem = self.driver.find_element(By.XPATH,
|
|
"//*[contains(@class,'error') or contains(@class,'alert-error')]")
|
|
error_text = error_elem.text.strip()[:200]
|
|
if error_text:
|
|
return f"ERROR: {error_text}"
|
|
except Exception:
|
|
pass
|
|
|
|
print("[Deltains Claim login] Could not determine login state - returning OTP_REQUIRED as fallback")
|
|
return "OTP_REQUIRED"
|
|
|
|
except Exception as e:
|
|
print(f"[Deltains Claim login] Exception: {e}")
|
|
return f"ERROR:LOGIN FAILED: {e}"
|
|
|
|
def _format_dob(self, dob_str):
|
|
if dob_str and "-" in dob_str:
|
|
dob_parts = dob_str.split("-")
|
|
if len(dob_parts) == 3:
|
|
return f"{dob_parts[1]}/{dob_parts[2]}/{dob_parts[0]}"
|
|
return dob_str
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 1 — Search patient (same search form as the eligibility flow) #
|
|
# ------------------------------------------------------------------ #
|
|
def step1_search_patient(self):
|
|
"""
|
|
Click the 'Submit claim' dashboard tile, which opens a 'Patient search'
|
|
modal, and search the patient roster by full name.
|
|
"""
|
|
try:
|
|
print(f"[Deltains Claim step1] Starting — memberId={self.memberId}, "
|
|
f"patient={self.firstName} {self.lastName}")
|
|
|
|
print("[Deltains Claim step1] Clicking 'Submit claim' dashboard tile...")
|
|
try:
|
|
submit_claim_tile = WebDriverWait(self.driver, 15).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[@data-testid='moduleTile'][.//a[contains(text(),'Submit claim')]] | "
|
|
"//a[@data-testid='moduleTileLabelLink'][contains(text(),'Submit claim')] | "
|
|
"//a[contains(text(),'Submit claim')]"))
|
|
)
|
|
submit_claim_tile.click()
|
|
time.sleep(5)
|
|
print("[Deltains Claim step1] Clicked 'Submit claim' tile")
|
|
except TimeoutException:
|
|
return "ERROR: 'Submit claim' dashboard tile not found"
|
|
|
|
print("[Deltains Claim step1] Waiting for 'Patient search' modal...")
|
|
try:
|
|
WebDriverWait(self.driver, 15).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//h1[contains(text(),'Patient search')] | "
|
|
"//h2[contains(text(),'Patient search')]"))
|
|
)
|
|
print("[Deltains Claim step1] 'Patient search' modal loaded")
|
|
except TimeoutException:
|
|
print("[Deltains Claim step1] 'Patient search' modal heading not found, continuing anyway...")
|
|
|
|
full_name = f"{self.firstName} {self.lastName}".strip()
|
|
print(f"[Deltains Claim step1] Searching patient roster by name: '{full_name}'...")
|
|
try:
|
|
name_field = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.ID, "patientRosterAutocomplete"))
|
|
)
|
|
name_field.click()
|
|
name_field.send_keys(Keys.CONTROL + "a")
|
|
name_field.send_keys(Keys.DELETE)
|
|
time.sleep(0.3)
|
|
name_field.send_keys(full_name)
|
|
time.sleep(1.5)
|
|
print(f"[Deltains Claim step1] Name entered: '{name_field.get_attribute('value')}'")
|
|
except TimeoutException:
|
|
return "ERROR: Patient roster search field not found"
|
|
|
|
print("[Deltains Claim step1] Clicking search icon button...")
|
|
try:
|
|
search_btn = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[.//*[contains(@class,'dd-icon-Search')]] | "
|
|
"//button[@data-testid='autocompleteSearchButton']"))
|
|
)
|
|
search_btn.click()
|
|
time.sleep(5)
|
|
print("[Deltains Claim step1] Clicked search icon button")
|
|
except TimeoutException:
|
|
return "ERROR: Search icon button not found"
|
|
|
|
print("[Deltains Claim step1] Checking for results...")
|
|
try:
|
|
patient_card = WebDriverWait(self.driver, 15).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//div[contains(@class,'patient-card-root')] | "
|
|
"//div[@data-testid='patientCard'] | "
|
|
"//div[starts-with(@data-testid,'patientCard')]"))
|
|
)
|
|
print("[Deltains Claim step1] Patient card found!")
|
|
self._patient_card = patient_card
|
|
except TimeoutException:
|
|
try:
|
|
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
if "no results" in body_text.lower() or "not found" in body_text.lower() or "no patient" in body_text.lower():
|
|
return "ERROR: No patient found with the provided name"
|
|
alerts = self.driver.find_elements(By.XPATH, "//*[@role='alert']")
|
|
for alert in alerts:
|
|
if alert.is_displayed():
|
|
return f"ERROR: {alert.text.strip()[:200]}"
|
|
except Exception:
|
|
pass
|
|
return "ERROR: No patient results found within timeout"
|
|
|
|
print("[Deltains Claim step1] Search completed")
|
|
return "SUCCESS"
|
|
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step1] Exception: {e}")
|
|
return f"ERROR: step1 failed: {e}"
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 2 — Select transaction type on the "Submit a claim" page #
|
|
# (search lands directly on this page — no separate member-page or #
|
|
# "Create claim" click is needed, unlike the DDMA flow) #
|
|
# ------------------------------------------------------------------ #
|
|
def step2_select_transaction_type(self):
|
|
"""
|
|
Confirm the "Submit a claim" page loaded, then click the
|
|
"Statement of completed services" radio button under
|
|
"Type of transaction".
|
|
"""
|
|
try:
|
|
print(f"[Deltains Claim step2] Current URL: {self.driver.current_url}")
|
|
try:
|
|
WebDriverWait(self.driver, 20).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//h1[contains(text(),'Submit a claim')] | "
|
|
"//*[contains(text(),'Type of transaction')]"))
|
|
)
|
|
print("[Deltains Claim step2] 'Submit a claim' page loaded")
|
|
except TimeoutException:
|
|
page_text = self.driver.execute_script("return document.body.innerText;")[:400]
|
|
print(f"[Deltains Claim step2] Claim form not detected — page: {page_text}")
|
|
return "ERROR: step2 failed: 'Submit a claim' page did not load"
|
|
|
|
print("[Deltains Claim step2] Clicking 'Statement of completed services' radio...")
|
|
try:
|
|
radio = WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//input[@data-testid='transactionRB_Claim']"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", radio)
|
|
self.driver.execute_script("arguments[0].click();", radio)
|
|
time.sleep(1)
|
|
print("[Deltains Claim step2] Selected 'Statement of completed services'")
|
|
except TimeoutException:
|
|
return "ERROR: step2 failed: 'Statement of completed services' radio not found"
|
|
|
|
return "SUCCESS"
|
|
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step2] Exception: {e}")
|
|
return f"ERROR: step2 failed: {e}"
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 4 — Fill service date and procedure lines #
|
|
# ------------------------------------------------------------------ #
|
|
def _fill_combobox(self, inp, value, label="field"):
|
|
"""Type value into a combobox and select the first matching dropdown option."""
|
|
try:
|
|
inp.click()
|
|
inp.send_keys(Keys.CONTROL + "a")
|
|
inp.send_keys(Keys.DELETE)
|
|
inp.send_keys(str(value))
|
|
time.sleep(0.5)
|
|
listbox_id = inp.get_attribute("aria-controls") or ""
|
|
try:
|
|
if listbox_id:
|
|
option = WebDriverWait(self.driver, 4).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
f"//*[@id='{listbox_id}']//*[@role='option'][1]"
|
|
))
|
|
)
|
|
else:
|
|
option = WebDriverWait(self.driver, 4).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"[Deltains Claim step4] {label}: selected '{value}'")
|
|
except TimeoutException:
|
|
inp.send_keys(Keys.TAB)
|
|
print(f"[Deltains Claim step4] {label}: typed '{value}' (no dropdown)")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Warning: could not fill {label}: {e}")
|
|
|
|
def _fill_by_id(self, element_id, value, label="field"):
|
|
"""Fill a plain text/tel input located by its exact id attribute."""
|
|
try:
|
|
inp = WebDriverWait(self.driver, 8).until(
|
|
EC.element_to_be_clickable((By.ID, element_id))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", inp)
|
|
inp.click()
|
|
inp.send_keys(Keys.CONTROL + "a")
|
|
inp.send_keys(Keys.DELETE)
|
|
inp.send_keys(str(value))
|
|
time.sleep(0.2)
|
|
print(f"[Deltains Claim step4] {label}: typed '{value}'")
|
|
return inp
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Warning: could not fill {label} (#{element_id}): {e}")
|
|
return None
|
|
|
|
_SURFACE_OPTIONS_JS = """
|
|
const box = document.querySelector('[role="listbox"]');
|
|
if (!box) return null;
|
|
return Array.from(box.querySelectorAll('[role="option"], li')).map(o => {
|
|
const cb = o.querySelector('input[type="checkbox"]');
|
|
return {
|
|
text: (o.textContent || '').trim().toUpperCase(),
|
|
selected: o.getAttribute('aria-selected') === 'true' || !!(cb && cb.checked),
|
|
};
|
|
});
|
|
"""
|
|
|
|
_SURFACE_CLICK_JS = """
|
|
const letter = arguments[0];
|
|
const box = document.querySelector('[role="listbox"]');
|
|
if (!box) return false;
|
|
const opt = Array.from(box.querySelectorAll('[role="option"], li'))
|
|
.find(o => (o.textContent || '').trim().toUpperCase() === letter);
|
|
if (!opt) return false;
|
|
opt.scrollIntoView({block: 'nearest'});
|
|
opt.click();
|
|
return true;
|
|
"""
|
|
|
|
def _select_tooth_surfaces(self, surface_select, letters, idx):
|
|
"""Open the toothSurfaces multi-select and check each letter, verifying
|
|
the option ends up selected (aria-selected / checkbox checked)."""
|
|
# Make sure the tooth-number listbox is gone so we don't read its options
|
|
self._close_mui_popover()
|
|
options = None
|
|
for _ in range(3):
|
|
surface_select.click()
|
|
try:
|
|
options = WebDriverWait(self.driver, 4).until(
|
|
lambda d: d.execute_script(self._SURFACE_OPTIONS_JS) or False)
|
|
break
|
|
except TimeoutException:
|
|
print(f"[Deltains Claim step4] surface[{idx}]: listbox did not open, retrying")
|
|
if not options:
|
|
print(f"[Deltains Claim step4] surface[{idx}]: could not open surface listbox")
|
|
return
|
|
print(f"[Deltains Claim step4] surface[{idx}]: options={[o['text'] for o in options]}")
|
|
|
|
for letter in letters:
|
|
state = {o["text"]: o["selected"] for o in (self.driver.execute_script(self._SURFACE_OPTIONS_JS) or [])}
|
|
if letter not in state:
|
|
print(f"[Deltains Claim step4] surface[{idx}]: option '{letter}' not found")
|
|
continue
|
|
if state[letter]:
|
|
print(f"[Deltains Claim step4] surface[{idx}]: '{letter}' already checked")
|
|
continue
|
|
self.driver.execute_script(self._SURFACE_CLICK_JS, letter)
|
|
time.sleep(0.3)
|
|
state = {o["text"]: o["selected"] for o in (self.driver.execute_script(self._SURFACE_OPTIONS_JS) or [])}
|
|
print(f"[Deltains Claim step4] surface[{idx}]: '{letter}' checked={state.get(letter)}")
|
|
|
|
shown = (surface_select.text or "").strip()
|
|
print(f"[Deltains Claim step4] surface[{idx}]: field now shows {shown!r}")
|
|
|
|
def _close_mui_popover(self):
|
|
"""Close any open MUI Select popover so its backdrop stops covering
|
|
the rest of the form."""
|
|
for attempt in range(3):
|
|
if not self.driver.find_elements(By.CSS_SELECTOR, '[role="listbox"]'):
|
|
return
|
|
try:
|
|
if attempt == 0:
|
|
self.driver.switch_to.active_element.send_keys(Keys.ESCAPE)
|
|
else:
|
|
self.driver.execute_script("""
|
|
const bd = document.querySelector('.MuiPopover-root .MuiBackdrop-root, .MuiMenu-root .MuiBackdrop-root');
|
|
if (bd) bd.click();
|
|
""")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] close popover attempt {attempt} failed: {e}")
|
|
time.sleep(0.4)
|
|
|
|
def step4_fill_claim_form(self):
|
|
"""
|
|
Fill each service line: date of service, procedure code, and fee
|
|
via their per-line ids (serviceLines.{idx}.dateOfService,
|
|
serviceLines.{idx}.procedure, serviceLines.{idx}.fee — confirmed
|
|
against the live Deltains claim page). Tooth/arch/quad/surface
|
|
selectors below remain unverified.
|
|
"""
|
|
try:
|
|
active_lines = [ln for ln in self.serviceLines if str(ln.get("procedureCode") or "").strip()]
|
|
print(f"[Deltains Claim 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"[Deltains Claim 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[contains(normalize-space(text()),'Add service')]"
|
|
))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn)
|
|
add_btn.click()
|
|
print(f"[Deltains Claim step4] Clicked 'Add service' for line {idx}")
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Could not click 'Add service': {e}")
|
|
|
|
formatted_dos = self._format_dob(self.serviceDate) if self.serviceDate else ""
|
|
if formatted_dos:
|
|
self._fill_by_id(f"serviceLines.{idx}.dateOfService", formatted_dos, f"dateOfService[{idx}]")
|
|
|
|
if code:
|
|
try:
|
|
proc_inp = WebDriverWait(self.driver, 8).until(
|
|
EC.element_to_be_clickable((By.ID, f"serviceLines.{idx}.procedure"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_inp)
|
|
self._fill_combobox(proc_inp, code, f"procedureCode[{idx}]")
|
|
time.sleep(0.5)
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Could not fill procedure code: {e}")
|
|
|
|
# Tooth number — MUI single-select (only rendered for codes
|
|
# that require it, e.g. fillings like D2392)
|
|
if tooth:
|
|
try:
|
|
tooth_select = WebDriverWait(self.driver, 8).until(
|
|
EC.element_to_be_clickable((By.ID, f"mui-component-select-serviceLines.{idx}.toothNumber"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_select)
|
|
tooth_select.click()
|
|
time.sleep(0.5)
|
|
padded = tooth.zfill(2)
|
|
option = WebDriverWait(self.driver, 5).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
f"//li[@role='option'][normalize-space(.)='{padded}'] | "
|
|
f"//li[@role='option'][normalize-space(.)='{tooth}']"))
|
|
)
|
|
option.click()
|
|
time.sleep(0.3)
|
|
print(f"[Deltains Claim step4] tooth[{idx}]: selected '{tooth}'")
|
|
except TimeoutException:
|
|
print(f"[Deltains Claim step4] tooth[{idx}]: select not present for this procedure code")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Could not fill tooth number: {e}")
|
|
|
|
# Tooth surfaces — MUI multi-select; each letter (B, D, F, I, L,
|
|
# M, O) is a checkbox option in a popover listbox that stays
|
|
# open until Escape / a backdrop click.
|
|
if surface:
|
|
try:
|
|
surface_select = WebDriverWait(self.driver, 8).until(
|
|
EC.element_to_be_clickable((By.ID, f"serviceLines.{idx}.toothSurfaces"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", surface_select)
|
|
surface_letters = []
|
|
for c in surface:
|
|
if c.isalpha() and c not in surface_letters:
|
|
surface_letters.append(c)
|
|
self._select_tooth_surfaces(surface_select, surface_letters, idx)
|
|
except TimeoutException:
|
|
print(f"[Deltains Claim step4] surface[{idx}]: select not present for this procedure code")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Could not fill tooth surfaces: {e}")
|
|
finally:
|
|
self._close_mui_popover()
|
|
|
|
if billed:
|
|
self._fill_by_id(f"serviceLines.{idx}.fee", billed, f"fee[{idx}]")
|
|
time.sleep(0.2)
|
|
|
|
print("[Deltains Claim step4] Done")
|
|
return "SUCCESS"
|
|
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step4] Exception: {e}")
|
|
return f"ERROR: step4 failed: {e}"
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 5 — Attach files #
|
|
# ------------------------------------------------------------------ #
|
|
def step5_attach_files(self):
|
|
"""
|
|
Expand the 'Attachments' accordion section under 'Additional
|
|
information' (max 5 files, PDF/JPEG only), then for each claimFile
|
|
with a filePath, send it directly to the hidden file input behind
|
|
the 'Choose a file' label.
|
|
"""
|
|
if not self.claimFiles:
|
|
print("[Deltains Claim step5] No files to attach")
|
|
return "SUCCESS"
|
|
|
|
try:
|
|
section = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//*[contains(text(),'Attachments') and contains(text(),'x-rays')]"))
|
|
)
|
|
if section.get_attribute("aria-expanded") == "false":
|
|
section.click()
|
|
time.sleep(1)
|
|
print("[Deltains Claim step5] Expanded 'Attachments' section")
|
|
except TimeoutException:
|
|
print("[Deltains Claim step5] 'Attachments' section header not found, "
|
|
"assuming already expanded")
|
|
|
|
attached = 0
|
|
for cf in self.claimFiles:
|
|
relative_path = cf.get("filePath") or ""
|
|
if not relative_path:
|
|
print(f"[Deltains Claim step5] Skipping file with no filePath: {cf}")
|
|
continue
|
|
|
|
abs_path = os.path.normpath(os.path.join(_BACKEND_CWD, relative_path.lstrip("/")))
|
|
if not os.path.isfile(abs_path):
|
|
print(f"[Deltains Claim step5] File not found on disk: {abs_path}")
|
|
continue
|
|
|
|
print(f"[Deltains Claim step5] Attaching: {abs_path}")
|
|
try:
|
|
file_input = WebDriverWait(self.driver, 8).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//label[contains(@class,'dd-file-uploader-btn')]//input[@type='file'] | "
|
|
"//input[@type='file']"))
|
|
)
|
|
self.driver.execute_script(
|
|
"arguments[0].style.display='block'; arguments[0].style.visibility='visible';",
|
|
file_input)
|
|
file_input.send_keys(abs_path)
|
|
time.sleep(1.5)
|
|
print(f"[Deltains Claim step5] Attached: {os.path.basename(abs_path)}")
|
|
attached += 1
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step5] Could not attach {abs_path}: {e}")
|
|
|
|
print(f"[Deltains Claim step5] Attached {attached}/{len(self.claimFiles)} file(s)")
|
|
return "SUCCESS"
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 6 — Click Review #
|
|
# ------------------------------------------------------------------ #
|
|
def step6_click_next(self):
|
|
"""Click the 'Review' button (data-testid='ctaReviewButton') to
|
|
advance from the claim-entry form to the review screen."""
|
|
try:
|
|
print(f"[Deltains Claim step6] Current URL: {self.driver.current_url}")
|
|
btn = WebDriverWait(self.driver, 15).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[@data-testid='ctaReviewButton']"
|
|
))
|
|
)
|
|
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("[Deltains Claim step6] Clicked 'Review'")
|
|
time.sleep(2)
|
|
print(f"[Deltains Claim step6] URL after Review: {self.driver.current_url}")
|
|
return "SUCCESS"
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step6] Exception: {e}")
|
|
return f"ERROR: step6 failed: {e}"
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Step 7 — Claims summary: check acknowledgement + submit #
|
|
# ------------------------------------------------------------------ #
|
|
def step7_submit_claim(self):
|
|
"""Click the 'Submit' button (data-testid='ctaSubmitButton') on the
|
|
review page to finalize the claim."""
|
|
try:
|
|
print(f"[Deltains Claim step7] Current URL: {self.driver.current_url}")
|
|
|
|
submit_btn = WebDriverWait(self.driver, 15).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[@data-testid='ctaSubmitButton']"
|
|
))
|
|
)
|
|
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("[Deltains Claim step7] Clicked 'Submit'")
|
|
time.sleep(2)
|
|
print(f"[Deltains Claim step7] URL after submit: {self.driver.current_url}")
|
|
return "SUCCESS"
|
|
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step7] Exception: {e}")
|
|
return f"ERROR: step7 failed: {e}"
|
|
|
|
def step8_save_confirmation_pdf(self):
|
|
"""
|
|
Wait for the 'Claim submitted successfully' confirmation modal,
|
|
extract the claim identification number, save the page as PDF
|
|
via CDP.
|
|
"""
|
|
try:
|
|
WebDriverWait(self.driver, 30).until(
|
|
lambda d: "claim submitted successfully" in d.page_source.lower()
|
|
)
|
|
time.sleep(2)
|
|
print(f"[Deltains Claim step8] Confirmation page URL: {self.driver.current_url}")
|
|
|
|
claim_number = None
|
|
try:
|
|
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
match = re.search(r'claim identification number\s*:\s*(\d+)', body_text, re.IGNORECASE)
|
|
if match:
|
|
claim_number = match.group(1)
|
|
print(f"[Deltains Claim step8] Extracted claim number: {claim_number}")
|
|
else:
|
|
match = re.search(r'\b(\d{10,})\b', body_text)
|
|
if match:
|
|
claim_number = match.group(1)
|
|
print(f"[Deltains Claim step8] Extracted claim number (fallback): {claim_number}")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step8] Could not extract claim number: {e}")
|
|
|
|
shared_downloads = os.path.join(_SERVICE_DIR, "downloads")
|
|
os.makedirs(shared_downloads, exist_ok=True)
|
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
|
safe_claim = ("_" + claim_number[:20]) if claim_number else ""
|
|
timestamp = time.strftime("%Y%m%d_%H%M%S")
|
|
pdf_filename = f"deltains_claim_confirmation_{safe_member}{safe_claim}_{timestamp}.pdf"
|
|
pdf_path = os.path.join(shared_downloads, pdf_filename)
|
|
|
|
try:
|
|
pdf_data = self.driver.execute_cdp_cmd("Page.printToPDF", {
|
|
"printBackground": True,
|
|
"paperWidth": 8.5,
|
|
"paperHeight": 11,
|
|
"marginTop": 0.4,
|
|
"marginBottom": 0.4,
|
|
"marginLeft": 0.4,
|
|
"marginRight": 0.4,
|
|
})
|
|
pdf_bytes = base64.b64decode(pdf_data["data"])
|
|
with open(pdf_path, "wb") as f:
|
|
f.write(pdf_bytes)
|
|
print(f"[Deltains Claim step8] PDF saved: {pdf_path}")
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step8] PDF capture failed: {e}")
|
|
return f"ERROR: step8 PDF failed: {e}"
|
|
|
|
print("[Deltains Claim step8] Clicking 'My claims'...")
|
|
try:
|
|
my_claims_btn = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[contains(normalize-space(text()),'My claims')]"))
|
|
)
|
|
my_claims_btn.click()
|
|
time.sleep(3)
|
|
print(f"[Deltains Claim step8] Navigated to My claims: {self.driver.current_url}")
|
|
except TimeoutException:
|
|
print("[Deltains Claim step8] 'My claims' button not found — leaving confirmation modal open")
|
|
|
|
return {
|
|
"status": "success",
|
|
"pdf_path": pdf_path,
|
|
"claimNumber": claim_number,
|
|
}
|
|
|
|
except Exception as e:
|
|
print(f"[Deltains Claim step8] Exception: {e}")
|
|
return f"ERROR: step8 failed: {e}"
|