feat: Delta Indiv eligibility on coversme portal and ADA paper claim/preauth

Eligibility: rebuilt the Delta Indiv worker for my.deltadentalcoversme.com —
Dentist tab -> Universal Delta Sign In -> username/Next -> password -> SIGN IN,
confirmation-code (MFA) page detected by URL so the app's OTP popup shows,
then Search for Patient -> Last Name + DOB -> Submit. On the "Benefits for"
details page it reads the patient name, Subscriber # and Eligible through
(active while that date is today or later) and saves the PDF downloaded by
the Print link. Session persists: login is skipped while Search for Patient
is reachable, and Delta Indiv cookies are no longer wiped on agent startup.
The button needs Last Name + DOB (member ID optional; backend falls back to
the scraped Subscriber #). Chatbot gains a Delta Indiv option and aliases.

Claims: Delta Indiv uses the same 2024 ADA form as BCBS with the payer
address Delta Dental, P.O. Box 103, Stevens Point, WI 54481-0103. Adds Delta
Indiv Claim/PreAuth (ADA Form) buttons, Claim All and AI queue routing, Print
ADA Form in the claim viewer, a Delta Indiv fee schedule (copied from BCBS),
and Delta Indiv as its own insurance type so it is no longer treated as
Deltains.

Also renames eligibility buttons: Delta Indiv (Last name, DOB or ID),
Metlife Dental (ID or SSN), Altus (ID, First name, DOB).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 22:03:30 -04:00
co-authored by Claude Opus 5.5
parent a5beeb7ba9
commit 0082b7bd53
19 changed files with 714 additions and 462 deletions
@@ -11,9 +11,26 @@ import os
import re
import base64
import calendar
from datetime import datetime, date
from deltaindiv_browser_manager import get_browser_manager
# Provider Dashboard "Search for Patient" button (Individual and Family plans)
SEARCH_FOR_PATIENT_XPATH = "//a[@data-test-id='SearchForPatientButton']"
SEARCH_DASHBOARD_URL = "https://my.deltadentalcoversme.com/private/provider/dashboard"
# deltadentalma.com/self-service/mfa page: "Confirmation Code" input + CONFIRM button
OTP_INPUT_XPATH = (
"//input[@data-test-id='MfaCode']"
" | //label[normalize-space()='Confirmation Code']/preceding::input[not(@type='hidden')][1]"
" | //label[normalize-space()='Confirmation Code']/following::input[not(@type='hidden')][1]"
" | //label[normalize-space()='Confirmation Code']/..//input[not(@type='hidden')]"
)
OTP_CONFIRM_XPATH = (
"//button[@data-test-id='ConfirmSubmitButton']"
" | //button[translate(normalize-space(), 'confirm', 'CONFIRM')='CONFIRM']"
)
class AutomationDeltaIndivEligibilityCheck:
def __init__(self, data):
self.headless = False
@@ -46,7 +63,7 @@ class AutomationDeltaIndivEligibilityCheck:
# Try to click logout button if visible
try:
self.driver.get("https://www.deltadentalma.com/")
self.driver.get("https://my.deltadentalcoversme.com/")
time.sleep(2)
logout_selectors = [
@@ -95,58 +112,27 @@ class AutomationDeltaIndivEligibilityCheck:
self.driver.get(url)
time.sleep(2)
# Check if already on a logged-in page (persistent session from profile)
# Persistent session: if the dashboard still shows "Search for Patient",
# we're logged in — skip the sign-in / OTP flow entirely.
print(f"[login] Current URL: {self.driver.current_url}")
if self.driver.find_elements(By.XPATH, SEARCH_FOR_PATIENT_XPATH):
print("[login] Search for Patient already visible - ALREADY_LOGGED_IN")
return "ALREADY_LOGGED_IN"
try:
current_url = self.driver.current_url
print(f"[login] Current URL: {current_url}")
logged_in_patterns = ["member", "dashboard", "eligibility", "search", "patients", "private/provider"]
is_logged_in_url = any(pattern in current_url.lower() for pattern in logged_in_patterns)
if is_logged_in_url and "sign-in" not in current_url.lower():
print("[login] Already on logged-in page - skipping login entirely")
if "member" not in current_url.lower():
try:
member_search = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
)
print("[login] Found member search input - returning ALREADY_LOGGED_IN")
return "ALREADY_LOGGED_IN"
except TimeoutException:
members_url = "https://www.deltadentalma.com/private/provider/dashboard"
print(f"[login] Navigating to members page: {members_url}")
self.driver.get(members_url)
time.sleep(2)
try:
member_search = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
)
print("[login] Member search found - ALREADY_LOGGED_IN")
return "ALREADY_LOGGED_IN"
except TimeoutException:
print("[login] Could not find member search, will try login")
except Exception as e:
print(f"[login] Error checking current state: {e}")
print(f"[login] Checking session via dashboard: {SEARCH_DASHBOARD_URL}")
self.driver.get(SEARCH_DASHBOARD_URL)
WebDriverWait(self.driver, 8).until(
EC.presence_of_element_located((By.XPATH, SEARCH_FOR_PATIENT_XPATH))
)
print("[login] Session still valid - ALREADY_LOGGED_IN")
return "ALREADY_LOGGED_IN"
except TimeoutException:
print(f"[login] Not logged in (landed on {self.driver.current_url}) - signing in")
# Navigate to login URL
self.driver.get(url)
time.sleep(2)
# Check if session redirected us straight to member search
try:
current_url = self.driver.current_url
print(f"[login] URL after navigation: {current_url}")
if "sign-in" not in current_url.lower():
member_search = WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
)
if member_search:
print("[login] Session valid - skipping login")
return "ALREADY_LOGGED_IN"
except TimeoutException:
print("[login] Proceeding with login")
print(f"[login] URL after navigation: {self.driver.current_url}")
# Dismiss any "Authentication flow continued in another tab" modal
modal_dismissed = False
@@ -173,7 +159,7 @@ class AutomationDeltaIndivEligibilityCheck:
try:
otp_candidate = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located(
(By.XPATH, "//input[@data-test-id='MfaCode']")
(By.XPATH, OTP_INPUT_XPATH)
)
)
if otp_candidate:
@@ -190,7 +176,7 @@ class AutomationDeltaIndivEligibilityCheck:
time.sleep(2)
try:
member_search = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
EC.presence_of_element_located((By.XPATH, "//a[@data-test-id='SearchForPatientButton' or contains(@href, 'check-patient-benefits-search')]"))
)
if member_search:
print("[login] Already authenticated after modal dismiss")
@@ -198,77 +184,130 @@ class AutomationDeltaIndivEligibilityCheck:
except TimeoutException:
pass
# Fill login form
# coversme sign-in page may open on the "Member" tab — click "Dentist" first
# so the "Universal Delta Sign In" link appears
try:
email_field = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//input[@data-test-id='Username']"))
dentist_tab = WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((
By.XPATH, "//span[contains(@class,'mainLabel') and normalize-space()='Dentist']"
)))
dentist_tab.click()
print("[login] Clicked Dentist tab")
time.sleep(1)
except TimeoutException:
print("[login] Dentist tab not found - continuing")
# coversme sign-in page: click "Universal Delta Sign In" to reach the DDPA login form
try:
universal_btn = WebDriverWait(self.driver, 10).until(
EC.element_to_be_clickable((By.XPATH, "//a[@data-test-id='DdpaLoginLink']"))
)
universal_btn.click()
print("[login] Clicked Universal Delta Sign In")
time.sleep(2)
except TimeoutException:
if "deltadentalcoversme.com/sign-in" in self.driver.current_url.lower():
# Still on coversme — don't fall through to the Member tab's username form
print("[login] Universal Delta Sign In link not found on coversme sign-in page")
return "ERROR:LOGIN FAILED: Universal Delta Sign In link not found"
print("[login] Universal Delta Sign In link not found - assuming already on login form")
# auth.deltadental.com "Have we met?" page: enter username, then click Next
username_xpath = (
"//input[@id='usernameInput' or @name='username' or @data-test-id='Username']"
)
try:
username_field = WebDriverWait(self.driver, 15).until(
EC.element_to_be_clickable((By.XPATH, username_xpath))
)
except TimeoutException:
print("[login] Could not find login form - page may have changed")
print(f"[login] Could not find username field - page may have changed. URL: {self.driver.current_url}")
return "ERROR: Login form not found"
email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@data-test-id='Username']")))
email_field.clear()
email_field.send_keys(self.deltaindiv_username)
username_field.clear()
username_field.send_keys(self.deltaindiv_username)
print("[login] Entered username")
password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@data-test-id='Password']")))
# Next is an <a id="verify-user">; aria-disabled flips to "false" once a username is typed
next_button = wait.until(EC.element_to_be_clickable((
By.XPATH, "//a[@id='verify-user' and @aria-disabled='false']"
)))
next_button.click()
print("[login] Clicked Next")
time.sleep(2)
# dp-prod-ddma auth0 page: username is prefilled, enter password, then SIGN IN
password_field = wait.until(EC.element_to_be_clickable((By.XPATH, "//input[@id='password']")))
password_field.clear()
password_field.send_keys(self.deltaindiv_password)
print("[login] Entered password")
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@data-test-id='SigninSubmitButton']")))
login_button.click()
try:
login_button = WebDriverWait(self.driver, 5).until(EC.element_to_be_clickable((
By.XPATH,
"//button[translate(normalize-space(), 'sign', 'SIGN')='SIGN IN' or @type='submit']"
)))
login_button.click()
print("[login] Clicked SIGN IN")
except TimeoutException:
password_field.send_keys(Keys.RETURN)
print("[login] SIGN IN button not found - submitted with Enter")
# Save credentials hash after login attempt
if self.deltaindiv_username:
browser_manager.save_credentials_hash(self.deltaindiv_username)
# OTP detection — wait up to 30 seconds for OTP input
# After SIGN IN wait (up to 30s) for whichever page comes next:
# confirmation-code (MFA) page or the dashboard.
def next_page(d):
url = d.current_url.lower()
if "self-service/mfa" in url or d.find_elements(By.XPATH, OTP_INPUT_XPATH):
return "otp"
if d.find_elements(By.XPATH, SEARCH_FOR_PATIENT_XPATH):
return "dashboard"
return False
try:
otp_candidate = WebDriverWait(self.driver, 30).until(
EC.presence_of_element_located(
(By.XPATH, "//input[@data-test-id='MfaCode']")
)
)
if otp_candidate:
print("[login] OTP input detected -> OTP_REQUIRED")
return "OTP_REQUIRED"
landed = WebDriverWait(self.driver, 30).until(next_page)
except TimeoutException:
print("[login] No OTP input detected in allowed time.")
# Check if we're now on the member search page (login succeeded without OTP)
try:
current_url = self.driver.current_url.lower()
if "member" in current_url or "dashboard" in current_url:
member_search = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, "//a[contains(@href, 'check-patient-benefits-search')]"))
)
print("[login] Login successful - now on member search page")
return "SUCCESS"
except TimeoutException:
pass
landed = None
print(f"[login] After SIGN IN: {landed or 'nothing recognised'} — URL: {self.driver.current_url}")
# Check for error messages
try:
error_elem = WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(@class,'error') or contains(text(),'invalid') or contains(text(),'failed')]"))
)
print(f"[login] Login failed - error detected: {error_elem.text}")
return f"ERROR:LOGIN FAILED: {error_elem.text}"
except TimeoutException:
pass
if landed == "otp":
print("[login] Confirmation code page detected -> OTP_REQUIRED")
return "OTP_REQUIRED"
if "sign-in" in self.driver.current_url.lower():
print("[login] Login failed - still on login page")
return "ERROR:LOGIN FAILED: Still on login page"
print("[login] Assuming login succeeded (no errors detected)")
if landed == "dashboard":
print("[login] Login successful - Search for Patient visible")
return "SUCCESS"
# Only a visible, non-empty error message counts as a failed login
for el in self.driver.find_elements(
By.XPATH,
"//*[contains(@class,'error') or contains(@class,'alert-danger') or @role='alert']"
):
try:
text = el.text.strip()
if text and el.is_displayed():
print(f"[login] Login failed - error detected: {text}")
return f"ERROR:LOGIN FAILED: {text}"
except Exception:
continue
if "sign-in" in self.driver.current_url.lower() or self.driver.find_elements(
By.XPATH, "//input[@id='password']"
):
print("[login] Login failed - still on login page")
return "ERROR:LOGIN FAILED: Still on login page"
print("[login] Assuming login succeeded (no errors detected)")
return "SUCCESS"
except Exception as e:
print("[login] Exception during login:", e)
return f"ERROR:LOGIN FAILED: {e}"
def step1(self):
"""Open the Group/Health Connector search page and search by Subscriber ID + Birthdate."""
"""Click Search for Patient on the dashboard and search by Last Name + Date of Birth."""
wait = WebDriverWait(self.driver, 30)
try:
@@ -279,113 +318,100 @@ class AutomationDeltaIndivEligibilityCheck:
fields.append(f"DOB: {self.dateOfBirth}")
print(f"[Delta Indiv step1] Starting search with: {', '.join(fields)}")
search_page_url = "https://www.deltadentalma.com/private/provider/check-patient-benefits-search"
# Try navigating straight to the search page first — avoids any click/visibility issues
print(f"[Delta Indiv step1] Navigating directly to search page: {search_page_url}")
self.driver.get(search_page_url)
time.sleep(2)
if "check-patient-benefits-search" not in self.driver.current_url.lower():
# Direct deep-link didn't stick (e.g. bounced back to sign-in/dashboard) —
# fall back to clicking the Group/Health Connector link from the dashboard.
print(f"[Delta Indiv step1] Direct navigation redirected to: {self.driver.current_url} — falling back to clicking the link")
if "private/provider/dashboard" not in self.driver.current_url.lower():
dashboard_url = "https://www.deltadentalma.com/private/provider/dashboard"
print(f"[Delta Indiv step1] Navigating to dashboard: {dashboard_url}")
self.driver.get(dashboard_url)
# Provider Dashboard -> "Search for Patient" (Individual and Family plans).
# Look for the button wherever login landed; only navigate if it isn't there.
search_link = None
for nav_url in (None, SEARCH_DASHBOARD_URL, "https://my.deltadentalcoversme.com/"):
if nav_url:
print(f"[Delta Indiv step1] Search for Patient not found — navigating to {nav_url}")
self.driver.get(nav_url)
time.sleep(2)
link_xpath = "//a[contains(@href, 'check-patient-benefits-search')]"
try:
connector_link = WebDriverWait(self.driver, 20).until(
EC.element_to_be_clickable((By.XPATH, link_xpath))
search_link = WebDriverWait(self.driver, 5 if nav_url is None else 15).until(
EC.presence_of_element_located((By.XPATH, SEARCH_FOR_PATIENT_XPATH))
)
connector_link.click()
break
except TimeoutException:
# Element may exist but be hidden (e.g. a responsive duplicate) — try any match via JS click
candidates = self.driver.find_elements(By.XPATH, link_xpath)
print(f"[Delta Indiv step1] element_to_be_clickable timed out; found {len(candidates)} matching link(s) in DOM")
if candidates:
try:
self.driver.execute_script("arguments[0].click();", candidates[0])
except Exception as js_err:
print(f"[Delta Indiv step1] JS click failed: {js_err}")
print(f"[Delta Indiv step1] Current URL: {self.driver.current_url}")
return "ERROR: Could not open Group/Health Connector search page"
else:
print(f"[Delta Indiv step1] No Group/Health Connector link found in DOM. Current URL: {self.driver.current_url}")
return "ERROR: Could not open Group/Health Connector search page"
print(f"[Delta Indiv step1] No Search for Patient button at: {self.driver.current_url}")
if search_link is None:
if "check-patient-benefits-search" not in self.driver.current_url.lower():
return "ERROR: Could not open patient search page"
else:
print(f"[Delta Indiv step1] Found Search for Patient on: {self.driver.current_url}")
href = search_link.get_attribute("href")
try:
self.driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", search_link)
search_link.click()
except Exception as click_err:
print(f"[Delta Indiv step1] Normal click failed ({click_err}) — using JS click")
self.driver.execute_script("arguments[0].click();", search_link)
print("[Delta Indiv step1] Clicked Search for Patient")
try:
wait.until(lambda d: "check-patient-benefits-search" in d.current_url)
WebDriverWait(self.driver, 15).until(
lambda d: "check-patient-benefits-search" in d.current_url.lower()
)
except TimeoutException:
print(f"[Delta Indiv step1] Click did not navigate to search page. Current URL: {self.driver.current_url}")
return "ERROR: Could not open Group/Health Connector search page"
if not href:
print(f"[Delta Indiv step1] Click did not navigate. Current URL: {self.driver.current_url}")
return "ERROR: Could not open patient search page"
print(f"[Delta Indiv step1] Click did not navigate — opening link directly: {href}")
self.driver.get(href)
try:
wait.until(lambda d: "check-patient-benefits-search" in d.current_url.lower())
except TimeoutException:
print(f"[Delta Indiv step1] Could not reach search page. Current URL: {self.driver.current_url}")
return "ERROR: Could not open patient search page"
print(f"[Delta Indiv step1] On Group/Health Connector search page: {self.driver.current_url}")
print(f"[Delta Indiv step1] On patient search page: {self.driver.current_url}")
# Fill Birthdate (masked mm/dd/yyyy input — send digits only, mask inserts slashes)
if self.dateOfBirth:
try:
dob_parts = self.dateOfBirth.split("-")
year = dob_parts[0]
month = dob_parts[1].zfill(2)
day = dob_parts[2].zfill(2)
# "Search by Patient Details" tab (default): Last Name + Date of Birth, then Submit
if not self.lastName or not self.dateOfBirth:
return "ERROR: Last name and date of birth are required for Delta Indiv search"
birthdate_input = wait.until(EC.element_to_be_clickable(
(By.XPATH, "//input[@data-test-id='InStateSearchBirthdate']")
))
birthdate_input.click()
birthdate_input.send_keys(Keys.CONTROL, "a")
birthdate_input.send_keys(Keys.BACKSPACE)
birthdate_input.send_keys(f"{month}{day}{year}")
print(f"[Delta Indiv step1] Filled Birthdate: {month}/{day}/{year}")
except Exception as e:
print(f"[Delta Indiv step1] Warning: Could not fill Birthdate: {e}")
last_name_input = wait.until(EC.element_to_be_clickable(
(By.XPATH, "//input[@data-test-id='LastName']")
))
last_name_input.clear()
last_name_input.send_keys(self.lastName)
print(f"[Delta Indiv step1] Filled Last Name: {self.lastName}")
# 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"[Delta Indiv step1] Filled Subscriber ID: {self.memberId}")
except Exception as e:
print(f"[Delta Indiv step1] Warning: Could not fill Subscriber ID: {e}")
# Masked mm/dd/yyyy input — send digits only, mask inserts slashes
dob_parts = self.dateOfBirth.split("-")
year = dob_parts[0]
month = dob_parts[1].zfill(2)
day = dob_parts[2].zfill(2)
dob_input = wait.until(EC.element_to_be_clickable(
(By.XPATH, "//input[@data-test-id='DateOfBirth']")
))
dob_input.click()
dob_input.send_keys(Keys.CONTROL, "a")
dob_input.send_keys(Keys.BACKSPACE)
dob_input.send_keys(f"{month}{day}{year}")
print(f"[Delta Indiv step1] Filled Date of Birth: {month}/{day}/{year}")
time.sleep(0.3)
# Click Search button
search_btn = wait.until(EC.element_to_be_clickable(
(By.XPATH, "//button[@data-test-id='InStateSearchButton']")
submit_btn = wait.until(EC.element_to_be_clickable(
(By.XPATH, "//button[@data-test-id='SearchByPatientButton']")
))
search_btn.click()
print("[Delta Indiv step1] Clicked Search button")
submit_btn.click()
print("[Delta Indiv step1] Clicked Submit")
# Wait for either the Members results table to render, or (when the
# Subscriber ID + DOB match exactly one family member) Delta Indiv to skip
# the results list and navigate straight to the patient detail page.
# Note: the detail page URL is nested UNDER the search page path
# (".../check-patient-benefits-search/check-patient-benefit-details"),
# so "check-patient-benefits-search" alone can't distinguish them —
# only the detail page's own "-benefit-details" segment can.
# Submit normally lands straight on the "Benefits for <NAME>" details page;
# a results list (multiple matches) is handled in step2.
try:
WebDriverWait(self.driver, 15).until(
lambda d: d.find_elements(By.XPATH, "//table//tbody//tr")
or "check-patient-benefit-details" in d.current_url.lower()
WebDriverWait(self.driver, 20).until(
lambda d: self._on_detail_page()
or d.find_elements(By.XPATH, "//table//tbody//tr//a")
)
time.sleep(2)
except TimeoutException:
print("[Delta Indiv step1] Warning: No results table appeared within timeout")
print("[Delta Indiv step1] Warning: No patient details or results appeared within timeout")
return "ERROR: INVALID SEARCH CRITERIA"
if "check-patient-benefit-details" in self.driver.current_url.lower():
print(f"[Delta Indiv step1] Search navigated directly to detail page: {self.driver.current_url}")
print("[Delta Indiv step1] Search completed successfully")
return "Success"
@@ -393,270 +419,178 @@ class AutomationDeltaIndivEligibilityCheck:
print(f"[Delta Indiv step1] Exception: {e}")
return f"ERROR:STEP1 - {e}"
def step2(self):
"""Navigate to patient detail page and generate PDF."""
wait = WebDriverWait(self.driver, 90)
def _on_detail_page(self):
"""True when the "Benefits for <NAME>" patient details page is showing."""
try:
return bool(self.driver.find_elements(
By.XPATH,
"//h1[starts-with(normalize-space(), 'Benefits for')]"
" | //*[normalize-space()='Subscriber #:']"
))
except Exception:
return False
def _open_patient_from_results(self):
"""If the search returned a list instead of the details page, click the matching patient."""
rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr[.//a]")
print(f"[Delta Indiv step2] Search returned {len(rows)} result row(s)")
if not rows:
return False
target_row = None
if self.dateOfBirth:
try:
y, m, d = self.dateOfBirth.split("-")
dob_variants = {
f"{calendar.month_abbr[int(m)]} {int(d)}, {y}".lower(),
f"{m.zfill(2)}/{d.zfill(2)}/{y}",
}
target_row = next(
(r for r in rows if any(v in r.text.lower() for v in dob_variants)), None
)
except Exception as e:
print(f"[Delta Indiv step2] DOB matching error (non-fatal): {e}")
if target_row is None and self.firstName:
target_row = next(
(r for r in rows if self.firstName.strip().lower() in r.text.lower()), None
)
if target_row is None:
target_row = rows[0]
print("[Delta Indiv step2] No DOB/name match — using first result row")
target_row.find_element(By.XPATH, ".//a").click()
WebDriverWait(self.driver, 20).until(lambda d: self._on_detail_page())
return True
def _write_full_page_pdf(self, pdf_path):
"""Print the current window as ONE long PDF page (no page breaks)."""
dims = self.driver.execute_script(
"return {w: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth),"
" h: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight)};"
)
# 8.5in wide; shrink to fit the page width, then make the paper as tall as the content
margin_in = 0.3
page_width_in = 8.5
scale = max(0.1, min(1.0, (page_width_in - 2 * margin_in) / (dims["w"] / 96)))
page_height_in = dims["h"] / 96 * scale + 2 * margin_in + 0.2
result = self.driver.execute_cdp_cmd("Page.printToPDF", {
"landscape": False,
"displayHeaderFooter": False,
"printBackground": True,
"preferCSSPageSize": False,
"paperWidth": page_width_in,
"paperHeight": page_height_in,
"marginTop": margin_in,
"marginBottom": margin_in,
"marginLeft": margin_in,
"marginRight": margin_in,
"scale": scale,
})
with open(pdf_path, "wb") as f:
f.write(base64.b64decode(result.get("data", "")))
def _save_print_pdf(self, pdf_path):
"""
Click the details page's Print link — it downloads a PDF of the page — and move
that download to pdf_path. Falls back to printing the page itself if no PDF arrives.
"""
files_before = set(os.listdir(self.download_dir))
try:
import re
print_link = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//a[@data-test-id='PrintCpdIcon']"))
)
self.driver.execute_script("arguments[0].click();", print_link)
print("[Delta Indiv step2] Clicked Print")
eligibilityText = "unknown"
foundMemberId = self.memberId or ""
# If the caller already knows this patient (e.g. an existing patient record
# being rechecked), use that name directly — no need to scrape/parse it off
# the Delta Indiv page, only the eligibility status is actually needed.
patientName = f"{self.firstName} {self.lastName}".strip() if self.firstName and self.lastName else ""
known_patient_name = bool(patientName)
def downloaded_pdf(_):
new = set(os.listdir(self.download_dir)) - files_before
# Chrome writes .crdownload until the download finishes
if any(f.endswith(".crdownload") for f in new):
return False
pdfs = [f for f in new if f.lower().endswith(".pdf")]
return pdfs[0] if pdfs else False
# Delta Indiv now sometimes skips the family-members results table entirely and
# navigates straight to the single-patient detail page (when Subscriber ID +
# DOB match exactly one person). In that case there's no members table to
# parse here — skip straight to the detail-page fallback extraction below.
already_on_detail_page = "check-patient-benefit-details" in self.driver.current_url.lower()
name = WebDriverWait(self.driver, 60).until(downloaded_pdf)
os.replace(os.path.join(self.download_dir, name), pdf_path)
print(f"[Delta Indiv step2] Print downloaded PDF: {name}")
except Exception as e:
print(f"[Delta Indiv step2] Print download failed ({e}) — printing the page itself")
self._write_full_page_pdf(pdf_path)
rows = []
target_row = None
print(f"[Delta Indiv step2] PDF saved at: {pdf_path}")
if not already_on_detail_page:
# Wait for results table, then pause for full render
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
)
time.sleep(2) # Let the row content fully render after table appears
except TimeoutException:
print("[Delta Indiv step2] Warning: Results table not found within timeout")
def step2(self):
"""On the patient details page: extract Subscriber #, name and eligibility, then save a PDF."""
try:
if not self._on_detail_page():
if not self._open_patient_from_results():
return {"status": "error", "message": "Patient details page not found"}
# The Members table lists the whole family sharing this subscriber ID —
# find the row matching this patient's DOB (falls back to last name, then first row).
rows = self.driver.find_elements(By.XPATH, "//table//tbody//tr")
print(f"[Delta Indiv step2] Found {len(rows)} family member row(s)")
else:
print(f"[Delta Indiv step2] Already on detail page: {self.driver.current_url}")
print(f"[Delta Indiv step2] On patient details page: {self.driver.current_url}")
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()
for row in rows:
cells = row.find_elements(By.XPATH, "./td")
if len(cells) >= 3 and cells[2].text.strip().lower() == expected_dob:
target_row = row
break
except Exception as e:
print(f"[Delta Indiv step2] DOB matching error (non-fatal): {e}")
if target_row is None and self.lastName and rows:
for row in rows:
if self.lastName.strip().lower() in row.text.lower():
target_row = row
break
if target_row is None and rows:
target_row = rows[0]
print("[Delta Indiv step2] No DOB/name match — defaulting to first family member row")
if target_row is not None:
try:
cells = target_row.find_elements(By.XPATH, "./td")
if cells and not known_patient_name:
patientName = cells[0].text.strip()
print(f"[Delta Indiv step2] Extracted patient name from row: '{patientName}'")
if len(cells) >= 2 and cells[1].text.strip():
foundMemberId = cells[1].text.strip()
if len(cells) >= 7:
status_text = cells[6].text.strip().lower()
if "inactive" in status_text or "termed" in status_text:
eligibilityText = "inactive"
elif "active" in status_text:
eligibilityText = "active"
else:
eligibilityText = status_text
print(f"[Delta Indiv step2] Found eligibility status: {eligibilityText}")
except Exception as e:
print(f"[Delta Indiv step2] Error reading matched row cells: {e}")
# Click into the member's detail page
try:
name_link = target_row.find_element(By.XPATH, ".//a")
name_link.click()
print(f"[Delta Indiv step2] Clicked into detail page for: {patientName}")
except Exception as e:
print(f"[Delta Indiv step2] Warning: Could not click patient name link: {e}")
else:
print("[Delta Indiv step2] Warning: No family member rows found in search results")
# Wait for the detail page to load (URL gains the "-benefit-details" segment)
try:
WebDriverWait(self.driver, 15).until(
lambda d: "check-patient-benefit-details" in d.current_url.lower()
)
print(f"[Delta Indiv step2] Navigated to detail page: {self.driver.current_url}")
except Exception:
print(f"[Delta Indiv step2] Warning: Still on search page: {self.driver.current_url}")
# Wait for meaningful content on the detail page
for selector in [
"//*[contains(text(),'Date of Birth') or contains(text(),'Address') or contains(text(),'Member ID')]",
"//table", "//h1", "//h2",
]:
try:
WebDriverWait(self.driver, 15).until(
EC.presence_of_element_located((By.XPATH, selector))
)
print(f"[Delta Indiv step2] Detail page content loaded: {selector}")
break
except Exception:
continue
time.sleep(3) # Let JavaScript finish rendering all sections
# Fallback: refine patient name from the detail page text if row extraction failed
if not patientName:
try:
page_lines = self.driver.execute_script(
"return document.body.innerText;"
).split('\n')
page_lines = [l.strip() for l in page_lines if l.strip()]
except Exception as e:
print(f"[Delta Indiv step2] Could not get page text: {e}")
page_lines = []
# UI noise words that appear in accessibility/sort-button text or field labels
ui_noise = ['click', 'sort', 'activate', 'direction', 'enter', 'space',
'current', 'use ', 'table', 'column', 'filter', 'search',
'date', 'birth', 'relationship', 'subscriber', 'coverage',
'status', 'period', 'network', 'plan', 'deductible']
def looks_like_name(text):
"""Return True if text is a plausible patient name."""
t = text.strip()
# Must be 2–60 chars, letters/spaces/hyphens/apostrophes only (no periods)
if not t or not (2 <= len(t) <= 60):
return False
if not re.match(r"^[A-Za-z\s\-']+$", t):
return False
# Must not be UI noise
if any(w in t.lower() for w in ui_noise):
return False
return True
# Preferred: the "Benefits for JOSHUA ZHANG" heading names the actual
# patient. "Subscriber name:" is the policy holder (e.g. a parent), so
# it is only used below if this heading is missing.
for line in page_lines:
heading = re.match(r'^benefits\s+for\s+(.+)$', line, re.IGNORECASE)
if heading and looks_like_name(heading.group(1)):
patientName = heading.group(1).strip()
print(f"[Delta Indiv step2] Extracted name from 'Benefits for' heading: '{patientName}'")
break
# Scan lines for the "Name" label. When "Name" appears alone we scan
# forward past accessibility text to the first plausible name value.
for i, line in enumerate(page_lines):
if patientName:
break
# Case 1: "Name : Value" or "Name: Value" on the same line
# (Delta Indiv's detail page labels this "Subscriber name:", so that
# prefix must be accepted rather than excluded)
same_line = re.match(
r'^(?:member\s+|subscriber\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE
)
if same_line and not re.search(
r'(provider|group|plan)\s+name', line, re.IGNORECASE
):
candidate = same_line.group(1).strip()
if looks_like_name(candidate):
patientName = candidate
print(f"[Delta Indiv step2] Extracted name from 'Name:' label: '{patientName}'")
break
# Case 2: "Name" or "Name:" alone on a line — scan forward for value
elif re.match(r'^(?:member\s+|subscriber\s+)?name\s*:?\s*$', line, re.IGNORECASE):
for j in range(i + 1, min(i + 6, len(page_lines))):
candidate = page_lines[j].strip()
if looks_like_name(candidate):
patientName = candidate
print(f"[Delta Indiv step2] Extracted name from 'Name' label (line +{j-i}): '{patientName}'")
break
if patientName:
break
# Clean patient name — strip any remaining date artifacts
if patientName:
cleaned = re.sub(r'\s*DOB[:\s]*\d{1,2}/\d{1,2}/\d{2,4}\s*', '', patientName, flags=re.IGNORECASE).strip()
cleaned = re.sub(r'\s+\d{1,2}/\d{1,2}/\d{2,4}$', '', cleaned).strip()
patientName = cleaned if cleaned else patientName
if not patientName:
print("[Delta Indiv step2] Could not extract patient name from detail page")
# Fallback: refine eligibility status from the detail page's "Coverage is:"
# label if the family-table row extraction didn't set it (e.g. Delta Indiv
# navigated straight to the detail page, skipping the members table).
if eligibilityText == "unknown":
try:
page_text = self.driver.execute_script("return document.body.innerText;")
coverage_match = re.search(
r'coverage\s+is\s*[:\-]?\s*([A-Za-z]+)', page_text, re.IGNORECASE
)
if coverage_match:
status_text = coverage_match.group(1).strip().lower()
if "inactive" in status_text or "termed" in status_text:
eligibilityText = "inactive"
elif "active" in status_text:
eligibilityText = "active"
else:
eligibilityText = status_text
print(f"[Delta Indiv step2] Extracted eligibility from 'Coverage is:' label: '{eligibilityText}'")
except Exception as e:
print(f"[Delta Indiv step2] Could not extract eligibility from 'Coverage is:' label: {e}")
# Wait for the page to be fully ready before capturing PDF
try:
WebDriverWait(self.driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
except Exception:
pass
time.sleep(3) # Let the benefit maximum sections finish rendering
# Extra wait for lazy-loaded sections (benefits summary, member history, etc.)
time.sleep(4)
page_text = self.driver.execute_script("return document.body.innerText;") or ""
# Generate PDF via Chrome DevTools Protocol
print("[Delta Indiv step2] Generating PDF of patient detail page...")
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,
}
def label_value(pattern):
m = re.search(pattern, page_text, re.IGNORECASE)
return m.group(1).strip() if m else ""
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
pdf_data = base64.b64decode(result.get('data', ''))
pdf_id = foundMemberId or self.memberId or "unknown"
# "Benefits for KAOUTER MOUSSAID" heading names the patient
# ("Subscriber name:" is the policy holder, which may be a parent)
patientName = label_value(r"(?m)^\s*Benefits\s+for\s+(.+?)\s*$")
if not patientName:
patientName = f"{self.firstName} {self.lastName}".strip()
firstName = patientName.split()[0] if patientName else self.firstName
foundMemberId = label_value(r"Subscriber\s*#\s*:\s*([A-Za-z0-9\-]+)") or self.memberId or ""
coverageIs = label_value(r"Coverage\s+is\s*:\s*([A-Za-z]+)")
eligibleThrough = label_value(
r"Eligible\s+through\s*:\s*([A-Za-z]{3,9}\.?\s+\d{1,2},\s*\d{4})"
)
# Eligibility: active while "Eligible through" is today or later;
# fall back to "Coverage is:" when the date can't be read.
eligibilityText = "unknown"
through_date = None
for fmt in ("%b %d, %Y", "%B %d, %Y"):
try:
through_date = datetime.strptime(eligibleThrough.replace(".", ""), fmt).date()
break
except ValueError:
continue
if through_date:
eligibilityText = "active" if through_date >= date.today() else "inactive"
elif coverageIs:
c = coverageIs.lower()
eligibilityText = "inactive" if ("inactive" in c or "termed" in c) else ("active" if "active" in c else c)
print(
f"[Delta Indiv step2] Extracted — Name: '{patientName}', First: '{firstName}', "
f"Subscriber #: '{foundMemberId}', Coverage is: '{coverageIs}', "
f"Eligible through: '{eligibleThrough}' -> {eligibilityText}"
)
pdf_id = foundMemberId or "unknown"
pdf_path = os.path.join(self.download_dir, f"eligibility_{pdf_id}.pdf")
with open(pdf_path, "wb") as f:
f.write(pdf_data)
print(f"[Delta Indiv step2] PDF saved at: {pdf_path}")
print(f"[Delta Indiv step2] Final — PatientName: '{patientName}', MemberID: '{foundMemberId}'")
self._save_print_pdf(pdf_path)
return {
"status": "success",
"eligibility": eligibilityText,
"eligibleThrough": eligibleThrough,
"coverageIs": coverageIs,
"ss_path": pdf_path, # kept for backward compatibility
"pdf_path": pdf_path, # explicit pdf_path
"pdf_path": pdf_path,
"patientName": patientName,
"firstName": firstName,
"memberId": foundMemberId,
"dateOfBirth": self.dateOfBirth,
}