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:
Gitead
2026-05-01 21:40:04 -04:00
parent 24bbaed6ab
commit e26ebf7fd5
213 changed files with 1698 additions and 1425 deletions

View File

@@ -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 260 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 = {