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:
@@ -94,10 +94,16 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
||||
const { insuranceId, firstName, lastName, dob, userId } = options;
|
||||
if (!insuranceId) throw new Error("Missing insuranceId");
|
||||
|
||||
// Normalize insuranceId the same way insertPatientSchema does (strip spaces)
|
||||
const normalizedId = insuranceId.replace(/\s+/g, "");
|
||||
|
||||
const incomingFirst = (firstName || "").trim();
|
||||
const incomingLast = (lastName || "").trim();
|
||||
|
||||
let patient = await storage.getPatientByInsuranceId(insuranceId);
|
||||
console.log(`[createOrUpdatePatient] insuranceId="${normalizedId}" firstName="${incomingFirst}" lastName="${incomingLast}" userId=${userId}`);
|
||||
|
||||
let patient = await storage.getPatientByInsuranceId(normalizedId);
|
||||
console.log(`[createOrUpdatePatient] existing patient lookup: ${patient ? `found id=${patient.id}` : "not found"}`);
|
||||
|
||||
if (patient && patient.id) {
|
||||
const updates: any = {};
|
||||
@@ -110,8 +116,9 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
||||
if (!isNaN(parsed.getTime())) updates.dateOfBirth = parsed;
|
||||
}
|
||||
if (Object.keys(updates).length > 0) {
|
||||
console.log(`[createOrUpdatePatient] updating patient id=${patient.id} with`, updates);
|
||||
await storage.updatePatient(patient.id, updates);
|
||||
patient = await storage.getPatientByInsuranceId(insuranceId);
|
||||
patient = await storage.getPatientByInsuranceId(normalizedId);
|
||||
}
|
||||
return patient;
|
||||
}
|
||||
@@ -123,24 +130,31 @@ export async function createOrUpdatePatientByInsuranceId(options: {
|
||||
gender: "",
|
||||
phone: "",
|
||||
userId,
|
||||
insuranceId,
|
||||
insuranceId: normalizedId,
|
||||
};
|
||||
|
||||
let patientData: InsertPatient;
|
||||
try {
|
||||
patientData = insertPatientSchema.parse(createPayload);
|
||||
} catch {
|
||||
// Remove fields that may fail validation (invalid date or alphanumeric insuranceId)
|
||||
} catch (e1) {
|
||||
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 1):`, e1);
|
||||
const safePayload = { ...createPayload };
|
||||
delete safePayload.dateOfBirth;
|
||||
try {
|
||||
patientData = insertPatientSchema.parse(safePayload);
|
||||
} catch {
|
||||
// Last resort: skip schema validation and cast directly
|
||||
} catch (e2) {
|
||||
console.warn(`[createOrUpdatePatient] schema parse failed (attempt 2):`, e2);
|
||||
patientData = safePayload as InsertPatient;
|
||||
}
|
||||
}
|
||||
|
||||
await storage.createPatient(patientData);
|
||||
return storage.getPatientByInsuranceId(insuranceId);
|
||||
try {
|
||||
await storage.createPatient(patientData);
|
||||
console.log(`[createOrUpdatePatient] patient created successfully for insuranceId="${normalizedId}"`);
|
||||
} catch (dbErr: any) {
|
||||
console.error(`[createOrUpdatePatient] DB create failed:`, dbErr?.message ?? dbErr);
|
||||
throw dbErr;
|
||||
}
|
||||
|
||||
return storage.getPatientByInsuranceId(normalizedId);
|
||||
}
|
||||
|
||||
@@ -90,9 +90,33 @@ async function processDdmaResult(
|
||||
? seleniumResult.patientName.trim()
|
||||
: null;
|
||||
|
||||
const { firstName, lastName } = rawName
|
||||
? splitName(rawName)
|
||||
: { firstName: formFirstName ?? "", lastName: formLastName ?? "" };
|
||||
let firstName: string;
|
||||
let lastName: string;
|
||||
|
||||
if (rawName) {
|
||||
// Strip trailing bare dates DDMA appends to names e.g. "Christian Rodriguez 04/27/2026"
|
||||
const cleanName = rawName.replace(/\s+\d{1,2}\/\d{1,2}\/\d{2,4}$/, "").trim();
|
||||
|
||||
if (cleanName.includes(",")) {
|
||||
// "LAST, FIRST" format common on insurance portals
|
||||
const [last, ...firstParts] = cleanName.split(",").map((s: string) => s.trim());
|
||||
lastName = last || formLastName || "";
|
||||
firstName = firstParts.join(" ").trim() || formFirstName || "";
|
||||
} else {
|
||||
const parsed = splitName(cleanName);
|
||||
if (!parsed.lastName) {
|
||||
// Single word — treat as last name, pull first name from form
|
||||
lastName = parsed.firstName || formLastName || "";
|
||||
firstName = formFirstName || "";
|
||||
} else {
|
||||
firstName = parsed.firstName || formFirstName || "";
|
||||
lastName = parsed.lastName || formLastName || "";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
firstName = formFirstName ?? "";
|
||||
lastName = formLastName ?? "";
|
||||
}
|
||||
|
||||
// 2) Create / update patient
|
||||
await createOrUpdatePatientByInsuranceId({
|
||||
@@ -104,7 +128,9 @@ async function processDdmaResult(
|
||||
});
|
||||
|
||||
// 3) Fetch patient (needed for ID)
|
||||
const patient = await storage.getPatientByInsuranceId(insuranceId);
|
||||
const normalizedInsuranceId = insuranceId.replace(/\s+/g, "");
|
||||
const patient = await storage.getPatientByInsuranceId(normalizedInsuranceId);
|
||||
log("ddma-processor", `patient lookup after create: ${patient ? `id=${patient.id}` : "NOT FOUND"} for insuranceId="${normalizedInsuranceId}"`);
|
||||
if (!patient?.id) {
|
||||
output.patientUpdateStatus = "Patient not found; no update performed";
|
||||
return output;
|
||||
@@ -177,6 +203,7 @@ async function processDdmaResult(
|
||||
output.pdfFileId = createdPdfFileId;
|
||||
return output;
|
||||
} catch (err: any) {
|
||||
log("ddma-processor", `processDdmaResult ERROR: ${err?.message ?? String(err)}`, err);
|
||||
return {
|
||||
...output,
|
||||
pdfUploadStatus:
|
||||
|
||||
@@ -24,9 +24,10 @@ router.get("/recent", async (req: Request, res: Response) => {
|
||||
const limit = parseInt(req.query.limit as string) || 10;
|
||||
const offset = parseInt(req.query.offset as string) || 0;
|
||||
|
||||
const userId = req.user!.id;
|
||||
const [patients, totalCount] = await Promise.all([
|
||||
storage.getRecentPatients(limit, offset),
|
||||
storage.getTotalPatientCount(),
|
||||
storage.getRecentPatients(limit, offset, userId),
|
||||
storage.getTotalPatientCount(userId),
|
||||
]);
|
||||
|
||||
res.json({ patients, totalCount });
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface IStorage {
|
||||
getPatient(id: number): Promise<Patient | undefined>;
|
||||
getPatientByInsuranceId(insuranceId: string): Promise<Patient | null>;
|
||||
getPatientsByUserId(userId: number): Promise<Patient[]>;
|
||||
getRecentPatients(limit: number, offset: number): Promise<Patient[]>;
|
||||
getRecentPatients(limit: number, offset: number, userId: number): Promise<Patient[]>;
|
||||
getPatientsByIds(ids: number[]): Promise<Patient[]>;
|
||||
createPatient(patient: InsertPatient): Promise<Patient>;
|
||||
updatePatient(id: number, patient: UpdatePatient): Promise<Patient>;
|
||||
@@ -33,7 +33,7 @@ export interface IStorage {
|
||||
status: string;
|
||||
}[]
|
||||
>;
|
||||
getTotalPatientCount(): Promise<number>;
|
||||
getTotalPatientCount(userId: number): Promise<number>;
|
||||
countPatients(filters: any): Promise<number>; // optional but useful
|
||||
getPatientFinancialRows(
|
||||
patientId: number,
|
||||
@@ -59,11 +59,12 @@ export const patientsStorage: IStorage = {
|
||||
});
|
||||
},
|
||||
|
||||
async getRecentPatients(limit: number, offset: number): Promise<Patient[]> {
|
||||
async getRecentPatients(limit: number, offset: number, userId: number): Promise<Patient[]> {
|
||||
return db.patient.findMany({
|
||||
where: { userId },
|
||||
skip: offset,
|
||||
take: limit,
|
||||
orderBy: { createdAt: "desc" },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -85,6 +86,7 @@ export const patientsStorage: IStorage = {
|
||||
status: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -124,7 +126,7 @@ export const patientsStorage: IStorage = {
|
||||
}) {
|
||||
return db.patient.findMany({
|
||||
where: filters,
|
||||
orderBy: { createdAt: "desc" },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: limit,
|
||||
skip: offset,
|
||||
select: {
|
||||
@@ -141,8 +143,8 @@ export const patientsStorage: IStorage = {
|
||||
});
|
||||
},
|
||||
|
||||
async getTotalPatientCount(): Promise<number> {
|
||||
return db.patient.count();
|
||||
async getTotalPatientCount(userId: number): Promise<number> {
|
||||
return db.patient.count({ where: { userId } });
|
||||
},
|
||||
|
||||
async countPatients(filters: any) {
|
||||
|
||||
@@ -44,28 +44,26 @@ class DDMABrowserManager:
|
||||
|
||||
def clear_session_on_startup(self):
|
||||
"""
|
||||
Clear session cookies from Chrome profile on startup.
|
||||
This forces a fresh login after PC restart.
|
||||
Preserves device trust tokens (LocalStorage, IndexedDB) to avoid OTPs.
|
||||
Clear only login cookies on startup to force credential re-entry after restart.
|
||||
NEVER clears Local Storage or IndexedDB — those hold the DDMA device trust token
|
||||
that allows the portal to skip OTP for recognised devices.
|
||||
"""
|
||||
print("[DDMA BrowserManager] Clearing session on startup...")
|
||||
print("[DDMA BrowserManager] Clearing login cookies on startup (preserving device trust)...")
|
||||
|
||||
try:
|
||||
# Clear credentials tracking file
|
||||
# Reset credentials tracking so the next login re-saves the hash
|
||||
if os.path.exists(self._credentials_file):
|
||||
os.remove(self._credentials_file)
|
||||
print("[DDMA BrowserManager] Cleared credentials tracking file")
|
||||
|
||||
# Clear session-related Chrome profile files
|
||||
# Only remove cookie / login-data files — these expire the session so the
|
||||
# user must re-enter credentials, but the device trust token is untouched.
|
||||
session_files = [
|
||||
"Cookies",
|
||||
"Cookies-journal",
|
||||
"Login Data",
|
||||
"Login Data-journal",
|
||||
"Web Data",
|
||||
"Web Data-journal",
|
||||
]
|
||||
|
||||
for filename in session_files:
|
||||
for base in [os.path.join(self.profile_dir, "Default"), self.profile_dir]:
|
||||
filepath = os.path.join(base, filename)
|
||||
@@ -76,55 +74,12 @@ class DDMABrowserManager:
|
||||
except Exception as e:
|
||||
print(f"[DDMA BrowserManager] Could not remove {filename}: {e}")
|
||||
|
||||
# Clear Session Storage (contains login state)
|
||||
session_storage_dir = os.path.join(self.profile_dir, "Default", "Session Storage")
|
||||
if os.path.exists(session_storage_dir):
|
||||
try:
|
||||
shutil.rmtree(session_storage_dir)
|
||||
print("[DDMA BrowserManager] Cleared Session Storage")
|
||||
except Exception as e:
|
||||
print(f"[DDMA BrowserManager] Could not clear Session Storage: {e}")
|
||||
|
||||
# Clear Local Storage (may contain auth tokens)
|
||||
local_storage_dir = os.path.join(self.profile_dir, "Default", "Local Storage")
|
||||
if os.path.exists(local_storage_dir):
|
||||
try:
|
||||
shutil.rmtree(local_storage_dir)
|
||||
print("[DDMA BrowserManager] Cleared Local Storage")
|
||||
except Exception as e:
|
||||
print(f"[DDMA BrowserManager] Could not clear Local Storage: {e}")
|
||||
|
||||
# Clear IndexedDB (may contain auth tokens)
|
||||
indexeddb_dir = os.path.join(self.profile_dir, "Default", "IndexedDB")
|
||||
if os.path.exists(indexeddb_dir):
|
||||
try:
|
||||
shutil.rmtree(indexeddb_dir)
|
||||
print("[DDMA BrowserManager] Cleared IndexedDB")
|
||||
except Exception as e:
|
||||
print(f"[DDMA BrowserManager] Could not clear IndexedDB: {e}")
|
||||
|
||||
# Clear browser caches
|
||||
cache_dirs = [
|
||||
os.path.join(self.profile_dir, "Default", "Cache"),
|
||||
os.path.join(self.profile_dir, "Default", "Code Cache"),
|
||||
os.path.join(self.profile_dir, "Default", "GPUCache"),
|
||||
os.path.join(self.profile_dir, "Default", "Service Worker"),
|
||||
os.path.join(self.profile_dir, "Cache"),
|
||||
os.path.join(self.profile_dir, "Code Cache"),
|
||||
os.path.join(self.profile_dir, "GPUCache"),
|
||||
os.path.join(self.profile_dir, "Service Worker"),
|
||||
os.path.join(self.profile_dir, "ShaderCache"),
|
||||
]
|
||||
for cache_dir in cache_dirs:
|
||||
if os.path.exists(cache_dir):
|
||||
try:
|
||||
shutil.rmtree(cache_dir)
|
||||
print(f"[DDMA BrowserManager] Cleared {os.path.basename(cache_dir)}")
|
||||
except Exception as e:
|
||||
print(f"[DDMA BrowserManager] Could not clear {os.path.basename(cache_dir)}: {e}")
|
||||
# Local Storage, IndexedDB, and Session Storage are intentionally
|
||||
# NOT cleared — they contain the DDMA device trust token that prevents
|
||||
# OTP from being required on every login.
|
||||
|
||||
self._needs_session_clear = True
|
||||
print("[DDMA BrowserManager] Session cleared - will require fresh login")
|
||||
print("[DDMA BrowserManager] Startup clear done — device trust preserved, OTP not required")
|
||||
|
||||
except Exception as e:
|
||||
print(f"[DDMA BrowserManager] Error clearing session: {e}")
|
||||
|
||||
@@ -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