feat: fix DDMA eligibility — patient list, name extraction, PDF page, OTP session
- Filter patient list by userId so each user sees only their own patients - Sort patients by updatedAt DESC so recently checked patients appear first - Add updatedAt field to Patient model (DB migration via raw SQL + db:generate) - Fix DDMA name extraction: read from detail page "Name:" label, not search results row text which included appended dates - Fix PDF capture: use driver.get() instead of click() to avoid race condition that was saving the search results page instead of the patient detail page - Strip trailing bare dates from extracted names (e.g. "Rodriguez 04/27/2026") - Handle "Last, First" comma format and single-word last names in splitName - Normalize insuranceId consistently in createOrUpdatePatientByInsuranceId - Fix OTP persistent session: stop clearing LocalStorage/IndexedDB on startup (these hold the DDMA device trust token that skips OTP on subsequent logins) - Increase post-navigation wait time for full page render before PDF generation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -378,53 +378,35 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
try:
|
||||
import re
|
||||
|
||||
# Wait for results table
|
||||
# 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("[DDMA step2] Warning: Results table not found within timeout")
|
||||
|
||||
eligibilityText = "unknown"
|
||||
foundMemberId = ""
|
||||
foundMemberId = self.memberId or ""
|
||||
patientName = ""
|
||||
|
||||
# Extract data from first result row
|
||||
# Extract eligibility status and member ID from search results row
|
||||
try:
|
||||
first_row = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]")
|
||||
row_text = first_row.text.strip()
|
||||
print(f"[DDMA step2] First row text: {row_text[:150]}...")
|
||||
|
||||
if row_text:
|
||||
lines = row_text.split('\n')
|
||||
|
||||
# Extract patient name (first line, strip DOB if present)
|
||||
if lines:
|
||||
potential_name = lines[0].strip()
|
||||
potential_name = re.sub(r'\s*DOB[:\s]*\d{1,2}/\d{1,2}/\d{2,4}\s*', '', potential_name, flags=re.IGNORECASE).strip()
|
||||
if potential_name and not potential_name.startswith('DOB') and not potential_name.isdigit():
|
||||
patientName = potential_name
|
||||
print(f"[DDMA step2] Extracted patient name: '{patientName}'")
|
||||
|
||||
# Extract Member ID
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and re.match(r'^[A-Z0-9]{5,}$', line) and not line.startswith('DOB'):
|
||||
foundMemberId = line
|
||||
print(f"[DDMA step2] Extracted Member ID: {foundMemberId}")
|
||||
break
|
||||
|
||||
if not foundMemberId and self.memberId:
|
||||
foundMemberId = self.memberId
|
||||
print(f"[DDMA step2] Using input Member ID: {foundMemberId}")
|
||||
|
||||
lines = row_text.split('\n') if row_text else []
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line and re.match(r'^[A-Z0-9]{5,}$', line) and not line.startswith('DOB'):
|
||||
foundMemberId = line
|
||||
print(f"[DDMA step2] Extracted Member ID: {foundMemberId}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Error extracting data from row: {e}")
|
||||
if self.memberId:
|
||||
foundMemberId = self.memberId
|
||||
print(f"[DDMA step2] Error reading first row: {e}")
|
||||
|
||||
# Extract eligibility status
|
||||
try:
|
||||
short_wait = WebDriverWait(self.driver, 3)
|
||||
status_link = short_wait.until(EC.presence_of_element_located((
|
||||
@@ -445,109 +427,128 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Navigate to detailed patient page
|
||||
print("[DDMA step2] Navigating to patient detail page...")
|
||||
patient_name_clicked = False
|
||||
# Find the member-details URL from the first row
|
||||
print("[DDMA step2] Looking for patient detail link...")
|
||||
detail_url = None
|
||||
|
||||
patient_link_selectors = [
|
||||
link_selectors = [
|
||||
"(//table//tbody//tr)[1]//td[1]//a",
|
||||
"(//tbody//tr)[1]//a[contains(@href, 'member-details')]",
|
||||
"(//tbody//tr)[1]//a[contains(@href, 'member')]",
|
||||
"//a[contains(@href, 'member-details')]",
|
||||
]
|
||||
|
||||
for selector in patient_link_selectors:
|
||||
for selector in link_selectors:
|
||||
try:
|
||||
patient_link = WebDriverWait(self.driver, 5).until(
|
||||
link_el = WebDriverWait(self.driver, 5).until(
|
||||
EC.presence_of_element_located((By.XPATH, selector))
|
||||
)
|
||||
link_text = patient_link.text.strip()
|
||||
href = patient_link.get_attribute("href")
|
||||
print(f"[DDMA step2] Found patient link: text='{link_text}', href={href}")
|
||||
|
||||
if link_text and not patientName:
|
||||
patientName = link_text
|
||||
|
||||
href = link_el.get_attribute("href")
|
||||
if href and "member-details" in href:
|
||||
detail_url = href
|
||||
patient_name_clicked = True
|
||||
print(f"[DDMA step2] Found detail URL: {href}")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Selector '{selector}' failed: {e}")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if not detail_url:
|
||||
try:
|
||||
all_links = self.driver.find_elements(By.XPATH, "//a[contains(@href, 'member-details')]")
|
||||
if all_links:
|
||||
detail_url = all_links[0].get_attribute("href")
|
||||
patient_name_clicked = True
|
||||
print(f"[DDMA step2] Fallback member-details link: {detail_url}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA step2] Could not find member-details link: {e}")
|
||||
|
||||
if patient_name_clicked and detail_url:
|
||||
print(f"[DDMA step2] Navigating directly to: {detail_url}")
|
||||
if detail_url:
|
||||
# Always navigate via driver.get() — this blocks until the new page loads,
|
||||
# unlike click() which returns immediately and causes a race condition.
|
||||
print(f"[DDMA step2] Navigating to detail page: {detail_url}")
|
||||
self.driver.get(detail_url)
|
||||
|
||||
# Wait for page to be ready
|
||||
# Confirm we actually landed on the detail page (not redirected away)
|
||||
try:
|
||||
WebDriverWait(self.driver, 30).until(
|
||||
lambda d: d.execute_script("return document.readyState") == "complete"
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
lambda d: "member-details" in d.current_url
|
||||
)
|
||||
print(f"[DDMA step2] Confirmed on detail page: {self.driver.current_url}")
|
||||
except Exception:
|
||||
print("[DDMA step2] Warning: document.readyState did not become 'complete'")
|
||||
print(f"[DDMA step2] Warning: URL after navigation: {self.driver.current_url}")
|
||||
|
||||
# Wait for meaningful content to appear
|
||||
content_selectors = [
|
||||
"//div[contains(@class,'member') or contains(@class,'detail') or contains(@class,'patient')]",
|
||||
"//h1", "//h2", "//table",
|
||||
"//*[contains(text(),'Member ID') or contains(text(),'Name') or contains(text(),'Date of Birth')]",
|
||||
]
|
||||
for selector in content_selectors:
|
||||
# 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, 10).until(
|
||||
WebDriverWait(self.driver, 15).until(
|
||||
EC.presence_of_element_located((By.XPATH, selector))
|
||||
)
|
||||
print(f"[DDMA step2] Content loaded: {selector}")
|
||||
print(f"[DDMA step2] Detail page content loaded: {selector}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
time.sleep(1) # Brief settle for any late-rendering elements
|
||||
time.sleep(3) # Let JavaScript finish rendering all sections
|
||||
|
||||
# Try to extract patient name from detail page if not already found
|
||||
if not patientName:
|
||||
for selector in ["//h1", "//h2", "//*[contains(@class,'patient-name') or contains(@class,'member-name')]"]:
|
||||
try:
|
||||
name_elem = self.driver.find_element(By.XPATH, selector)
|
||||
name_text = name_elem.text.strip()
|
||||
if name_text and len(name_text) > 1:
|
||||
if not any(x in name_text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'date', 'print']):
|
||||
patientName = name_text
|
||||
print(f"[DDMA step2] Found patient name on detail page: {patientName}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
# Get full page text as lines
|
||||
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"[DDMA 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
|
||||
|
||||
# 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):
|
||||
# Case 1: "Name : Value" or "Name: Value" on the same line
|
||||
same_line = re.match(
|
||||
r'^(?:member\s+)?name\s*[:\-]\s*(.+)$', line, re.IGNORECASE
|
||||
)
|
||||
if same_line and not re.search(
|
||||
r'(provider|group|subscriber|plan)\s+name', line, re.IGNORECASE
|
||||
):
|
||||
candidate = same_line.group(1).strip()
|
||||
if looks_like_name(candidate):
|
||||
patientName = candidate
|
||||
print(f"[DDMA 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+)?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"[DDMA step2] Extracted name from 'Name' label (line +{j-i}): '{patientName}'")
|
||||
break
|
||||
if patientName:
|
||||
break
|
||||
else:
|
||||
print("[DDMA step2] Warning: Could not navigate to patient detail page")
|
||||
if not patientName:
|
||||
try:
|
||||
name_elem = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]//td[1]")
|
||||
patientName = name_elem.text.strip()
|
||||
except Exception:
|
||||
pass
|
||||
print("[DDMA step2] Warning: Could not find patient detail link")
|
||||
|
||||
if not patientName:
|
||||
print("[DDMA step2] Could not extract patient name")
|
||||
|
||||
# Clean patient name
|
||||
# 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()
|
||||
if cleaned:
|
||||
patientName = cleaned
|
||||
cleaned = re.sub(r'\s+\d{1,2}/\d{1,2}/\d{2,4}$', '', cleaned).strip()
|
||||
patientName = cleaned if cleaned else patientName
|
||||
|
||||
# Wait for page ready before PDF
|
||||
if not patientName:
|
||||
print("[DDMA step2] Could not extract patient name from detail page")
|
||||
|
||||
# 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"
|
||||
@@ -555,6 +556,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Extra wait for lazy-loaded sections (benefits summary, member history, etc.)
|
||||
time.sleep(4)
|
||||
|
||||
# Generate PDF via Chrome DevTools Protocol
|
||||
print("[DDMA step2] Generating PDF of patient detail page...")
|
||||
pdf_options = {
|
||||
|
||||
Reference in New Issue
Block a user