- Rewrote UnitedDH claim worker to navigate via eligibility page → Selected Patient → Submit Claim button flow - Updated helpers_uniteddh_claim.py step names to match new 9-step workflow - Changed payer selection in both eligibility and claim workers to type + Enter - Updated patient table column header from 'DOB / Gender' to 'DOB' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1295 lines
62 KiB
Python
1295 lines
62 KiB
Python
"""
|
|
United/DentalHub Claim Submission Worker.
|
|
Portal: app.dentalhub.com (United SCO)
|
|
|
|
Flow:
|
|
1. Navigate to eligibility page, fill member ID + DOB + payer, continue through
|
|
Select Insurance popup and Provider & Location page → land on Selected Patient page.
|
|
2. Click Submit Claim button on Selected Patient page.
|
|
3. Submit claim page is pre-filled — just click Continue.
|
|
4. Select Insurance popup — click Ok.
|
|
5. Practitioner & Location page — click Continue only (no dropdowns).
|
|
6. Date Entry page — fill CDT codes, tooth, billed amount, attach files, submit.
|
|
7. Capture claim number and PDF from Status & History page.
|
|
"""
|
|
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", "")
|
|
|
|
self.uniteddh_username = claim.get("uniteddhUsername", "")
|
|
self.uniteddh_password = claim.get("uniteddhPassword", "")
|
|
|
|
self.download_dir = get_browser_manager().download_dir
|
|
os.makedirs(self.download_dir, exist_ok=True)
|
|
|
|
def config_driver(self):
|
|
self.driver = get_browser_manager().get_driver(self.headless)
|
|
|
|
def _force_logout(self):
|
|
try:
|
|
print("[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)
|
|
|
|
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"
|
|
|
|
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
|
|
|
|
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}")
|
|
|
|
if "b2clogin.com" in current_url or "login" in current_url.lower():
|
|
print("[UnitedDH Claim login] On B2C login page - filling credentials")
|
|
|
|
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)
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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 "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):
|
|
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] 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] 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):
|
|
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):
|
|
import glob
|
|
return set(glob.glob(os.path.join(self.download_dir, "*.pdf")))
|
|
|
|
def _wait_for_new_download(self, existing_files, timeout=15):
|
|
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):
|
|
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):
|
|
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):
|
|
"""
|
|
Navigate to the eligibility page, fill member ID + DOB + payer, continue
|
|
through the Select Insurance popup and Provider & Location dropdowns to
|
|
land on the Selected Patient results page.
|
|
|
|
Mirrors the eligibility worker's step1() exactly.
|
|
"""
|
|
from selenium.webdriver.common.action_chains import ActionChains
|
|
|
|
try:
|
|
print(f"[UnitedDH Claim] step1: memberId={self.memberId}, dob={self.dateOfBirth}")
|
|
|
|
self.driver.get("https://app.dentalhub.com/app/patient/eligibility")
|
|
time.sleep(3)
|
|
print(f"[UnitedDH Claim] step1 URL: {self.driver.current_url}")
|
|
|
|
# Wait for Patient Information form
|
|
try:
|
|
WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.ID, "firstName_Back"))
|
|
)
|
|
print("[UnitedDH Claim] step1: Patient Information form loaded")
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step1: Patient Information form not found")
|
|
return "ERROR: step1 - Patient Information form not found"
|
|
|
|
# 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[@id='medicaidId_Back']",
|
|
"//label[contains(text(),'Subscriber ID')]/..//input[not(@id='firstName_Back') and not(@id='lastName_Back') and not(@id='dateOfBirth_Back')]",
|
|
"//input[contains(@placeholder,'Subscriber') or contains(@placeholder,'subscriber')]",
|
|
"//input[contains(@placeholder,'Medicaid') or contains(@placeholder,'medicaid')]",
|
|
"//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)
|
|
field_id = sid_input.get_attribute("id") or "unknown"
|
|
print(f"[UnitedDH Claim] step1: Subscriber ID entered: {self.memberId} (field='{field_id}')")
|
|
subscriber_filled = True
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
if not subscriber_filled:
|
|
try:
|
|
all_inputs = self.driver.find_elements(By.XPATH, "//form//input[@type='text' or not(@type)]")
|
|
known_ids = {'firstName_Back', 'lastName_Back', 'dateOfBirth_Back', 'procedureDate_Back', 'insurerId'}
|
|
for inp in all_inputs:
|
|
inp_id = inp.get_attribute("id") or ""
|
|
if inp_id not in known_ids and inp.is_displayed():
|
|
inp.clear()
|
|
inp.send_keys(self.memberId)
|
|
print(f"[UnitedDH Claim] step1: Subscriber ID in fallback field id='{inp_id}'")
|
|
subscriber_filled = True
|
|
break
|
|
except Exception as e2:
|
|
print(f"[UnitedDH Claim] step1: Fallback subscriber field error: {e2}")
|
|
|
|
if not subscriber_filled:
|
|
print(f"[UnitedDH Claim] step1: WARNING - Could not find Subscriber ID field")
|
|
|
|
# Fill Date of Birth
|
|
try:
|
|
dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back")
|
|
dob_input.clear()
|
|
dob_formatted = self._format_dob(self.dateOfBirth)
|
|
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"
|
|
|
|
time.sleep(1)
|
|
|
|
# Dismiss any blocking overlays (Chrome password manager etc.)
|
|
try:
|
|
self.driver.execute_script("""
|
|
var dialogs = document.querySelectorAll('[role="dialog"], .cdk-overlay-container');
|
|
dialogs.forEach(function(d) { d.style.display = 'none'; });
|
|
""")
|
|
except Exception:
|
|
pass
|
|
|
|
# Select Payer: UnitedHealthcare Massachusetts
|
|
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')]]",
|
|
]
|
|
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)
|
|
search_input = payer_ng_select.find_element(By.XPATH,
|
|
".//input[contains(@type,'text') or contains(@role,'combobox')]")
|
|
search_input.clear()
|
|
search_input.send_keys("UnitedHealthcare Massachusetts")
|
|
print("[UnitedDH Claim] step1: Typed payer search text")
|
|
time.sleep(2)
|
|
search_input.send_keys(Keys.ENTER)
|
|
print("[UnitedDH Claim] step1: Pressed Enter to select Payer")
|
|
time.sleep(0.5)
|
|
payer_selected = True
|
|
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 (Patient Info)
|
|
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 (Patient Info)")
|
|
time.sleep(3)
|
|
|
|
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}"
|
|
|
|
# Click Ok on Select Insurance popup
|
|
print("[UnitedDH Claim] step1: Checking for Select Insurance popup...")
|
|
try:
|
|
ok_btn = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//button[contains(@class,'btn-primary') and "
|
|
"(normalize-space(.)='Ok' or normalize-space(.)='OK' or normalize-space(.)='Okay')] | "
|
|
"//modal-container//button[normalize-space(.)='Ok' or normalize-space(.)='OK' or normalize-space(.)='Okay'] | "
|
|
"//div[contains(@class,'modal')]//button[normalize-space(.)='Ok' or normalize-space(.)='OK' or normalize-space(.)='Okay']"
|
|
))
|
|
)
|
|
try:
|
|
self.driver.execute_script("arguments[0].click();", ok_btn)
|
|
print("[UnitedDH Claim] step1: Clicked OK on Select Insurance popup (JS)")
|
|
except Exception:
|
|
ok_btn.click()
|
|
print("[UnitedDH Claim] step1: Clicked OK on Select Insurance popup (direct)")
|
|
try:
|
|
WebDriverWait(self.driver, 10).until(EC.staleness_of(ok_btn))
|
|
print("[UnitedDH Claim] step1: Select Insurance modal closed")
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step1: Modal staleness timeout — continuing anyway")
|
|
WebDriverWait(self.driver, 5).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//label[@for='treatmentLocation'] | //label[@for='paymentGroupId']"))
|
|
)
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step1: Select Insurance popup not found — proceeding")
|
|
|
|
# Provider & Location page — select Treatment Location and Billing Entity, then Continue
|
|
print("[UnitedDH Claim] step1: Waiting for Provider & Location page...")
|
|
try:
|
|
WebDriverWait(self.driver, 20).until(
|
|
EC.visibility_of_element_located((By.XPATH, "//label[@for='paymentGroupId']"))
|
|
)
|
|
|
|
print("[UnitedDH Claim] step1: Selecting Treatment Location...")
|
|
location_selected = False
|
|
try:
|
|
location_ng = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//label[@for='treatmentLocation']/following-sibling::ng-select | "
|
|
"//label[@for='treatmentLocation']/..//ng-select"
|
|
))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", location_ng)
|
|
arrow = location_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper")
|
|
arrow.click()
|
|
first_option = WebDriverWait(self.driver, 5).until(
|
|
EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option"))
|
|
)
|
|
option_text = first_option.text.strip()
|
|
first_option.click()
|
|
print(f"[UnitedDH Claim] step1: Selected Treatment Location: {option_text}")
|
|
location_selected = True
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step1: Treatment Location selection failed: {e}")
|
|
|
|
if not location_selected:
|
|
print("[UnitedDH Claim] step1: WARNING - Could not select Treatment Location")
|
|
|
|
print("[UnitedDH Claim] step1: Selecting Billing Entity...")
|
|
billing_selected = False
|
|
try:
|
|
billing_ng = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH,
|
|
"//label[@for='paymentGroupId']/following-sibling::ng-select | "
|
|
"//label[@for='paymentGroupId']/..//ng-select"
|
|
))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", billing_ng)
|
|
arrow = billing_ng.find_element(By.CSS_SELECTOR, ".ng-arrow-wrapper")
|
|
arrow.click()
|
|
first_option = WebDriverWait(self.driver, 5).until(
|
|
EC.element_to_be_clickable((By.CSS_SELECTOR, ".ng-dropdown-panel .ng-option"))
|
|
)
|
|
option_text = first_option.text.strip()
|
|
first_option.click()
|
|
print(f"[UnitedDH Claim] step1: Selected Billing Entity: {option_text}")
|
|
billing_selected = True
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step1: Billing Entity selection failed: {e}")
|
|
|
|
if not billing_selected:
|
|
print("[UnitedDH Claim] step1: WARNING - Could not select Billing Entity")
|
|
|
|
continue_btn2 = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Continue')]"))
|
|
)
|
|
continue_btn2.click()
|
|
print("[UnitedDH Claim] step1: Clicked Continue (Provider & Location) → Selected Patient page")
|
|
time.sleep(5)
|
|
|
|
except TimeoutException:
|
|
try:
|
|
results_elem = self.driver.find_element(By.XPATH,
|
|
"//*[contains(text(),'Selected Patient') or contains(@id,'patient-name') or contains(@id,'eligibility')]"
|
|
)
|
|
if results_elem.is_displayed():
|
|
print("[UnitedDH Claim] step1: Already on Selected Patient page")
|
|
return "OK"
|
|
except Exception:
|
|
pass
|
|
print("[UnitedDH Claim] step1: Continue not found on Provider & Location page — proceeding")
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step1: Error clicking Continue on Provider & Location page: {e}")
|
|
error_result = self._check_for_error_dialog()
|
|
if error_result:
|
|
return error_result
|
|
|
|
error_result = self._check_for_error_dialog()
|
|
if error_result:
|
|
return error_result
|
|
|
|
print("[UnitedDH Claim] step1: Patient search complete — on Selected Patient page")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step1_search_patient - {e}"
|
|
|
|
def step2_click_submit_claim(self):
|
|
"""
|
|
On the Selected Patient results page, click the Submit Claim button
|
|
(id="btnSubmitClaim").
|
|
"""
|
|
try:
|
|
print("[UnitedDH Claim] step2: Looking for Submit Claim button (id='btnSubmitClaim')...")
|
|
time.sleep(2)
|
|
|
|
submit_claim_btn = WebDriverWait(self.driver, 15).until(
|
|
EC.element_to_be_clickable((By.ID, "btnSubmitClaim"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", submit_claim_btn)
|
|
time.sleep(0.5)
|
|
submit_claim_btn.click()
|
|
print("[UnitedDH Claim] step2: Clicked Submit Claim button")
|
|
time.sleep(4)
|
|
|
|
print(f"[UnitedDH Claim] step2 URL: {self.driver.current_url}")
|
|
return "OK"
|
|
|
|
except TimeoutException:
|
|
# Fallback: find by text
|
|
try:
|
|
btn = self.driver.find_element(By.XPATH,
|
|
"//button[contains(normalize-space(.),'Submit Claim') and not(contains(@class,'btn-primary'))]"
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", btn)
|
|
btn.click()
|
|
print("[UnitedDH Claim] step2: Clicked Submit Claim button (text fallback)")
|
|
time.sleep(4)
|
|
return "OK"
|
|
except Exception as e2:
|
|
return f"ERROR: step2 - Could not click Submit Claim button: {e2}"
|
|
except Exception as e:
|
|
return f"ERROR: step2_click_submit_claim - {e}"
|
|
|
|
def step3_continue_prefilled(self):
|
|
"""
|
|
Submit Claim page: member ID and DOB are pre-filled.
|
|
Select Payer by typing "UnitedHealthcare Massachusetts" + Enter, then click Continue.
|
|
"""
|
|
try:
|
|
print("[UnitedDH Claim] step3: Submit Claim page — selecting Payer then clicking Continue...")
|
|
|
|
# Wait for the form to load
|
|
WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.XPATH,
|
|
"//label[contains(text(),'Payer')] | //ng-select"
|
|
))
|
|
)
|
|
|
|
# Select Payer: type + Enter
|
|
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')]]",
|
|
]
|
|
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)
|
|
search_input = payer_ng_select.find_element(By.XPATH,
|
|
".//input[contains(@type,'text') or contains(@role,'combobox')]")
|
|
search_input.clear()
|
|
search_input.send_keys("UnitedHealthcare Massachusetts")
|
|
print("[UnitedDH Claim] step3: Typed payer search text")
|
|
time.sleep(2)
|
|
search_input.send_keys(Keys.ENTER)
|
|
print("[UnitedDH Claim] step3: Pressed Enter to select Payer")
|
|
time.sleep(0.5)
|
|
payer_selected = True
|
|
else:
|
|
print("[UnitedDH Claim] step3: Could not find Payer ng-select element")
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step3: Payer selection error: {e}")
|
|
|
|
if not payer_selected:
|
|
print("[UnitedDH Claim] step3: WARNING - Could not select Payer")
|
|
|
|
time.sleep(1)
|
|
|
|
continue_btn = WebDriverWait(self.driver, 15).until(
|
|
EC.element_to_be_clickable((By.XPATH, "//button[contains(text(),'Continue')]"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", continue_btn)
|
|
continue_btn.click()
|
|
print("[UnitedDH Claim] step3: Clicked Continue")
|
|
time.sleep(4)
|
|
|
|
error_result = self._check_for_error_dialog()
|
|
if error_result:
|
|
return error_result
|
|
|
|
print(f"[UnitedDH Claim] step3 URL: {self.driver.current_url}")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step3_continue_prefilled - {e}"
|
|
|
|
def step4_select_insurance_ok(self):
|
|
"""
|
|
Click Ok on the Select Insurance popup.
|
|
"""
|
|
try:
|
|
print("[UnitedDH Claim] step4: Waiting for Select Insurance popup...")
|
|
|
|
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')] | "
|
|
"//modal-container//button[normalize-space(.)='Ok' or normalize-space(.)='OK'] | "
|
|
"//div[contains(@class,'modal')]//button[normalize-space(.)='Ok' or normalize-space(.)='OK']"
|
|
))
|
|
)
|
|
ActionChains(self.driver).move_to_element(ok_btn).pause(0.5).click().perform()
|
|
print("[UnitedDH Claim] step4: Clicked Ok on Select Insurance popup")
|
|
try:
|
|
WebDriverWait(self.driver, 8).until(EC.staleness_of(ok_btn))
|
|
print("[UnitedDH Claim] step4: Select Insurance modal closed")
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step4: Modal staleness timeout — continuing anyway")
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step4: Select Insurance popup not found — proceeding")
|
|
|
|
print(f"[UnitedDH Claim] step4 URL: {self.driver.current_url}")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step4_select_insurance_ok - {e}"
|
|
|
|
def step5_practitioner_continue(self):
|
|
"""
|
|
Practitioner & Location page — click Continue only, no dropdown selections.
|
|
"""
|
|
try:
|
|
print("[UnitedDH Claim] step5: Waiting for Practitioner & Location page...")
|
|
|
|
# Wait for the page to render (either treatmentLocation or paymentGroupId label)
|
|
try:
|
|
WebDriverWait(self.driver, 20).until(
|
|
EC.visibility_of_element_located((By.XPATH,
|
|
"//label[@for='treatmentLocation'] | //label[@for='paymentGroupId'] | "
|
|
"//label[@for='paymentGroup']"
|
|
))
|
|
)
|
|
print("[UnitedDH Claim] step5: Practitioner & Location page loaded")
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step5: Practitioner & Location labels not found — trying Continue anyway")
|
|
|
|
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')] | "
|
|
"//button[contains(normalize-space(text()),'Continue')]"
|
|
))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", continue_btn)
|
|
continue_btn.click()
|
|
print("[UnitedDH Claim] step5: Clicked Continue — waiting for Code Entry page")
|
|
time.sleep(3)
|
|
|
|
try:
|
|
WebDriverWait(self.driver, 10).until(
|
|
EC.presence_of_element_located((By.ID, "procedureCode"))
|
|
)
|
|
print("[UnitedDH Claim] step5: Code Entry page loaded (procedureCode found)")
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step5: procedureCode input not found — proceeding anyway")
|
|
|
|
print(f"[UnitedDH Claim] step5 URL: {self.driver.current_url}")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step5_practitioner_continue - {e}"
|
|
|
|
def step6_fill_claim_form(self):
|
|
"""
|
|
For each service line with a procedure code:
|
|
1. Click btnAddItem to open/activate the row
|
|
2. Type CDT code into id="procedureCode"
|
|
3. Click id="btnAddItem" — billed amount input appears
|
|
4. Fill tooth number (if provided)
|
|
5. Click surface boxes (if provided)
|
|
6. Fill id="billedAmount"
|
|
7. Click the span "Add" button to confirm the row
|
|
Then select "No" for Other coverage.
|
|
"""
|
|
try:
|
|
active_lines = [
|
|
ln for ln in self.serviceLines
|
|
if str(ln.get("procedureCode") or "").strip()
|
|
]
|
|
print(f"[UnitedDH Claim] step6: {len(active_lines)} service line(s)")
|
|
|
|
if not active_lines:
|
|
print("[UnitedDH Claim] step6: 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()
|
|
tooth = str(line.get("toothNumber") or line.get("tooth_number") or "").strip()
|
|
surface = str(line.get("toothSurface") or line.get("tooth_surface") or "").strip().upper()
|
|
print(f"[UnitedDH Claim] step6: line {idx}: code={code}, billed={billed}, tooth={tooth}, surface={surface}")
|
|
|
|
# Click btnAddItem to open/activate the procedure row
|
|
try:
|
|
add_btn = WebDriverWait(self.driver, 10).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] step6: clicked btnAddItem to open row {idx}")
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: could not click btnAddItem to open row {idx}: {e}")
|
|
|
|
# Type CDT code
|
|
try:
|
|
proc_input = WebDriverWait(self.driver, 10).until(
|
|
EC.element_to_be_clickable((By.ID, "procedureCode"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", proc_input)
|
|
self.driver.execute_script("arguments[0].click();", proc_input)
|
|
proc_input.send_keys(Keys.CONTROL + "a")
|
|
proc_input.send_keys(Keys.DELETE)
|
|
proc_input.send_keys(code)
|
|
print(f"[UnitedDH Claim] step6: typed procedure code: {code}")
|
|
time.sleep(0.5)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: 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] step6: clicked btnAddItem to reveal billedAmount for row {idx}")
|
|
time.sleep(1.5)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: could not click btnAddItem for billed amount row {idx}: {e}")
|
|
continue
|
|
|
|
# Fill tooth number
|
|
if tooth:
|
|
try:
|
|
tooth_input = WebDriverWait(self.driver, 5).until(
|
|
EC.element_to_be_clickable((By.ID, "tooth"))
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", tooth_input)
|
|
tooth_input.click()
|
|
tooth_input.send_keys(Keys.CONTROL + "a")
|
|
tooth_input.send_keys(Keys.DELETE)
|
|
tooth_input.send_keys(tooth)
|
|
print(f"[UnitedDH Claim] step6: entered tooth number: {tooth}")
|
|
time.sleep(0.3)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: could not fill tooth number for row {idx}: {e}")
|
|
|
|
# Click surface boxes
|
|
if surface:
|
|
try:
|
|
surface_boxes = self.driver.find_elements(By.XPATH,
|
|
"//div[contains(@class,'claim-add-item-group__box')]")
|
|
if surface_boxes:
|
|
for letter in surface:
|
|
if not letter.strip():
|
|
continue
|
|
try:
|
|
box = self.driver.find_element(By.XPATH,
|
|
f"//div[contains(@class,'claim-add-item-group__box') "
|
|
f"and not(contains(@class,'--disabled')) "
|
|
f"and @id='{letter}']"
|
|
)
|
|
self.driver.execute_script("arguments[0].scrollIntoView({block:'center'});", box)
|
|
box.click()
|
|
print(f"[UnitedDH Claim] step6: clicked surface '{letter}'")
|
|
time.sleep(0.2)
|
|
except Exception:
|
|
print(f"[UnitedDH Claim] step6: surface '{letter}' not found or disabled")
|
|
else:
|
|
print(f"[UnitedDH Claim] step6: no surface boxes on page — skipping")
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: surface click error for row {idx}: {e}")
|
|
|
|
# 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] step6: entered billed amount: {billed}")
|
|
time.sleep(0.5)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: 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] step6: clicked span Add — row {idx} confirmed")
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: could not click span Add for row {idx}: {e}")
|
|
|
|
# Other coverage: click "No" (second radio button)
|
|
try:
|
|
print("[UnitedDH Claim] step6: selecting 'No' for Other coverage")
|
|
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] step6: Clicked 'No' (2nd radio) for Other coverage")
|
|
else:
|
|
print(f"[UnitedDH Claim] step6: Only {len(radio_buttons)} radio button(s) found — skipping")
|
|
time.sleep(0.5)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step6: Could not click 'No' for Other coverage (non-fatal): {e}")
|
|
|
|
print("[UnitedDH Claim] step6: Done filling claim form")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step6_fill_claim_form - {e}"
|
|
|
|
def step7_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] step7: No files to attach")
|
|
return "OK"
|
|
|
|
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] step7: Clicked caret-up to expand Attached Documents")
|
|
time.sleep(1)
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step7: 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] step7: 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] step7: File not found on disk: {abs_path}")
|
|
continue
|
|
|
|
print(f"[UnitedDH Claim] step7: Attaching: {abs_path}")
|
|
try:
|
|
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)
|
|
|
|
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] step7: Attached: {os.path.basename(abs_path)}")
|
|
attached += 1
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step7: Could not attach {abs_path}: {e}")
|
|
|
|
print(f"[UnitedDH Claim] step7: Attached {attached}/{len(self.claimFiles)} file(s)")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step7_attach_files - {e}"
|
|
|
|
def step8_submit_claim(self):
|
|
"""
|
|
Click Submit Claim on the Code Entry page, then click
|
|
"View Status and History" on the post-submit popup.
|
|
"""
|
|
try:
|
|
print(f"[UnitedDH Claim] step8: 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] step8: Clicked Submit Claim — waiting for post-submit popup")
|
|
time.sleep(3)
|
|
|
|
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] step8: Clicked 'View Status and History'")
|
|
time.sleep(3)
|
|
except TimeoutException:
|
|
print("[UnitedDH Claim] step8: Post-submit popup not found — proceeding to step9")
|
|
|
|
print(f"[UnitedDH Claim] step8: URL after popup: {self.driver.current_url}")
|
|
return "OK"
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step8_submit_claim - {e}"
|
|
|
|
def step9_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] step9: waiting for Status & History page")
|
|
|
|
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] step9: Status & History URL: {self.driver.current_url}")
|
|
|
|
self.driver.refresh()
|
|
print("[UnitedDH Claim] step9: 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)
|
|
|
|
claim_number = None
|
|
try:
|
|
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()
|
|
match = re.search(r'\b(\d{14})\b', ref_text)
|
|
if match:
|
|
claim_number = match.group(1)
|
|
else:
|
|
match = re.search(r'\b(\d{10,})\b', ref_text)
|
|
if match:
|
|
claim_number = match.group(1)
|
|
print(f"[UnitedDH Claim] step9: Claim number: {claim_number!r} (cell: {ref_text!r})")
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step9: Could not read first-row reference number: {e}")
|
|
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] step9: Claim number (body scan): {claim_number}")
|
|
except Exception:
|
|
pass
|
|
|
|
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] step9: PDF saved: {pdf_path}")
|
|
except Exception as e:
|
|
print(f"[UnitedDH Claim] step9: PDF capture failed: {e}")
|
|
return f"ERROR: step9 PDF failed: {e}"
|
|
|
|
self._hide_browser()
|
|
|
|
return {
|
|
"status": "success",
|
|
"pdf_path": pdf_path,
|
|
"claimNumber": claim_number,
|
|
}
|
|
|
|
except Exception as e:
|
|
return f"ERROR: step9_save_confirmation_pdf - {e}"
|
|
|
|
# ── Main workflow ──────────────────────────────────────────────────────────
|
|
|
|
def main_workflow(self, url):
|
|
try:
|
|
self.config_driver()
|
|
|
|
login_result = self.login(url)
|
|
print(f"[main_workflow] Login result: {login_result}")
|
|
|
|
if login_result == "OTP_REQUIRED":
|
|
return {"status": "otp_required", "message": "OTP required after login"}
|
|
|
|
if isinstance(login_result, str) and login_result.startswith("ERROR"):
|
|
return {"status": "error", "message": login_result}
|
|
|
|
# Step 1: eligibility-style patient search → Selected Patient page
|
|
step1_result = self.step1_search_patient()
|
|
print(f"[main_workflow] step1 result: {step1_result}")
|
|
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step1_result}
|
|
|
|
# Step 2: click Submit Claim on Selected Patient page
|
|
step2_result = self.step2_click_submit_claim()
|
|
print(f"[main_workflow] step2 result: {step2_result}")
|
|
if isinstance(step2_result, str) and step2_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step2_result}
|
|
|
|
# Step 3: pre-filled Submit Claim page — just Continue
|
|
step3_result = self.step3_continue_prefilled()
|
|
print(f"[main_workflow] step3 result: {step3_result}")
|
|
if isinstance(step3_result, str) and step3_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step3_result}
|
|
|
|
# Step 4: Select Insurance popup — click Ok
|
|
step4_result = self.step4_select_insurance_ok()
|
|
print(f"[main_workflow] step4 result: {step4_result}")
|
|
if isinstance(step4_result, str) and step4_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step4_result}
|
|
|
|
# Step 5: Practitioner & Location — click Continue only
|
|
step5_result = self.step5_practitioner_continue()
|
|
print(f"[main_workflow] step5 result: {step5_result}")
|
|
if isinstance(step5_result, str) and step5_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step5_result}
|
|
|
|
# Step 6: fill CDT codes, tooth, billed amount
|
|
step6_result = self.step6_fill_claim_form()
|
|
print(f"[main_workflow] step6 result: {step6_result}")
|
|
if isinstance(step6_result, str) and step6_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step6_result}
|
|
|
|
# Step 7: attach files (if any)
|
|
step7_result = self.step7_attach_files()
|
|
print(f"[main_workflow] step7 result: {step7_result}")
|
|
if isinstance(step7_result, str) and step7_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step7_result}
|
|
|
|
# Step 8: click Submit Claim, then View Status and History
|
|
step8_result = self.step8_submit_claim()
|
|
print(f"[main_workflow] step8 result: {step8_result}")
|
|
if isinstance(step8_result, str) and step8_result.startswith("ERROR"):
|
|
return {"status": "error", "message": step8_result}
|
|
|
|
# Step 9: capture claim number and PDF
|
|
step9_result = self.step9_save_confirmation_pdf()
|
|
print(f"[main_workflow] step9 result: {step9_result}")
|
|
return step9_result
|
|
|
|
except Exception as e:
|
|
return {"status": "error", "message": str(e)}
|