fix: update DDMA claim worker for new provider portal
DDMA redesigned their claims-submission flow, so the worker now navigates via the dashboard's "Submit Claims" quick link, the "Member Search" and "Claim Detail" sidebar sections, checkbox-based member selection, indexed treatment-row fields, a multi-file upload input, and the new confirmation page layout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -245,18 +245,58 @@ class AutomationDDMAClaimSubmit:
|
||||
# ID + Birthdate (same search form as the eligibility flow) #
|
||||
# ------------------------------------------------------------------ #
|
||||
def step1_search_patient(self):
|
||||
"""Open the claims-submission page and search for the patient."""
|
||||
"""From the dashboard, click 'Submit Claims' and search for the patient."""
|
||||
wait = WebDriverWait(self.driver, 30)
|
||||
|
||||
try:
|
||||
claims_url = "https://www.deltadentalma.com/private/provider/claims-submission"
|
||||
print(f"[DDMA Claim step1] Navigating to claims-submission page: {claims_url}")
|
||||
|
||||
if "dashboard" not in self.driver.current_url.lower():
|
||||
print(f"[DDMA Claim step1] Not on dashboard — navigating to {MEMBERS_URL}")
|
||||
self.driver.get(MEMBERS_URL)
|
||||
time.sleep(2)
|
||||
|
||||
try:
|
||||
submit_claims_link = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//a[@href='/private/provider/claims-submission']")
|
||||
))
|
||||
submit_claims_link.click()
|
||||
print("[DDMA Claim step1] Clicked 'Submit Claims' quick link")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step1] Could not click 'Submit Claims' link ({e}) — falling back to direct navigation")
|
||||
self.driver.get(claims_url)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
if "claims-submission" not in self.driver.current_url.lower():
|
||||
print(f"[DDMA Claim step1] Warning: Ended up at {self.driver.current_url} instead of claims-submission page")
|
||||
|
||||
# Claims-submission page opens on the "Service Office" step — click the
|
||||
# "Member Search" sidebar item to get to the birthdate/subscriber search form.
|
||||
try:
|
||||
member_search_link = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//span[contains(@class,'mainLabel') and normalize-space(text())='Member Search']")
|
||||
))
|
||||
member_search_link.click()
|
||||
print("[DDMA Claim step1] Clicked 'Member Search' sidebar link")
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step1] Warning: Could not click 'Member Search' sidebar link: {e}")
|
||||
|
||||
# Fill Member ID
|
||||
if self.memberId:
|
||||
try:
|
||||
member_id_input = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//input[@data-test-id='memberId']")
|
||||
))
|
||||
member_id_input.click()
|
||||
member_id_input.send_keys(Keys.CONTROL, "a")
|
||||
member_id_input.send_keys(Keys.BACKSPACE)
|
||||
member_id_input.send_keys(self.memberId)
|
||||
print(f"[DDMA Claim step1] Filled Member ID: {self.memberId}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step1] Warning: Could not fill Member ID: {e}")
|
||||
|
||||
# Fill Birthdate (masked mm/dd/yyyy input — send digits only, mask inserts slashes)
|
||||
if self.dateOfBirth:
|
||||
try:
|
||||
@@ -266,7 +306,7 @@ class AutomationDDMAClaimSubmit:
|
||||
day = dob_parts[2].zfill(2)
|
||||
|
||||
birthdate_input = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//input[@data-test-id='InStateSearchBirthdate']")
|
||||
(By.XPATH, "//input[@data-test-id='memberBirthDate']")
|
||||
))
|
||||
birthdate_input.click()
|
||||
birthdate_input.send_keys(Keys.CONTROL, "a")
|
||||
@@ -276,23 +316,11 @@ class AutomationDDMAClaimSubmit:
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step1] Warning: Could not fill Birthdate: {e}")
|
||||
|
||||
# Fill Subscriber ID (== member ID)
|
||||
if self.memberId:
|
||||
try:
|
||||
subscriber_input = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//input[@data-test-id='InStateSearchSubscriberId']")
|
||||
))
|
||||
subscriber_input.clear()
|
||||
subscriber_input.send_keys(self.memberId)
|
||||
print(f"[DDMA Claim step1] Filled Subscriber ID: {self.memberId}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step1] Warning: Could not fill Subscriber ID: {e}")
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
# Click Search
|
||||
search_btn = wait.until(EC.element_to_be_clickable(
|
||||
(By.XPATH, "//button[@data-test-id='InStateSearchButton']")
|
||||
(By.XPATH, "//button[@data-test-id='memberSearchButton']")
|
||||
))
|
||||
search_btn.click()
|
||||
print("[DDMA Claim step1] Clicked Search button")
|
||||
@@ -318,7 +346,11 @@ class AutomationDDMAClaimSubmit:
|
||||
# Step 2 — Select the matching family member row → Member detail page #
|
||||
# ------------------------------------------------------------------ #
|
||||
def step2_open_member_page(self):
|
||||
"""Pick the family member row matching this patient and open their detail page."""
|
||||
"""Pick the family member row matching this patient and check its selection box.
|
||||
|
||||
The results table (columns: checkbox, Member, Member Id, Role, Group Name,
|
||||
Date Of Birth, Termination Date) stays on the same page — no navigation happens.
|
||||
"""
|
||||
try:
|
||||
# Wait for the results table to appear, then let it stabilize
|
||||
try:
|
||||
@@ -337,11 +369,15 @@ class AutomationDDMAClaimSubmit:
|
||||
if self.dateOfBirth and rows:
|
||||
try:
|
||||
dob_parts = self.dateOfBirth.split("-")
|
||||
dob_year, dob_month, dob_day = dob_parts[0], int(dob_parts[1]), int(dob_parts[2])
|
||||
expected_dob = f"{calendar.month_abbr[dob_month]} {dob_day}, {dob_year}".lower()
|
||||
dob_year = dob_parts[0]
|
||||
dob_month = dob_parts[1].zfill(2)
|
||||
dob_day = dob_parts[2].zfill(2)
|
||||
expected_dob = f"{dob_month}/{dob_day}/{dob_year}"
|
||||
for row in rows:
|
||||
cells = row.find_elements(By.XPATH, "./td")
|
||||
if len(cells) >= 3 and cells[2].text.strip().lower() == expected_dob:
|
||||
# cells: [0]=checkbox, [1]=Member, [2]=Member Id, [3]=Role,
|
||||
# [4]=Group Name, [5]=Date Of Birth, [6]=Termination Date
|
||||
if len(cells) >= 6 and cells[5].text.strip() == expected_dob:
|
||||
target_row = row
|
||||
break
|
||||
except Exception as e:
|
||||
@@ -361,26 +397,27 @@ class AutomationDDMAClaimSubmit:
|
||||
return "ERROR: step2 failed: no family member rows found"
|
||||
|
||||
try:
|
||||
name_link = target_row.find_element(By.XPATH, ".//a")
|
||||
name_link.click()
|
||||
print("[DDMA Claim step2] Clicked into patient detail page")
|
||||
checkbox = target_row.find_element(By.XPATH, ".//i[contains(@class,'fa-check-square')]")
|
||||
checkbox.click()
|
||||
print("[DDMA Claim step2] Checked the matching family member row")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step2] Warning: Could not click patient name link: {e}")
|
||||
return f"ERROR: step2 failed: could not click patient link: {e}"
|
||||
print(f"[DDMA Claim step2] Warning: Could not click member checkbox: {e}")
|
||||
return f"ERROR: step2 failed: could not click member checkbox: {e}"
|
||||
|
||||
time.sleep(2) # let the detail page navigate/render
|
||||
print(f"[DDMA Claim step2] Member detail page loaded: {self.driver.current_url}")
|
||||
time.sleep(1)
|
||||
|
||||
try:
|
||||
# Wait until Create claim button is visible AND enabled (not just in the DOM)
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//button[@aria-label='Create claim']"))
|
||||
claim_detail_link = WebDriverWait(self.driver, 10).until(
|
||||
EC.element_to_be_clickable(
|
||||
(By.XPATH, "//span[contains(@class,'mainLabel') and normalize-space(text())='Claim Detail']")
|
||||
)
|
||||
print("[DDMA Claim step2] 'Create claim' button is clickable")
|
||||
time.sleep(3) # let React finish any remaining re-renders
|
||||
except TimeoutException:
|
||||
print("[DDMA Claim step2] Warning: 'Create claim' button not clickable in time")
|
||||
)
|
||||
claim_detail_link.click()
|
||||
print("[DDMA Claim step2] Clicked 'Claim Detail' sidebar link")
|
||||
time.sleep(2)
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step2] Warning: Could not click 'Claim Detail' sidebar link: {e}")
|
||||
return f"ERROR: step2 failed: could not click 'Claim Detail' link: {e}"
|
||||
|
||||
return "SUCCESS"
|
||||
|
||||
@@ -389,118 +426,23 @@ class AutomationDDMAClaimSubmit:
|
||||
return f"ERROR: step2 failed: {e}"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Step 3 — Click "Create claim" button #
|
||||
# Step 3 — No-op: step2 already navigates into the Claim Detail form #
|
||||
# on the new site (no separate "Create claim" button exists there). #
|
||||
# ------------------------------------------------------------------ #
|
||||
def step3_click_create_claim(self):
|
||||
"""Click the 'Create claim' button on the Member Information page."""
|
||||
"""Confirm the Claim Detail / Treatment Form is present (navigation already happened in step2)."""
|
||||
try:
|
||||
print(f"[DDMA Claim step3] Current URL: {self.driver.current_url}")
|
||||
handles_before = set(self.driver.window_handles)
|
||||
url_before = self.driver.current_url
|
||||
|
||||
self.driver.execute_script("window.scrollTo(0, 0);")
|
||||
time.sleep(0.5)
|
||||
|
||||
# Re-find the button fresh each attempt to avoid stale element references.
|
||||
# The page may re-render after step2, so we do not cache the element.
|
||||
CLAIM_BTN_XPATHS = [
|
||||
"//button[@aria-label='Create claim']",
|
||||
"//button[contains(normalize-space(text()),'Create claim')]",
|
||||
"//button[.//span[contains(text(),'Create claim')]]",
|
||||
]
|
||||
|
||||
btn = None
|
||||
for xpath in CLAIM_BTN_XPATHS:
|
||||
try:
|
||||
btn = WebDriverWait(self.driver, 8).until(
|
||||
EC.element_to_be_clickable((By.XPATH, xpath))
|
||||
)
|
||||
if btn:
|
||||
print(f"[DDMA Claim step3] Found 'Create claim' via: {xpath}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not btn:
|
||||
all_btns = self.driver.find_elements(By.XPATH, "//button")
|
||||
print(f"[DDMA Claim step3] Buttons on page: {[b.get_attribute('aria-label') or b.text for b in all_btns]}")
|
||||
return "ERROR: step3 failed: 'Create claim' button not found"
|
||||
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
|
||||
time.sleep(0.5)
|
||||
|
||||
# Try direct Selenium click first, fall back to JS events
|
||||
clicked = False
|
||||
for attempt, method in enumerate(["selenium", "js_events", "js_click"]):
|
||||
try:
|
||||
# Re-find fresh on each attempt to avoid stale reference
|
||||
for xpath in CLAIM_BTN_XPATHS:
|
||||
try:
|
||||
btn = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, xpath))
|
||||
)
|
||||
if btn:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if method == "selenium":
|
||||
btn.click()
|
||||
elif method == "js_events":
|
||||
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)
|
||||
else:
|
||||
self.driver.execute_script("arguments[0].click();", btn)
|
||||
|
||||
print(f"[DDMA Claim step3] Clicked via {method} (attempt {attempt+1})")
|
||||
time.sleep(2)
|
||||
|
||||
# Check if navigation happened (URL changed or new tab opened)
|
||||
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(f"[DDMA Claim step3] Switched to new tab: {self.driver.current_url}")
|
||||
clicked = True
|
||||
break
|
||||
if self.driver.current_url != url_before:
|
||||
print(f"[DDMA Claim step3] URL changed to: {self.driver.current_url}")
|
||||
clicked = True
|
||||
break
|
||||
print(f"[DDMA Claim step3] URL unchanged after {method}, retrying...")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step3] {method} click failed: {e}, retrying...")
|
||||
|
||||
if not clicked:
|
||||
page_text = self.driver.execute_script("return document.body.innerText;")[:300]
|
||||
print(f"[DDMA Claim step3] No navigation after all click attempts — page: {page_text}")
|
||||
return "ERROR: step3 failed: button clicked but no navigation occurred"
|
||||
|
||||
# Wait for claim form to load
|
||||
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']")),
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//*[contains(text(),'date of service') or contains(text(),'Date of service') "
|
||||
"or contains(text(),'Procedure code')]"
|
||||
)),
|
||||
"//*[contains(text(),'Treatment Form')] | //input[@data-test-id='claimRowServiceDate0']"
|
||||
))
|
||||
)
|
||||
)
|
||||
print(f"[DDMA Claim step3] Claim form loaded: {self.driver.current_url}")
|
||||
print("[DDMA Claim step3] Treatment Form is present — no-op (step2 already navigated here)")
|
||||
return "SUCCESS"
|
||||
except TimeoutException:
|
||||
page_text = self.driver.execute_script("return document.body.innerText;")[:400]
|
||||
print(f"[DDMA Claim step3] Claim form not detected — page: {page_text}")
|
||||
|
||||
time.sleep(1)
|
||||
return "SUCCESS"
|
||||
|
||||
print(f"[DDMA Claim step3] Treatment Form not detected — page: {page_text}")
|
||||
return "ERROR: step3 failed: Treatment Form not found"
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step3] Exception: {e}")
|
||||
return f"ERROR: step3 failed: {e}"
|
||||
@@ -591,200 +533,117 @@ class AutomationDDMAClaimSubmit:
|
||||
print(f"[DDMA Claim step4] Warning: could not fill {label}: {e}")
|
||||
|
||||
def step4_fill_claim_form(self):
|
||||
"""Fill service date then all procedure line fields."""
|
||||
"""Fill each treatment row: service date, procedure code, tooth number, surface, amount.
|
||||
|
||||
The new Claim Detail Treatment Form has one row per procedure, each with
|
||||
indexed fields (ServiceDate{i}, Surface{i}, Amount{i} by data-test-id;
|
||||
procedure code and tooth number have no id, so they're matched positionally).
|
||||
"""
|
||||
try:
|
||||
month, day, year = self._parse_service_date()
|
||||
|
||||
# ── Service date (once, at the top of the form) ──────────────────
|
||||
if month and day and year:
|
||||
print(f"[DDMA Claim step4] Filling service date: {month}/{day}/{year}")
|
||||
try:
|
||||
dos_container = WebDriverWait(self.driver, 8).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//*[@data-testid and contains(@data-testid,'date-of-service')] | "
|
||||
"//*[contains(@aria-label,'Select date of service')]/ancestor::div[1]"
|
||||
))
|
||||
)
|
||||
month_el = dos_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']")
|
||||
day_el = dos_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']")
|
||||
year_el = dos_container.find_element(By.XPATH, ".//span[@data-type='year' and @contenteditable='true']")
|
||||
for elem, val in [(month_el, month), (day_el, day), (year_el, year)]:
|
||||
elem.click()
|
||||
elem.send_keys(Keys.CONTROL, "a")
|
||||
elem.send_keys(Keys.BACKSPACE)
|
||||
elem.send_keys(val)
|
||||
time.sleep(0.05)
|
||||
print("[DDMA Claim step4] Service date filled")
|
||||
except Exception:
|
||||
self._fill_spinbutton("month", month)
|
||||
self._fill_spinbutton("day", day)
|
||||
self._fill_spinbutton("year", year)
|
||||
else:
|
||||
service_date_str = f"{month}/{day}/{year}" if (month and day and year) else None
|
||||
if not service_date_str:
|
||||
print(f"[DDMA Claim step4] No valid service date: {self.serviceDate!r}")
|
||||
|
||||
time.sleep(0.3)
|
||||
|
||||
active_lines = [ln for ln in self.serviceLines if str(ln.get("procedureCode") or "").strip()]
|
||||
print(f"[DDMA 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"[DDMA Claim step4] Line {idx}: code={code} tooth={tooth!r} arch={arch!r} "
|
||||
f"quad={quad!r} surface={surface!r} billed={billed!r}")
|
||||
print(f"[DDMA Claim step4] Line {idx}: code={code} tooth={tooth!r} surface={surface!r} billed={billed!r}")
|
||||
|
||||
# ── Click "Add a procedure" for lines after the first ─────────
|
||||
# ── Click "+ Add another treatment" for lines after the first ──
|
||||
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']"
|
||||
))
|
||||
add_link = WebDriverWait(self.driver, 10).until(
|
||||
EC.element_to_be_clickable((By.XPATH, "//a[@data-test-id='addNewRowButton']"))
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn)
|
||||
add_btn.click()
|
||||
print(f"[DDMA Claim step4] Clicked 'Add a procedure' for line {idx}")
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_link)
|
||||
add_link.click()
|
||||
print(f"[DDMA Claim step4] Clicked 'Add another treatment' for line {idx}")
|
||||
time.sleep(1)
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not click 'Add a procedure': {e}")
|
||||
print(f"[DDMA Claim step4] Could not click 'Add another treatment': {e}")
|
||||
|
||||
# ── Procedure code ────────────────────────────────────────────
|
||||
# ── Service date ────────────────────────────────────────────
|
||||
if service_date_str:
|
||||
try:
|
||||
date_inp = WebDriverWait(self.driver, 8).until(
|
||||
EC.element_to_be_clickable((By.XPATH, f"//input[@data-test-id='claimRowServiceDate{idx}']"))
|
||||
)
|
||||
date_inp.click()
|
||||
date_inp.send_keys(Keys.CONTROL, "a")
|
||||
date_inp.send_keys(Keys.BACKSPACE)
|
||||
date_inp.send_keys(f"{month}{day}{year}")
|
||||
print(f"[DDMA Claim step4] serviceDate[{idx}]: filled {service_date_str}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not fill service date[{idx}]: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
# ── Procedure code (no id/data-test-id — matched positionally) ─
|
||||
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]
|
||||
proc_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='e.g. D1234']")
|
||||
proc_inp = proc_inputs[idx] if idx < len(proc_inputs) else (proc_inputs[-1] if proc_inputs else None)
|
||||
if proc_inp:
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_inp)
|
||||
self._fill_combobox(proc_inp, code, f"procedureCode[{idx}]")
|
||||
# Wait for the tooth input on the second row to be present AND interactable
|
||||
if tooth:
|
||||
try:
|
||||
WebDriverWait(self.driver, 8).until(
|
||||
lambda d: (
|
||||
len(d.find_elements(By.XPATH, "//input[@aria-label='Tooth']")) > idx and
|
||||
d.find_elements(By.XPATH, "//input[@aria-label='Tooth']")[idx].is_enabled() and
|
||||
d.find_elements(By.XPATH, "//input[@aria-label='Tooth']")[idx].is_displayed()
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
time.sleep(1) # fallback: just wait a second
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not fill procedure code: {e}")
|
||||
|
||||
# ── Tooth ─────────────────────────────────────────────────────
|
||||
if tooth:
|
||||
try:
|
||||
tooth_inputs = self.driver.find_elements(By.XPATH, "//input[@aria-label='Tooth']")
|
||||
if idx < len(tooth_inputs):
|
||||
ti = tooth_inputs[idx]
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", ti)
|
||||
time.sleep(0.3)
|
||||
listbox_id = ti.get_attribute("aria-controls") or ""
|
||||
|
||||
# Click the "^" chevron toggle button to open the dropdown.
|
||||
# It lives in the same container as the tooth input —
|
||||
# try progressively wider ancestor scopes.
|
||||
toggle_clicked = False
|
||||
for ancestor_steps in range(1, 6):
|
||||
ancestor_axis = "/".join([".."] * ancestor_steps)
|
||||
toggle_xpath = (
|
||||
f"//input[@aria-label='Tooth'][{idx+1}]"
|
||||
f"/{ancestor_axis}//button"
|
||||
)
|
||||
toggles = self.driver.find_elements(By.XPATH, toggle_xpath)
|
||||
if toggles:
|
||||
try:
|
||||
self.driver.execute_script("arguments[0].click();", toggles[0])
|
||||
print(f"[DDMA Claim step4] tooth[{idx}]: clicked toggle btn (ancestor depth {ancestor_steps})")
|
||||
toggle_clicked = True
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not toggle_clicked:
|
||||
# Fallback: JS focus the input to open the combobox
|
||||
self.driver.execute_script("arguments[0].focus();", ti)
|
||||
print(f"[DDMA Claim step4] tooth[{idx}]: no toggle btn found, used JS focus")
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Select the matching tooth number from the 1-32 listbox
|
||||
try:
|
||||
if listbox_id:
|
||||
option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
f"//*[@id='{listbox_id}']//*[@role='option' and normalize-space(.)='{tooth}']"
|
||||
))
|
||||
)
|
||||
else:
|
||||
print(f"[DDMA Claim step4] procedureCode[{idx}]: input not found")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not fill procedure code[{idx}]: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
# ── Tooth Number (custom scrollable dropdown, no native <select>) ─
|
||||
if tooth:
|
||||
try:
|
||||
tooth_triggers = self.driver.find_elements(By.XPATH, "//span[contains(@class,'filtrable')]")
|
||||
if idx < len(tooth_triggers):
|
||||
trigger = tooth_triggers[idx]
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", trigger)
|
||||
trigger.click()
|
||||
time.sleep(0.4)
|
||||
option = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
f"//*[@role='listbox']//*[@role='option' and normalize-space(.)='{tooth}']"
|
||||
f"//*[@role='option' and normalize-space(.)='{tooth}'] | "
|
||||
f"//li[normalize-space(.)='{tooth}'] | "
|
||||
f"//*[contains(@class,'dropdown')]//*[normalize-space(.)='{tooth}']"
|
||||
))
|
||||
)
|
||||
option.click()
|
||||
print(f"[DDMA Claim step4] tooth[{idx}]: selected '{tooth}' from dropdown")
|
||||
except TimeoutException:
|
||||
print(f"[DDMA Claim step4] tooth[{idx}]: option '{tooth}' not found in dropdown")
|
||||
time.sleep(0.3)
|
||||
print(f"[DDMA Claim step4] tooth[{idx}]: selected '{tooth}'")
|
||||
else:
|
||||
print(f"[DDMA Claim step4] tooth[{idx}]: dropdown trigger not found")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not fill tooth: {e}")
|
||||
print(f"[DDMA Claim step4] Could not fill tooth[{idx}]: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
# ── Arch ──────────────────────────────────────────────────────
|
||||
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"[DDMA Claim step4] Could not fill arch: {e}")
|
||||
|
||||
# ── Quad ──────────────────────────────────────────────────────
|
||||
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"[DDMA Claim step4] Could not fill quad: {e}")
|
||||
|
||||
# ── Surface (type directly then Tab to billed amount) ─────────
|
||||
# ── Surface ──────────────────────────────────────────────────
|
||||
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)
|
||||
print(f"[DDMA Claim step4] surface[{idx}]: typed '{surface}'")
|
||||
time.sleep(0.2)
|
||||
surf_inp = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, f"//input[@data-test-id='claimRowSurface{idx}']"))
|
||||
)
|
||||
self._fill_text_input(surf_inp, surface, f"surface[{idx}]")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not fill surface: {e}")
|
||||
print(f"[DDMA Claim step4] Could not fill surface[{idx}]: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
# ── Billed amount ─────────────────────────────────────────────
|
||||
# ── Amount ───────────────────────────────────────────────────
|
||||
if billed:
|
||||
try:
|
||||
billed_inputs = self.driver.find_elements(By.XPATH,
|
||||
"//input[@aria-label='Enter billed amount']"
|
||||
amount_inp = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((By.XPATH, f"//input[@data-test-id='claimRowAmount{idx}']"))
|
||||
)
|
||||
if idx < len(billed_inputs):
|
||||
self._fill_text_input(billed_inputs[idx], billed, f"billedAmount[{idx}]")
|
||||
time.sleep(0.2)
|
||||
self._fill_text_input(amount_inp, billed, f"amount[{idx}]")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step4] Could not fill billed amount: {e}")
|
||||
print(f"[DDMA Claim step4] Could not fill amount[{idx}]: {e}")
|
||||
time.sleep(0.2)
|
||||
|
||||
print("[DDMA Claim step4] Done")
|
||||
return "SUCCESS"
|
||||
@@ -797,12 +656,12 @@ class AutomationDDMAClaimSubmit:
|
||||
# Step 5 — Attach files #
|
||||
# ------------------------------------------------------------------ #
|
||||
def step5_attach_files(self):
|
||||
"""For each claimFile with a filePath, click 'Add a file' and upload it."""
|
||||
"""Upload all claimFiles at once via the multi-file drag-and-drop input."""
|
||||
if not self.claimFiles:
|
||||
print("[DDMA Claim step5] No files to attach")
|
||||
return "SUCCESS"
|
||||
|
||||
attached = 0
|
||||
abs_paths = []
|
||||
for cf in self.claimFiles:
|
||||
relative_path = cf.get("filePath") or ""
|
||||
if not relative_path:
|
||||
@@ -814,49 +673,38 @@ class AutomationDDMAClaimSubmit:
|
||||
if not os.path.isfile(abs_path):
|
||||
print(f"[DDMA Claim step5] File not found on disk: {abs_path}")
|
||||
continue
|
||||
abs_paths.append(abs_path)
|
||||
|
||||
if not abs_paths:
|
||||
print("[DDMA Claim step5] No valid files found on disk to attach")
|
||||
return "SUCCESS"
|
||||
|
||||
print(f"[DDMA Claim step5] Attaching: {abs_path}")
|
||||
try:
|
||||
# Click "Add a file" button/link
|
||||
add_file_btn = WebDriverWait(self.driver, 10).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//button[.//span[contains(text(),'Add a file')]] | "
|
||||
"//button[contains(normalize-space(text()),'Add a file')] | "
|
||||
"//*[contains(text(),'Add a file') and (@role='button' or self::label)]"
|
||||
))
|
||||
file_input = WebDriverWait(self.driver, 10).until(
|
||||
EC.presence_of_element_located((By.XPATH, "//input[@data-test-id='fileUploadInput']"))
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_file_btn)
|
||||
ActionChains(self.driver).move_to_element(add_file_btn).click().perform()
|
||||
time.sleep(1)
|
||||
|
||||
# Find the file input that appeared (may be hidden)
|
||||
file_input = WebDriverWait(self.driver, 8).until(
|
||||
EC.presence_of_element_located((By.XPATH, "//input[@type='file']"))
|
||||
)
|
||||
# Make hidden inputs interactable
|
||||
# The input may be visually hidden behind a drag-and-drop overlay.
|
||||
self.driver.execute_script("arguments[0].style.display='block';", file_input)
|
||||
file_input.send_keys(abs_path)
|
||||
# <input multiple> accepts several paths joined by newlines in one send_keys call.
|
||||
file_input.send_keys("\n".join(abs_paths))
|
||||
time.sleep(1.5)
|
||||
print(f"[DDMA Claim step5] Attached: {os.path.basename(abs_path)}")
|
||||
attached += 1
|
||||
print(f"[DDMA Claim step5] Attached {len(abs_paths)}/{len(self.claimFiles)} file(s): "
|
||||
f"{[os.path.basename(p) for p in abs_paths]}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step5] Could not attach {abs_path}: {e}")
|
||||
print(f"[DDMA Claim step5] Could not attach files: {e}")
|
||||
return f"ERROR: step5 failed: {e}"
|
||||
|
||||
print(f"[DDMA Claim 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 (React Aria — dispatches pointer events directly)."""
|
||||
"""Click the sidebar 'Submit Claim' navigation button to advance from the Claim Detail form."""
|
||||
try:
|
||||
print(f"[DDMA Claim step6] Current URL: {self.driver.current_url}")
|
||||
btn = WebDriverWait(self.driver, 15).until(
|
||||
EC.element_to_be_clickable((By.XPATH,
|
||||
"//button[@data-testid='next-step-btn'] | "
|
||||
"//button[@aria-label='Next step']"
|
||||
))
|
||||
EC.element_to_be_clickable((By.XPATH, "//button[@data-test-id='submitClaimNavigationButton']"))
|
||||
)
|
||||
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
|
||||
time.sleep(0.5)
|
||||
@@ -876,85 +724,40 @@ class AutomationDDMAClaimSubmit:
|
||||
return f"ERROR: step6 failed: {e}"
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Step 7 — Claims summary: check acknowledgement + submit #
|
||||
# Step 7 — No-op: step6's "Submit Claim" click already finalizes the #
|
||||
# claim on the new site (no separate acknowledgement/summary page). #
|
||||
# ------------------------------------------------------------------ #
|
||||
def step7_submit_claim(self):
|
||||
"""On the claims summary page, tick the acknowledgement checkbox then click Submit claim."""
|
||||
"""Confirm the confirmation page is present (submission already happened in step6)."""
|
||||
try:
|
||||
print(f"[DDMA Claim step7] Current URL: {self.driver.current_url}")
|
||||
|
||||
# Wait for the acknowledgement checkbox to appear
|
||||
checkbox = WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH,
|
||||
"//input[@type='checkbox'] | "
|
||||
"//*[@role='checkbox'] | "
|
||||
"//label[contains(.,'submitting this claim')]//input | "
|
||||
"//*[contains(@aria-label,'submitting this claim')]"
|
||||
))
|
||||
WebDriverWait(self.driver, 20).until(
|
||||
lambda d: "claim submitted successfully" in d.page_source.lower()
|
||||
)
|
||||
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)
|
||||
print("[DDMA Claim step7] Checked acknowledgement checkbox")
|
||||
time.sleep(0.5)
|
||||
|
||||
# Click Submit claim button
|
||||
submit_btn = None
|
||||
for xpath in [
|
||||
"//button[.//span[contains(text(),'Submit claim')]]",
|
||||
"//button[contains(normalize-space(text()),'Submit claim')]",
|
||||
"//button[@aria-label='Submit claim']",
|
||||
]:
|
||||
try:
|
||||
submit_btn = WebDriverWait(self.driver, 8).until(
|
||||
EC.element_to_be_clickable((By.XPATH, xpath))
|
||||
)
|
||||
if submit_btn:
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
if not submit_btn:
|
||||
return "ERROR: step7 failed: Submit claim button not found"
|
||||
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("[DDMA Claim step7] Clicked 'Submit claim'")
|
||||
time.sleep(2)
|
||||
print(f"[DDMA Claim step7] URL after submit: {self.driver.current_url}")
|
||||
print("[DDMA Claim step7] Confirmation page detected — no-op (step6 already submitted)")
|
||||
return "SUCCESS"
|
||||
|
||||
except TimeoutException:
|
||||
print(f"[DDMA Claim step7] Confirmation page not detected — URL: {self.driver.current_url}")
|
||||
return "ERROR: step7 failed: confirmation page not detected"
|
||||
except Exception as e:
|
||||
print(f"[DDMA Claim step7] Exception: {e}")
|
||||
return f"ERROR: step7 failed: {e}"
|
||||
|
||||
def step8_save_confirmation_pdf(self):
|
||||
"""Wait for the Thank-you page, extract the claim number, save page as PDF."""
|
||||
"""Wait for the confirmation page, extract the claim number, save page as PDF."""
|
||||
import re
|
||||
try:
|
||||
# Wait for confirmation page
|
||||
WebDriverWait(self.driver, 30).until(
|
||||
lambda d: "thank" in d.page_source.lower() or "submitted claim" in d.page_source.lower()
|
||||
lambda d: "claim submitted successfully" in d.page_source.lower()
|
||||
)
|
||||
time.sleep(2)
|
||||
print(f"[DDMA Claim step8] Confirmation page URL: {self.driver.current_url}")
|
||||
|
||||
# Extract claim number from page text
|
||||
# Extract claim number from page text (row: "Claim Number" label + value, e.g. "202626032473000")
|
||||
claim_number = None
|
||||
try:
|
||||
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
||||
match = re.search(r'submitted claim\s+(\d{10,})', body_text, re.IGNORECASE)
|
||||
match = re.search(r'claim number\s*\n?\s*(\d{6,})', body_text, re.IGNORECASE)
|
||||
if match:
|
||||
claim_number = match.group(1)
|
||||
print(f"[DDMA Claim step8] Extracted claim number: {claim_number}")
|
||||
|
||||
Reference in New Issue
Block a user