Files
DentalManagementMH06/apps/SeleniumService/selenium_UnitedDH_claimSubmitWorker.py
Gitead 1e581c193c feat: United/DentalHub claim submission automation and patient list sync
- Add full Selenium automation for United/DentalHub claim submission
  (steps 1-8: login, OTP, patient search, practitioner page, code entry,
  other coverage No, attachments, submit, Status & History PDF)
- Consolidate UnitedDH siteKey to UNITED_SCO throughout app
- Fix procedure date overwrite with Ctrl+A+Delete before typing service date
- Fix OTP popup reliability: emit every poll (no throttle)
- Fix Chrome session persistence: only clear cookies on startup
- Add touchPatient() to storage: claim submission now pushes patient to
  top of list across eligibility, claims, and documents pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 00:29:04 -04:00

1018 lines
48 KiB
Python

"""
United/DentalHub Claim Submission Worker.
Based on the UnitedSCO eligibility check worker (same portal: app.dentalhub.com).
"""
from selenium import webdriver
from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
import time
import os
import base64
import json
from unitedsco_browser_manager import get_browser_manager
_SERVICE_DIR = os.path.dirname(os.path.abspath(__file__))
_BACKEND_CWD = os.path.normpath(os.path.join(_SERVICE_DIR, "..", "Backend"))
class AutomationUnitedDHClaimSubmit:
def __init__(self, data):
self.headless = False
self.driver = None
claim = data.get("claim", {}) if isinstance(data, dict) else {}
self.memberId = claim.get("memberId", "")
self.dateOfBirth = claim.get("dateOfBirth", "")
self.firstName = claim.get("firstName", "")
self.lastName = claim.get("lastName", "")
self.serviceDate = claim.get("serviceDate", "")
self.serviceLines = claim.get("serviceLines", [])
self.claimFiles = claim.get("claimFiles", [])
self.patientName = claim.get("patientName", "")
self.remarks = claim.get("remarks", "")
# Credentials (injected by backend)
self.uniteddh_username = claim.get("uniteddhUsername", "")
self.uniteddh_password = claim.get("uniteddhPassword", "")
# Re-use the UnitedSCO browser manager (same portal)
self.download_dir = get_browser_manager().download_dir
os.makedirs(self.download_dir, exist_ok=True)
def config_driver(self):
self.driver = get_browser_manager().get_driver(self.headless)
def _force_logout(self):
"""Force logout by clearing cookies for the DentalHub domain."""
try:
print("[UnitedDH Claim login] Forcing logout due to credential change...")
browser_manager = get_browser_manager()
try:
self.driver.get("https://app.dentalhub.com/app/dashboard")
time.sleep(2)
for selector in [
"//button[contains(text(),'Log out') or contains(text(),'Logout') or contains(text(),'Sign out')]",
"//a[contains(text(),'Log out') or contains(text(),'Logout')]",
"//button[@aria-label='Log out' or @aria-label='Logout']",
]:
try:
btn = WebDriverWait(self.driver, 3).until(EC.element_to_be_clickable((By.XPATH, selector)))
btn.click()
print("[UnitedDH Claim login] Clicked logout button")
time.sleep(2)
break
except TimeoutException:
continue
except Exception as e:
print(f"[UnitedDH Claim login] Could not click logout button: {e}")
try:
self.driver.delete_all_cookies()
print("[UnitedDH Claim login] Cleared all cookies")
except Exception as e:
print(f"[UnitedDH Claim login] Error clearing cookies: {e}")
browser_manager.clear_credentials_hash()
return True
except Exception as e:
print(f"[UnitedDH Claim login] Error during forced logout: {e}")
return False
def login(self, url):
wait = WebDriverWait(self.driver, 30)
browser_manager = get_browser_manager()
try:
if self.uniteddh_username and browser_manager.credentials_changed(self.uniteddh_username):
self._force_logout()
self.driver.get(url)
time.sleep(2)
# Check if already logged in
try:
current_url = self.driver.current_url
print(f"[UnitedDH Claim login] Current URL: {current_url}")
if "app.dentalhub.com" in current_url and "login" not in current_url.lower():
try:
WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.XPATH,
'//input[contains(@placeholder,"Search")] | //*[contains(@class,"dashboard")] | //nav'))
)
print("[UnitedDH Claim login] Already logged in")
return "ALREADY_LOGGED_IN"
except TimeoutException:
pass
except Exception as e:
print(f"[UnitedDH Claim login] Error checking current state: {e}")
self.driver.get(url)
time.sleep(3)
current_url = self.driver.current_url
print(f"[UnitedDH Claim login] After navigation URL: {current_url}")
if "app.dentalhub.com" in current_url and "login" not in current_url.lower():
print("[UnitedDH Claim login] Already on dashboard")
return "ALREADY_LOGGED_IN"
# Check for OTP input first
try:
WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.XPATH,
"//input[@type='tel' or contains(@placeholder,'code') or contains(@aria-label,'Verification')]"))
)
print("[UnitedDH Claim login] OTP input found")
return "OTP_REQUIRED"
except TimeoutException:
pass
# Click LOGIN button on dentalhub landing page
if "app.dentalhub.com" in current_url:
try:
login_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.XPATH,
"//button[contains(text(),'LOGIN') or contains(text(),'Log In') or contains(text(),'Login')]"))
)
login_btn.click()
print("[UnitedDH Claim login] Clicked LOGIN button")
time.sleep(5)
except TimeoutException:
print("[UnitedDH Claim login] No LOGIN button found, proceeding...")
current_url = self.driver.current_url
print(f"[UnitedDH Claim login] After LOGIN click URL: {current_url}")
# Fill Azure B2C credentials
if "b2clogin.com" in current_url or "login" in current_url.lower():
print("[UnitedDH Claim login] On B2C login page - filling credentials")
# Check if already on phone verification page
try:
send_code_btn = self.driver.find_element(By.XPATH,
"//button[@id='sendCode'] | //input[@id='sendCode'] | "
"//button[contains(text(),'Text Me') or contains(text(),'Send Code')]"
)
if send_code_btn.is_displayed():
print("[UnitedDH Claim login] Already on phone verification page - clicking 'Text Me'")
self.driver.execute_script("arguments[0].click();", send_code_btn)
time.sleep(3)
return "OTP_REQUIRED"
except Exception:
pass
try:
email_field = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH,
"//input[@id='signInName' or @name='signInName' or @name='Email address' or @type='email']"))
)
email_field.clear()
email_field.send_keys(self.uniteddh_username)
print(f"[UnitedDH Claim login] Entered username: {self.uniteddh_username}")
password_field = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH,
"//input[@id='password' or @type='password']"))
)
password_field.clear()
password_field.send_keys(self.uniteddh_password)
print("[UnitedDH Claim login] Entered password")
signin_button = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH,
"//button[@id='next'] | //button[@type='submit' and contains(text(),'Sign')]"))
)
signin_button.click()
print("[UnitedDH Claim login] Clicked Sign in button")
if self.uniteddh_username:
browser_manager.save_credentials_hash(self.uniteddh_username)
time.sleep(5)
# Handle MFA method selection page
try:
continue_btn = self.driver.find_element(By.XPATH, "//button[contains(text(),'Continue')]")
phone_elements = self.driver.find_elements(By.XPATH, "//*[contains(text(),'Phone')]")
if continue_btn and phone_elements:
print("[UnitedDH Claim login] MFA method selection page detected")
try:
phone_radio = self.driver.find_element(By.XPATH,
"//input[@type='radio' and (contains(@value,'phone') or contains(@value,'Phone'))] | "
"//label[contains(text(),'Phone')]/preceding-sibling::input[@type='radio'] | "
"//input[@type='radio']"
)
if phone_radio and not phone_radio.is_selected():
phone_radio.click()
print("[UnitedDH Claim login] Selected 'Phone' radio button")
except Exception as radio_err:
print(f"[UnitedDH Claim login] Could not click Phone radio: {radio_err}")
time.sleep(1)
continue_btn.click()
print("[UnitedDH Claim login] Clicked 'Continue' on MFA selection page")
time.sleep(3)
except Exception:
pass
# Check for "Text Me" / Send Code button (phone OTP)
try:
send_code_btn = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.XPATH,
"//button[@id='sendCode'] | //input[@id='sendCode'] | "
"//button[contains(text(),'Text Me') or contains(text(),'Send Code')]"))
)
print("[UnitedDH Claim login] Found 'Text Me' / Send Code button")
self.driver.execute_script("arguments[0].click();", send_code_btn)
time.sleep(3)
return "OTP_REQUIRED"
except TimeoutException:
pass
# Check if OTP input appeared
try:
WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH,
"//input[@type='tel' or contains(@placeholder,'code') or contains(@aria-label,'Verification')]"))
)
print("[UnitedDH Claim login] OTP input appeared after sign-in")
return "OTP_REQUIRED"
except TimeoutException:
pass
# Check if login succeeded
current_url = self.driver.current_url
if "app.dentalhub.com" in current_url and "login" not in current_url.lower():
print("[UnitedDH Claim login] Login succeeded without OTP")
return "SUCCESS"
print(f"[UnitedDH Claim login] Unexpected state - URL: {current_url}")
return "SUCCESS"
except Exception as e:
return f"ERROR: Login failed - {e}"
# If still on dentalhub, may already be logged in
if "app.dentalhub.com" in current_url:
return "ALREADY_LOGGED_IN"
return "SUCCESS"
except Exception as e:
return f"ERROR: Login exception - {e}"
# ── Helpers ────────────────────────────────────────────────────────────────
def _check_for_error_dialog(self):
"""Check for and dismiss common error dialogs. Returns error message string or None."""
error_patterns = [
("Patient Not Found", "Patient Not Found - please check the Subscriber ID, DOB, and Payer selection"),
("Insufficient Information", "Insufficient Information - need Subscriber ID + DOB, or First Name + Last Name + DOB"),
("No Eligibility", "No eligibility information found for this patient"),
("Error", None),
]
for pattern, default_msg in error_patterns:
try:
dialog_elem = self.driver.find_element(By.XPATH,
f"//modal-container//*[contains(text(),'{pattern}')] | "
f"//div[contains(@class,'modal')]//*[contains(text(),'{pattern}')]"
)
if dialog_elem.is_displayed():
try:
modal = self.driver.find_element(By.XPATH, "//modal-container | //div[contains(@class,'modal-dialog')]")
dialog_text = modal.text.strip()[:200]
except Exception:
dialog_text = dialog_elem.text.strip()[:200]
print(f"[UnitedDH Claim step1] Error dialog detected: {dialog_text}")
try:
dismiss_btn = self.driver.find_element(By.XPATH,
"//modal-container//button[contains(text(),'Ok') or contains(text(),'OK') or contains(text(),'Close')] | "
"//div[contains(@class,'modal')]//button[contains(text(),'Ok') or contains(text(),'OK') or contains(text(),'Close')]"
)
dismiss_btn.click()
print("[UnitedDH Claim step1] Dismissed error dialog")
time.sleep(1)
except Exception:
try:
close_btn = self.driver.find_element(By.XPATH, "//modal-container//button[@class='close']")
close_btn.click()
except Exception:
pass
error_msg = default_msg if default_msg else f"ERROR: {dialog_text}"
return f"ERROR: {error_msg}"
except Exception:
continue
return None
def _format_dob(self, dob_str):
"""Convert DOB from YYYY-MM-DD to MM/DD/YYYY format."""
if dob_str and "-" in dob_str:
dob_parts = dob_str.split("-")
if len(dob_parts) == 3:
return f"{dob_parts[1]}/{dob_parts[2]}/{dob_parts[0]}"
return dob_str
def _get_existing_downloads(self):
"""Get set of existing PDF files in download dir before clicking."""
import glob
return set(glob.glob(os.path.join(self.download_dir, "*.pdf")))
def _wait_for_new_download(self, existing_files, timeout=15):
"""Wait for a new PDF file to appear in the download dir."""
import glob
for _ in range(timeout * 2):
time.sleep(0.5)
current = set(glob.glob(os.path.join(self.download_dir, "*.pdf")))
new_files = current - existing_files
if new_files:
crdownloads = glob.glob(os.path.join(self.download_dir, "*.crdownload"))
if not crdownloads:
return list(new_files)[0]
return None
def _hide_browser(self):
"""Hide the browser window after task completion."""
try:
try:
self.driver.get("about:blank")
time.sleep(0.5)
except Exception:
pass
try:
self.driver.minimize_window()
print("[UnitedDH Claim] Browser window minimized")
return
except Exception:
pass
try:
self.driver.set_window_position(-10000, -10000)
print("[UnitedDH Claim] Browser window moved off-screen")
return
except Exception:
pass
try:
import subprocess
subprocess.run(["xdotool", "getactivewindow", "windowminimize"],
timeout=3, capture_output=True)
print("[UnitedDH Claim] Browser minimized via xdotool")
except Exception:
pass
except Exception as e:
print(f"[UnitedDH Claim] Could not hide browser: {e}")
def _capture_pdf(self, identifier):
"""Capture the current page as PDF using Chrome DevTools Protocol."""
try:
pdf_options = {
"landscape": False,
"displayHeaderFooter": False,
"printBackground": True,
"preferCSSPageSize": True,
"paperWidth": 8.5,
"paperHeight": 11,
"marginTop": 0.4,
"marginBottom": 0.4,
"marginLeft": 0.4,
"marginRight": 0.4,
"scale": 0.9,
}
file_id = identifier if identifier else f"{self.firstName}_{self.lastName}"
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
pdf_data = base64.b64decode(result.get("data", ""))
pdf_path = os.path.join(self.download_dir, f"uniteddh_claim_{file_id}_{int(time.time())}.pdf")
with open(pdf_path, "wb") as f:
f.write(pdf_data)
return pdf_path
except Exception as e:
print(f"[UnitedDH Claim _capture_pdf] Error: {e}")
return None
# ── Claim steps ────────────────────────────────────────────────────────────
def step1_search_patient(self):
"""
1. Navigate directly to the Submit Claim page.
2. Fill Subscriber ID, Date of Birth, Procedure Date, select Payer
(first UnitedHealthcare Massachusetts result).
3. Click Continue.
"""
try:
print(f"[UnitedDH Claim] step1: memberId={self.memberId}, dob={self.dateOfBirth}, serviceDate={self.serviceDate}")
# Navigate directly to the claim submission page
self.driver.get("https://app.dentalhub.com/app/claims-auths/claim")
time.sleep(3)
print(f"[UnitedDH Claim] step1 URL: {self.driver.current_url}")
# --- Wait for claim form to load (Subscriber ID field) ---
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, "subscriberId_Front"))
)
print("[UnitedDH Claim] step1: Claim form loaded (subscriberId_Front found)")
except TimeoutException:
# Try alternate field names
try:
WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH,
"//input[contains(@id,'subscriberId') or contains(@id,'memberId')]"
))
)
print("[UnitedDH Claim] step1: Claim form loaded (alternate subscriber field)")
except TimeoutException:
return "ERROR: step1 - Claim form not found after clicking Submit Claim"
# --- Fill Subscriber ID ---
if self.memberId:
subscriber_id_selectors = [
"//input[@id='subscriberId_Front']",
"//input[@id='subscriberId_Back' or @id='subscriberID_Back']",
"//input[@id='memberId_Back' or @id='memberid_Back']",
"//input[contains(@placeholder,'Subscriber') or contains(@placeholder,'subscriber')]",
"//input[contains(@placeholder,'Member') or contains(@placeholder,'member')]",
]
subscriber_filled = False
for sel in subscriber_id_selectors:
try:
sid_input = self.driver.find_element(By.XPATH, sel)
if sid_input.is_displayed():
sid_input.clear()
sid_input.send_keys(self.memberId)
print(f"[UnitedDH Claim] step1: Subscriber ID entered: {self.memberId} (field='{sid_input.get_attribute('id')}')")
subscriber_filled = True
break
except Exception:
continue
if not subscriber_filled:
print(f"[UnitedDH Claim] step1: WARNING - Could not find Subscriber ID field")
# --- Fill Date of Birth ---
dob_formatted = self._format_dob(self.dateOfBirth)
try:
dob_input = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH,
"//input[@id='dateOfBirth_Back' or @id='dateOfBirth_Front']"
" | //input[contains(@placeholder,'Date of Birth') or contains(@placeholder,'DOB')]"
))
)
dob_input.clear()
dob_input.send_keys(dob_formatted)
print(f"[UnitedDH Claim] step1: DOB entered: {dob_formatted}")
except Exception as e:
print(f"[UnitedDH Claim] step1: Error entering DOB: {e}")
return "ERROR: step1 - Could not enter Date of Birth"
# --- Fill Procedure Date (portal pre-fills today's date — must overwrite) ---
procedure_date_formatted = self._format_dob(self.serviceDate) # YYYY-MM-DD -> MM/DD/YYYY
try:
proc_date_input = WebDriverWait(self.driver, 5).until(
EC.element_to_be_clickable((By.XPATH,
"//input[@id='procedureDate_Back' or @id='procedureDate_Front']"
" | //input[contains(@placeholder,'Procedure Date') or contains(@placeholder,'Service Date')]"
))
)
proc_date_input.click()
proc_date_input.send_keys(Keys.CONTROL + "a")
proc_date_input.send_keys(Keys.DELETE)
proc_date_input.send_keys(procedure_date_formatted)
print(f"[UnitedDH Claim] step1: Procedure Date entered: {procedure_date_formatted}")
except Exception as e:
print(f"[UnitedDH Claim] step1: Error entering Procedure Date: {e}")
time.sleep(1)
# --- Select Payer: first UnitedHealthcare Massachusetts result ---
print("[UnitedDH Claim] step1: Selecting Payer...")
payer_selected = False
try:
payer_selectors = [
"//label[contains(text(),'Payer')]/following-sibling::ng-select",
"//label[contains(text(),'Payer')]/..//ng-select",
"//ng-select[contains(@placeholder,'Payer') or contains(@placeholder,'payer')]",
"//ng-select[.//input[contains(@placeholder,'Search by Payers') or contains(@placeholder,'Payer')]]",
]
payer_ng_select = None
for sel in payer_selectors:
try:
elem = self.driver.find_element(By.XPATH, sel)
if elem.is_displayed():
payer_ng_select = elem
break
except Exception:
continue
if payer_ng_select:
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", payer_ng_select)
time.sleep(0.5)
payer_ng_select.click()
time.sleep(1)
try:
search_input = payer_ng_select.find_element(By.XPATH,
".//input[@type='text' or @role='combobox']")
search_input.clear()
search_input.send_keys("UnitedHealthcare Massachusetts")
print("[UnitedDH Claim] step1: Typed payer search text")
time.sleep(2)
except Exception:
try:
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(self.driver).send_keys("UnitedHealthcare Mass").perform()
time.sleep(2)
except Exception:
pass
# Pick the first visible matching option
payer_options = self.driver.find_elements(By.XPATH,
"//ng-dropdown-panel//div[contains(@class,'ng-option')]")
for opt in payer_options:
if opt.is_displayed():
opt_text = opt.text.strip()
if "UnitedHealthcare" in opt_text and "Massachusetts" in opt_text:
opt.click()
print(f"[UnitedDH Claim] step1: Selected Payer: {opt_text}")
payer_selected = True
break
if not payer_selected:
# Fallback: first visible option at all
for opt in payer_options:
if opt.is_displayed():
opt.click()
print(f"[UnitedDH Claim] step1: Selected first visible Payer: {opt.text.strip()}")
payer_selected = True
break
try:
from selenium.webdriver.common.action_chains import ActionChains
ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
except Exception:
pass
time.sleep(0.5)
else:
print("[UnitedDH Claim] step1: Could not find Payer ng-select element")
except Exception as e:
print(f"[UnitedDH Claim] step1: Payer selection error: {e}")
if not payer_selected:
print("[UnitedDH Claim] step1: WARNING - Could not select Payer")
time.sleep(1)
# --- Click Continue ---
try:
continue_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Continue')]"))
)
continue_btn.click()
print("[UnitedDH Claim] step1: Clicked Continue")
time.sleep(4)
error_result = self._check_for_error_dialog()
if error_result:
return error_result
except Exception as e:
return f"ERROR: step1 - Could not click Continue: {e}"
print("[UnitedDH Claim] step1: Patient search completed")
return "OK"
except Exception as e:
return f"ERROR: step1_search_patient - {e}"
def step2_open_member_page(self):
"""
After step1 clicks Continue, the portal shows a "Select Insurance" popup.
Click Ok on it, then wait for the patient info / office location page.
"""
try:
print("[UnitedDH Claim] step2: waiting for 'Select Insurance' popup")
time.sleep(2)
# Click the Ok button on the "Select Insurance" modal
try:
ok_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH,
"//button[@type='button' and contains(@class,'btn-primary') and "
"(normalize-space(text())='Ok' or normalize-space(text())='OK')]"
))
)
ok_btn.click()
print("[UnitedDH Claim] step2: Clicked Ok on Select Insurance popup")
time.sleep(2)
except TimeoutException:
print("[UnitedDH Claim] step2: Select Insurance popup not found — proceeding")
# Wait for patient info / office location page (Continue button visible)
try:
WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.XPATH,
"//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Continue')]"
))
)
print("[UnitedDH Claim] step2: Patient info page loaded (Continue button found)")
except TimeoutException:
print("[UnitedDH Claim] step2: Continue button not found — proceeding anyway")
print(f"[UnitedDH Claim] step2 URL: {self.driver.current_url}")
return "OK"
except Exception as e:
return f"ERROR: step2_open_member_page - {e}"
def step3_click_create_claim(self):
"""
Click Continue on the Practitioner & Location page.
The location is auto-filled by Angular asynchronously — wait for it to be
populated before clicking Continue, otherwise the field may submit blank.
"""
try:
print("[UnitedDH Claim] step3: waiting for Practitioner & Location page")
# Wait for the Continue button to appear (page has loaded)
continue_btn = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.XPATH,
"//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Continue')]"
))
)
# Explicit wait: hold until Angular has auto-filled the location dropdown
try:
WebDriverWait(self.driver, 10).until(
lambda d: d.find_elements(By.XPATH,
"//ng-select//span[contains(@class,'ng-value-label') and normalize-space(text())!=''] | "
"//ng-select//div[contains(@class,'ng-value') and normalize-space(.)!='']"
)
)
print("[UnitedDH Claim] step3: Location auto-filled")
except TimeoutException:
print("[UnitedDH Claim] step3: Location field did not populate in time — proceeding anyway")
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", continue_btn)
continue_btn.click()
print("[UnitedDH Claim] step3: Clicked Continue — waiting for Code Entry page")
time.sleep(3)
# Confirm we're on the Code Entry page
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, "procedureCode"))
)
print("[UnitedDH Claim] step3: Code Entry page loaded (procedureCode found)")
except TimeoutException:
print("[UnitedDH Claim] step3: procedureCode input not found — proceeding anyway")
print(f"[UnitedDH Claim] step3 URL: {self.driver.current_url}")
return "OK"
except Exception as e:
return f"ERROR: step3_click_create_claim - {e}"
def step4_fill_claim_form(self):
"""
For each service line with a procedure code:
1. (For lines after the first: click btnAddItem to start a new row)
2. Type CDT code into id="procedureCode"
3. Click id="btnAddItem" — billed amount input appears
4. Clear id="billedAmount" and enter the billed amount
5. Click the <span>Add</span> button to confirm the row
"""
try:
active_lines = [
ln for ln in self.serviceLines
if str(ln.get("procedureCode") or "").strip()
]
print(f"[UnitedDH Claim] step4: {len(active_lines)} service line(s)")
if not active_lines:
print("[UnitedDH Claim] step4: No service lines — skipping")
return "OK"
for idx, line in enumerate(active_lines):
code = str(line.get("procedureCode") or "").strip().upper()
billed = str(
line.get("totalBilled") or
line.get("billedAmount") or
line.get("fee") or ""
).strip()
print(f"[UnitedDH Claim] step4: line {idx}: code={code}, billed={billed}")
# For lines after the first, click btnAddItem to open a new procedure row
if idx > 0:
try:
add_btn = WebDriverWait(self.driver, 8).until(
EC.element_to_be_clickable((By.ID, "btnAddItem"))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn)
add_btn.click()
print(f"[UnitedDH Claim] step4: clicked btnAddItem to start row {idx}")
time.sleep(1)
except Exception as e:
print(f"[UnitedDH Claim] step4: could not click btnAddItem for row {idx}: {e}")
# Type CDT code in procedureCode input
try:
proc_input = WebDriverWait(self.driver, 8).until(
EC.element_to_be_clickable((By.ID, "procedureCode"))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_input)
proc_input.click()
proc_input.send_keys(Keys.CONTROL + "a")
proc_input.send_keys(Keys.DELETE)
proc_input.send_keys(code)
print(f"[UnitedDH Claim] step4: typed procedure code: {code}")
time.sleep(0.5)
except Exception as e:
print(f"[UnitedDH Claim] step4: could not type procedure code for row {idx}: {e}")
continue
# Click btnAddItem to confirm code and reveal billed amount input
try:
add_btn = WebDriverWait(self.driver, 8).until(
EC.element_to_be_clickable((By.ID, "btnAddItem"))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", add_btn)
add_btn.click()
print(f"[UnitedDH Claim] step4: clicked btnAddItem to reveal billedAmount for row {idx}")
time.sleep(1.5)
except Exception as e:
print(f"[UnitedDH Claim] step4: could not click btnAddItem for billed amount row {idx}: {e}")
continue
# Fill billed amount
if billed:
try:
billed_input = WebDriverWait(self.driver, 8).until(
EC.element_to_be_clickable((By.ID, "billedAmount"))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", billed_input)
billed_input.click()
billed_input.send_keys(Keys.CONTROL + "a")
billed_input.send_keys(Keys.DELETE)
billed_input.send_keys(billed)
print(f"[UnitedDH Claim] step4: entered billed amount: {billed}")
time.sleep(0.5)
except Exception as e:
print(f"[UnitedDH Claim] step4: could not fill billed amount for row {idx}: {e}")
# Click the span "Add" button to confirm the row
try:
span_add = WebDriverWait(self.driver, 8).until(
EC.element_to_be_clickable((By.XPATH,
"//span[contains(@class,'ng-star-inserted') and normalize-space(text())='Add'] | "
"//button[normalize-space(text())='Add' and not(@id='btnAddItem')] | "
"//span[normalize-space(text())='Add']"
))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", span_add)
span_add.click()
print(f"[UnitedDH Claim] step4: clicked span Add — row {idx} confirmed")
time.sleep(1)
except Exception as e:
print(f"[UnitedDH Claim] step4: could not click span Add for row {idx}: {e}")
# --- Other coverage section: click "No" (second radio button) ---
try:
print("[UnitedDH Claim] step4: selecting 'No' for Other coverage")
# The "No" option is the second radio button on the page
radio_buttons = WebDriverWait(self.driver, 8).until(
lambda d: d.find_elements(By.XPATH, "//input[@type='radio']")
)
if len(radio_buttons) >= 2:
no_radio = radio_buttons[1]
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", no_radio)
no_radio.click()
print("[UnitedDH Claim] step4: Clicked 'No' (2nd radio) for Other coverage")
else:
print(f"[UnitedDH Claim] step4: Only {len(radio_buttons)} radio button(s) found — skipping")
time.sleep(0.5)
except Exception as e:
print(f"[UnitedDH Claim] step4: Could not click 'No' for Other coverage (non-fatal): {e}")
print("[UnitedDH Claim] step4: Done filling claim form")
return "OK"
except Exception as e:
return f"ERROR: step4_fill_claim_form - {e}"
def step5_attach_files(self):
"""
If there are claim files:
1. Click the fa-caret-up dropdown icon to reveal the Add Document button
2. Click id="upload-document"
3. Send the absolute file path to the file input
"""
try:
if not self.claimFiles:
print("[UnitedDH Claim] step5: No files to attach")
return "OK"
# Open the Attached Documents section by clicking the caret-up icon
try:
caret = WebDriverWait(self.driver, 8).until(
EC.element_to_be_clickable((By.XPATH,
"//em[contains(@class,'fa-caret-up')] | "
"//i[contains(@class,'fa-caret-up')] | "
"//*[contains(@class,'fa') and contains(@class,'fa-caret-up')]"
))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", caret)
caret.click()
print("[UnitedDH Claim] step5: Clicked caret-up to expand Attached Documents")
time.sleep(1)
except Exception as e:
print(f"[UnitedDH Claim] step5: Could not click caret (section may already be open): {e}")
attached = 0
for cf in self.claimFiles:
relative_path = cf.get("filePath") or ""
if not relative_path:
print(f"[UnitedDH Claim] step5: Skipping file with no filePath: {cf}")
continue
abs_path = os.path.normpath(os.path.join(_BACKEND_CWD, relative_path.lstrip("/")))
if not os.path.isfile(abs_path):
print(f"[UnitedDH Claim] step5: File not found on disk: {abs_path}")
continue
print(f"[UnitedDH Claim] step5: Attaching: {abs_path}")
try:
# Click the Add Document button
upload_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.ID, "upload-document"))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", upload_btn)
upload_btn.click()
time.sleep(1)
# Find the file input (may be hidden) and send the path
file_input = WebDriverWait(self.driver, 8).until(
EC.presence_of_element_located((By.XPATH, "//input[@type='file']"))
)
self.driver.execute_script("arguments[0].style.display='block';", file_input)
file_input.send_keys(abs_path)
time.sleep(1.5)
print(f"[UnitedDH Claim] step5: Attached: {os.path.basename(abs_path)}")
attached += 1
except Exception as e:
print(f"[UnitedDH Claim] step5: Could not attach {abs_path}: {e}")
print(f"[UnitedDH Claim] step5: Attached {attached}/{len(self.claimFiles)} file(s)")
return "OK"
except Exception as e:
return f"ERROR: step5_attach_files - {e}"
def step6_click_next(self):
"""No separate Next step on DentalHub claim form — submission goes directly from Code Entry."""
print("[UnitedDH Claim] step6: no-op (DentalHub has no Next step before Submit)")
return "OK"
def step7_submit_claim(self):
"""
Click Submit Claim, then handle the post-submit popup:
- Top button: "Submit another claim"
- Bottom button: "View Status and History" ← click this one
"""
try:
print(f"[UnitedDH Claim] step7: submitting claim — URL: {self.driver.current_url}")
submit_btn = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.XPATH,
"//button[contains(@class,'btn-primary') and contains(normalize-space(text()),'Submit Claim')] | "
"//button[normalize-space(text())='Submit Claim'] | "
"//button[contains(normalize-space(.),'Submit Claim')]"
))
)
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_btn)
time.sleep(0.5)
submit_btn.click()
print("[UnitedDH Claim] step7: Clicked Submit Claim — waiting for post-submit popup")
time.sleep(3)
# Click "View Status and History" (the bottom button in the popup)
try:
view_btn = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.XPATH,
"//button[contains(normalize-space(.),'View Status and History')] | "
"//a[contains(normalize-space(.),'View Status and History')]"
))
)
view_btn.click()
print("[UnitedDH Claim] step7: Clicked 'View Status and History'")
time.sleep(3)
except TimeoutException:
print("[UnitedDH Claim] step7: Post-submit popup not found — proceeding to step8")
print(f"[UnitedDH Claim] step7: URL after popup: {self.driver.current_url}")
return "OK"
except Exception as e:
return f"ERROR: step7_submit_claim - {e}"
def step8_save_confirmation_pdf(self):
"""
On the Status & History page, read the claim number from the first row
(Reference Number column), then save the page as PDF.
"""
import re
try:
print("[UnitedDH Claim] step8: waiting for Status & History page")
# Wait for Status & History page to load
WebDriverWait(self.driver, 20).until(
lambda d: "status" in d.current_url.lower() or "history" in d.current_url.lower()
or d.find_elements(By.XPATH, "//td | //th[contains(text(),'Reference')]")
)
time.sleep(2)
print(f"[UnitedDH Claim] step8: Status & History URL: {self.driver.current_url}")
# Refresh so the just-submitted claim appears at the top
self.driver.refresh()
print("[UnitedDH Claim] step8: Page refreshed — waiting for table to reload")
WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.XPATH, "//table//tr[td]"))
)
time.sleep(2)
# Extract claim number from the first data row, Reference Number column
claim_number = None
try:
# The first <td> in the first data row that looks like a 14-digit reference number
first_ref = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH,
"(//table//tr[not(th)]/td[2] | "
"//table//tr[td]/td[contains(normalize-space(.),'2026') or "
" contains(normalize-space(.),'2025')])[1]"
))
)
ref_text = first_ref.text.strip()
# Reference numbers are 14-digit integers like 20260524181895
match = re.search(r'\b(\d{14})\b', ref_text)
if match:
claim_number = match.group(1)
else:
# Fallback: any 10+ digit number in the cell
match = re.search(r'\b(\d{10,})\b', ref_text)
if match:
claim_number = match.group(1)
print(f"[UnitedDH Claim] step8: Claim number from first row: {claim_number!r} (cell text: {ref_text!r})")
except Exception as e:
print(f"[UnitedDH Claim] step8: Could not read first-row reference number: {e}")
# Last-resort: scan all visible text for a 14-digit number
try:
body_text = self.driver.find_element(By.TAG_NAME, "body").text
match = re.search(r'\b(\d{14})\b', body_text)
if match:
claim_number = match.group(1)
print(f"[UnitedDH Claim] step8: Claim number (body scan): {claim_number}")
except Exception:
pass
# Save page as PDF
shared_downloads = os.path.join(_SERVICE_DIR, "downloads")
os.makedirs(shared_downloads, exist_ok=True)
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
safe_claim = ("_" + claim_number[:20]) if claim_number else ""
timestamp = time.strftime("%Y%m%d_%H%M%S")
pdf_filename = f"uniteddh_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"[UnitedDH Claim] step8: PDF saved: {pdf_path}")
except Exception as e:
print(f"[UnitedDH Claim] step8: PDF capture failed: {e}")
return f"ERROR: step8 PDF failed: {e}"
return {
"status": "success",
"pdf_path": pdf_path,
"claimNumber": claim_number,
}
except Exception as e:
return f"ERROR: step8_save_confirmation_pdf - {e}"