feat(eligibility-check) - enhance DentaQuest and United SCO workflows with flexible input handling; added Selenium session clearing on credential updates and deletions; improved patient name extraction and eligibility checks across services

This commit is contained in:
2026-02-06 08:57:29 -05:00
parent e43329e95f
commit e425a829b2
12 changed files with 418 additions and 224 deletions

3
.gitignore vendored
View File

@@ -38,4 +38,5 @@ dist/
# env # env
*.env *.env
*chrome_profile_ddma* *chrome_profile_ddma*
*chrome_profile_dentaquest* *chrome_profile_dentaquest*
*chrome_profile_unitedsco*

View File

@@ -76,8 +76,33 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
const id = Number(req.params.id); const id = Number(req.params.id);
if (isNaN(id)) return res.status(400).send("Invalid credential ID"); if (isNaN(id)) return res.status(400).send("Invalid credential ID");
// Get existing credential to know its siteKey
const existing = await storage.getInsuranceCredential(id);
if (!existing) {
return res.status(404).json({ message: "Credential not found" });
}
const updates = req.body as Partial<InsuranceCredential>; const updates = req.body as Partial<InsuranceCredential>;
const credential = await storage.updateInsuranceCredential(id, updates); const credential = await storage.updateInsuranceCredential(id, updates);
// Clear Selenium browser session when credentials are changed
const seleniumAgentUrl = process.env.SELENIUM_AGENT_URL || "http://localhost:5002";
try {
if (existing.siteKey === "DDMA") {
await fetch(`${seleniumAgentUrl}/clear-ddma-session`, { method: "POST" });
console.log("[insuranceCreds] Cleared DDMA browser session after credential update");
} else if (existing.siteKey === "DENTAQUEST") {
await fetch(`${seleniumAgentUrl}/clear-dentaquest-session`, { method: "POST" });
console.log("[insuranceCreds] Cleared DentaQuest browser session after credential update");
} else if (existing.siteKey === "UNITEDSCO") {
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
console.log("[insuranceCreds] Cleared United SCO browser session after credential update");
}
} catch (seleniumErr) {
// Don't fail the update if Selenium session clear fails
console.error("[insuranceCreds] Failed to clear Selenium session:", seleniumErr);
}
return res.status(200).json(credential); return res.status(200).json(credential);
} catch (err) { } catch (err) {
return res return res
@@ -115,6 +140,25 @@ router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
.status(404) .status(404)
.json({ message: "Credential not found or already deleted" }); .json({ message: "Credential not found or already deleted" });
} }
// 4) Clear Selenium browser session for this provider
const seleniumAgentUrl = process.env.SELENIUM_AGENT_URL || "http://localhost:5002";
try {
if (existing.siteKey === "DDMA") {
await fetch(`${seleniumAgentUrl}/clear-ddma-session`, { method: "POST" });
console.log("[insuranceCreds] Cleared DDMA browser session after credential deletion");
} else if (existing.siteKey === "DENTAQUEST") {
await fetch(`${seleniumAgentUrl}/clear-dentaquest-session`, { method: "POST" });
console.log("[insuranceCreds] Cleared DentaQuest browser session after credential deletion");
} else if (existing.siteKey === "UNITEDSCO") {
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
console.log("[insuranceCreds] Cleared United SCO browser session after credential deletion");
}
} catch (seleniumErr) {
// Don't fail the delete if Selenium session clear fails
console.error("[insuranceCreds] Failed to clear Selenium session:", seleniumErr);
}
return res.status(204).send(); return res.status(204).send();
} catch (err) { } catch (err) {
return res return res

View File

@@ -136,12 +136,17 @@ async function handleDentaQuestCompletedJob(
// We'll wrap the processing in try/catch/finally so cleanup always runs // We'll wrap the processing in try/catch/finally so cleanup always runs
try { try {
// 1) ensuring memberid.
const insuranceEligibilityData = job.insuranceEligibilityData; const insuranceEligibilityData = job.insuranceEligibilityData;
const insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
// 1) Get Member ID - prefer the one extracted from the page by Selenium,
// since we now allow searching by name only
let insuranceId = String(seleniumResult?.memberId ?? "").trim();
if (!insuranceId) { if (!insuranceId) {
throw new Error("Missing memberId for DentaQuest job"); // Fallback to the one provided in the request
insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
} }
console.log(`[dentaquest-eligibility] Insurance ID: ${insuranceId || "(none)"}`);
// 2) Create or update patient (with name from selenium result if available) // 2) Create or update patient (with name from selenium result if available)
const patientNameFromResult = const patientNameFromResult =
@@ -149,23 +154,93 @@ async function handleDentaQuestCompletedJob(
? seleniumResult.patientName.trim() ? seleniumResult.patientName.trim()
: null; : null;
const { firstName, lastName } = splitName(patientNameFromResult); // Get name from request data as fallback
let firstName = insuranceEligibilityData.firstName || "";
let lastName = insuranceEligibilityData.lastName || "";
// Override with name from Selenium result if available
if (patientNameFromResult) {
const parsedName = splitName(patientNameFromResult);
firstName = parsedName.firstName || firstName;
lastName = parsedName.lastName || lastName;
}
await createOrUpdatePatientByInsuranceId({ // Create or update patient if we have an insurance ID
insuranceId, if (insuranceId) {
firstName, await createOrUpdatePatientByInsuranceId({
lastName, insuranceId,
dob: insuranceEligibilityData.dateOfBirth, firstName,
userId: job.userId, lastName,
}); dob: insuranceEligibilityData.dateOfBirth,
userId: job.userId,
});
} else {
console.log("[dentaquest-eligibility] No Member ID available - will try to find patient by name/DOB");
}
// 3) Update patient status + PDF upload // 3) Update patient status + PDF upload
const patient = await storage.getPatientByInsuranceId( // First try to find by insurance ID, then by name + DOB
insuranceEligibilityData.memberId let patient = insuranceId
); ? await storage.getPatientByInsuranceId(insuranceId)
: null;
// If not found by ID and we have name + DOB, try to find by those
if (!patient && firstName && lastName) {
console.log(`[dentaquest-eligibility] Looking up patient by name: ${firstName} ${lastName}`);
const patients = await storage.getPatientsByUserId(job.userId);
patient = patients.find(p =>
p.firstName?.toLowerCase() === firstName.toLowerCase() &&
p.lastName?.toLowerCase() === lastName.toLowerCase()
) || null;
// If found and we now have the insurance ID, update the patient record
if (patient && insuranceId) {
await storage.updatePatient(patient.id, { insuranceId });
console.log(`[dentaquest-eligibility] Updated patient ${patient.id} with insuranceId: ${insuranceId}`);
}
}
// If still no patient found, CREATE a new one with the data we have
if (!patient?.id && firstName && lastName) {
console.log(`[dentaquest-eligibility] Creating new patient: ${firstName} ${lastName}`);
const createPayload: any = {
firstName,
lastName,
dateOfBirth: insuranceEligibilityData.dateOfBirth || null,
gender: "",
phone: "",
userId: job.userId,
insuranceId: insuranceId || null,
};
try {
const patientData = insertPatientSchema.parse(createPayload);
const newPatient = await storage.createPatient(patientData);
if (newPatient) {
patient = newPatient;
console.log(`[dentaquest-eligibility] Created new patient with ID: ${patient.id}`);
}
} catch (err: any) {
// Try without dateOfBirth if it fails
try {
const safePayload = { ...createPayload };
delete safePayload.dateOfBirth;
const patientData = insertPatientSchema.parse(safePayload);
const newPatient = await storage.createPatient(patientData);
if (newPatient) {
patient = newPatient;
console.log(`[dentaquest-eligibility] Created new patient (no DOB) with ID: ${patient.id}`);
}
} catch (err2: any) {
console.error(`[dentaquest-eligibility] Failed to create patient: ${err2?.message}`);
}
}
}
if (!patient?.id) { if (!patient?.id) {
outputResult.patientUpdateStatus = outputResult.patientUpdateStatus =
"Patient not found; no update performed"; "Patient not found and could not be created";
return { return {
patientUpdateStatus: outputResult.patientUpdateStatus, patientUpdateStatus: outputResult.patientUpdateStatus,
pdfUploadStatus: "none", pdfUploadStatus: "none",

View File

@@ -135,6 +135,7 @@ async function handleUnitedSCOCompletedJob(
seleniumResult: any seleniumResult: any
) { ) {
let createdPdfFileId: number | null = null; let createdPdfFileId: number | null = null;
let generatedPdfPath: string | null = null;
const outputResult: any = {}; const outputResult: any = {};
// We'll wrap the processing in try/catch/finally so cleanup always runs // We'll wrap the processing in try/catch/finally so cleanup always runs
@@ -204,7 +205,6 @@ async function handleUnitedSCOCompletedJob(
// Handle PDF or convert screenshot -> pdf if available // Handle PDF or convert screenshot -> pdf if available
let pdfBuffer: Buffer | null = null; let pdfBuffer: Buffer | null = null;
let generatedPdfPath: string | null = null;
if ( if (
seleniumResult && seleniumResult &&
@@ -233,7 +233,8 @@ async function handleUnitedSCOCompletedJob(
// Convert image to PDF // Convert image to PDF
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path); pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
const pdfFileName = `unitedsco_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`; // Use insuranceId (which may come from Selenium result) for filename
const pdfFileName = `unitedsco_eligibility_${insuranceId || "unknown"}_${Date.now()}.pdf`;
generatedPdfPath = path.join( generatedPdfPath = path.join(
path.dirname(seleniumResult.ss_path), path.dirname(seleniumResult.ss_path),
pdfFileName pdfFileName
@@ -287,18 +288,25 @@ async function handleUnitedSCOCompletedJob(
"No valid PDF path provided by Selenium, Couldn't upload pdf to server."; "No valid PDF path provided by Selenium, Couldn't upload pdf to server.";
} }
// Get filename for frontend preview
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
return { return {
patientUpdateStatus: outputResult.patientUpdateStatus, patientUpdateStatus: outputResult.patientUpdateStatus,
pdfUploadStatus: outputResult.pdfUploadStatus, pdfUploadStatus: outputResult.pdfUploadStatus,
pdfFileId: createdPdfFileId, pdfFileId: createdPdfFileId,
pdfFilename,
}; };
} catch (err: any) { } catch (err: any) {
// Get filename for frontend preview if available
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
return { return {
patientUpdateStatus: outputResult.patientUpdateStatus, patientUpdateStatus: outputResult.patientUpdateStatus,
pdfUploadStatus: pdfUploadStatus:
outputResult.pdfUploadStatus ?? outputResult.pdfUploadStatus ??
`Failed to process United SCO job: ${err?.message ?? String(err)}`, `Failed to process United SCO job: ${err?.message ?? String(err)}`,
pdfFileId: createdPdfFileId, pdfFileId: createdPdfFileId,
pdfFilename,
error: err?.message ?? String(err), error: err?.message ?? String(err),
}; };
} finally { } finally {

View File

@@ -127,6 +127,11 @@ export function DentaQuestEligibilityButton({
const [isStarting, setIsStarting] = useState(false); const [isStarting, setIsStarting] = useState(false);
const [isSubmittingOtp, setIsSubmittingOtp] = useState(false); const [isSubmittingOtp, setIsSubmittingOtp] = useState(false);
// DentaQuest allows flexible search - only DOB is required, plus at least one identifier
// Can use: memberId, firstName, lastName, or any combination
const hasAnyIdentifier = memberId || firstName || lastName;
const isDentaQuestFormIncomplete = !dateOfBirth || !hasAnyIdentifier;
// Clean up socket on unmount // Clean up socket on unmount
useEffect(() => { useEffect(() => {
return () => { return () => {
@@ -370,10 +375,13 @@ export function DentaQuestEligibilityButton({
}; };
const startDentaQuestEligibility = async () => { const startDentaQuestEligibility = async () => {
if (!memberId || !dateOfBirth) { // Flexible search - DOB required plus at least one identifier
const hasAnyIdentifier = memberId || firstName || lastName;
if (!dateOfBirth || !hasAnyIdentifier) {
toast({ toast({
title: "Missing fields", title: "Missing fields",
description: "Member ID and Date of Birth are required.", description: "Please provide Date of Birth and at least one of: Member ID, First Name, or Last Name.",
variant: "destructive", variant: "destructive",
}); });
return; return;
@@ -538,7 +546,7 @@ export function DentaQuestEligibilityButton({
<Button <Button
className="w-full" className="w-full"
variant="outline" variant="outline"
disabled={isFormIncomplete || isStarting} disabled={isDentaQuestFormIncomplete || isStarting}
onClick={startDentaQuestEligibility} onClick={startDentaQuestEligibility}
> >
{isStarting ? ( {isStarting ? (

View File

@@ -19,6 +19,7 @@ const SITE_KEY_OPTIONS = [
{ value: "MH", label: "MassHealth" }, { value: "MH", label: "MassHealth" },
{ value: "DDMA", label: "Delta Dental MA" }, { value: "DDMA", label: "Delta Dental MA" },
{ value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" }, { value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" },
{ value: "UNITEDSCO", label: "United SCO" },
]; ];
export function CredentialForm({ onClose, userId, defaultValues }: CredentialFormProps) { export function CredentialForm({ onClose, userId, defaultValues }: CredentialFormProps) {

View File

@@ -18,6 +18,7 @@ const SITE_KEY_LABELS: Record<string, string> = {
MH: "MassHealth", MH: "MassHealth",
DDMA: "Delta Dental MA", DDMA: "Delta Dental MA",
DENTAQUEST: "Tufts SCO / DentaQuest", DENTAQUEST: "Tufts SCO / DentaQuest",
UNITEDSCO: "United SCO",
}; };
function getSiteKeyLabel(siteKey: string): string { function getSiteKeyLabel(siteKey: string): string {

View File

@@ -147,6 +147,28 @@ async def start_ddma_run(sid: str, data: dict, url: str):
s["last_activity"] = time.time() s["last_activity"] = time.time()
try: try:
# Check if OTP was submitted via API (from app)
otp_value = s.get("otp_value")
if otp_value:
print(f"[OTP] OTP received from app: {otp_value}")
try:
otp_input = driver.find_element(By.XPATH,
"//input[contains(@aria-label,'Verification') or contains(@placeholder,'verification') or @type='tel']"
)
otp_input.clear()
otp_input.send_keys(otp_value)
# Click verify button
try:
verify_btn = driver.find_element(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
verify_btn.click()
except:
otp_input.send_keys("\n") # Press Enter as fallback
print("[OTP] OTP typed and submitted via app")
s["otp_value"] = None # Clear so we don't submit again
await asyncio.sleep(3) # Wait for verification
except Exception as type_err:
print(f"[OTP] Failed to type OTP from app: {type_err}")
# Check current URL - if we're on member search page, login succeeded # Check current URL - if we're on member search page, login succeeded
current_url = driver.current_url.lower() current_url = driver.current_url.lower()
print(f"[OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...") print(f"[OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")

View File

@@ -146,6 +146,36 @@ async def start_dentaquest_run(sid: str, data: dict, url: str):
s["last_activity"] = time.time() s["last_activity"] = time.time()
try: try:
# Check if OTP was submitted via API (from app)
otp_value = s.get("otp_value")
if otp_value:
print(f"[DentaQuest OTP] OTP received from app: {otp_value}")
try:
otp_input = driver.find_element(By.XPATH,
"//input[contains(@name,'otp') or contains(@name,'code') or @type='tel' or contains(@aria-label,'Verification') or contains(@placeholder,'code')]"
)
otp_input.clear()
otp_input.send_keys(otp_value)
# Click verify button - use same pattern as Delta MA
try:
verify_btn = driver.find_element(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
verify_btn.click()
print("[DentaQuest OTP] Clicked verify button (aria-label)")
except:
try:
# Fallback: try other button patterns
verify_btn = driver.find_element(By.XPATH, "//button[contains(text(),'Verify') or contains(text(),'Submit') or @type='submit']")
verify_btn.click()
print("[DentaQuest OTP] Clicked verify button (text/type)")
except:
otp_input.send_keys("\n") # Press Enter as fallback
print("[DentaQuest OTP] Pressed Enter as fallback")
print("[DentaQuest OTP] OTP typed and submitted via app")
s["otp_value"] = None # Clear so we don't submit again
await asyncio.sleep(3) # Wait for verification
except Exception as type_err:
print(f"[DentaQuest OTP] Failed to type OTP from app: {type_err}")
# Check current URL - if we're on dashboard/member page, login succeeded # Check current URL - if we're on dashboard/member page, login succeeded
current_url = driver.current_url.lower() current_url = driver.current_url.lower()
print(f"[DentaQuest OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...") print(f"[DentaQuest OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")

View File

@@ -391,8 +391,8 @@ class AutomationDeltaDentalMAEligibilityCheck:
except: except:
pass pass
# 2) Extract patient name and click to navigate to detailed patient page # 2) Click on patient name to navigate to detailed patient page
print("[DDMA step2] Extracting patient name and finding detail link...") print("[DDMA step2] Clicking on patient name to open detailed page...")
patient_name_clicked = False patient_name_clicked = False
patientName = "" patientName = ""
@@ -400,29 +400,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
current_url_before = self.driver.current_url current_url_before = self.driver.current_url
print(f"[DDMA step2] Current URL before click: {current_url_before}") print(f"[DDMA step2] Current URL before click: {current_url_before}")
# Try to extract patient name from the first row of search results
# This is more reliable than extracting from link text
name_extraction_selectors = [
"(//tbody//tr)[1]//td[1]", # First column of first row (usually name)
"(//table//tbody//tr)[1]//td[1]", # Alternative table structure
"//table//tr[2]//td[1]", # Skip header row
"(//tbody//tr)[1]//td[contains(@class,'name')]", # Name column by class
"(//tbody//tr)[1]//a", # Link in first row (might contain name)
]
for selector in name_extraction_selectors:
try:
elem = self.driver.find_element(By.XPATH, selector)
text = elem.text.strip()
# Filter out non-name text
if text and len(text) > 1 and len(text) < 100:
if not any(x in text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'date', 'print', 'view', 'details', 'status']):
patientName = text
print(f"[DDMA step2] Extracted patient name from search results: '{patientName}'")
break
except Exception:
continue
# Try to find all links in the first row and print them for debugging # Try to find all links in the first row and print them for debugging
try: try:
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a") all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
@@ -431,11 +408,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
href = link.get_attribute("href") or "no-href" href = link.get_attribute("href") or "no-href"
text = link.text.strip() or "(empty text)" text = link.text.strip() or "(empty text)"
print(f" Link {i}: href={href[:80]}..., text={text}") print(f" Link {i}: href={href[:80]}..., text={text}")
# Also try to get name from link if we haven't found it yet
if not patientName and text and len(text) > 1:
if not any(x in text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'view', 'details']):
patientName = text
print(f"[DDMA step2] Got patient name from link text: '{patientName}'")
except Exception as e: except Exception as e:
print(f"[DDMA step2] Error listing links: {e}") print(f"[DDMA step2] Error listing links: {e}")
@@ -452,14 +424,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
patient_link = WebDriverWait(self.driver, 5).until( patient_link = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, selector)) EC.presence_of_element_located((By.XPATH, selector))
) )
link_text = patient_link.text.strip() patientName = patient_link.text.strip()
href = patient_link.get_attribute("href") href = patient_link.get_attribute("href")
print(f"[DDMA step2] Found patient link: text='{link_text}', href={href}") print(f"[DDMA step2] Found patient link: text='{patientName}', href={href}")
# Use link text as name if we don't have one yet
if not patientName and link_text and len(link_text) > 1:
if not any(x in link_text.lower() for x in ['active', 'inactive', 'view', 'details']):
patientName = link_text
if href and "member-details" in href: if href and "member-details" in href:
detail_url = href detail_url = href
@@ -540,70 +507,30 @@ class AutomationDeltaDentalMAEligibilityCheck:
# Try to extract patient name from detailed page if not already found # Try to extract patient name from detailed page if not already found
if not patientName: if not patientName:
detail_name_selectors = [ detail_name_selectors = [
"//*[contains(@class,'member-name')]", "//h1",
"//*[contains(@class,'patient-name')]", "//h2",
"//h1[not(contains(@class,'page-title'))]", "//*[contains(@class,'patient-name') or contains(@class,'member-name')]",
"//h2[not(contains(@class,'section-title'))]", "//div[contains(@class,'header')]//span",
"//div[contains(@class,'header')]//span[string-length(text()) > 2]",
"//div[contains(@class,'member-info')]//span",
"//div[contains(@class,'patient-info')]//span",
"//span[contains(@class,'name')]",
] ]
for selector in detail_name_selectors: for selector in detail_name_selectors:
try: try:
name_elem = self.driver.find_element(By.XPATH, selector) name_elem = self.driver.find_element(By.XPATH, selector)
name_text = name_elem.text.strip() name_text = name_elem.text.strip()
if name_text and len(name_text) > 2 and len(name_text) < 100: if name_text and len(name_text) > 1:
# Filter out common non-name text if not any(x in name_text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'date', 'print']):
skip_words = ['active', 'inactive', 'eligible', 'search', 'date', 'print',
'view', 'details', 'member', 'patient', 'status', 'eligibility',
'welcome', 'home', 'logout', 'menu', 'close', 'expand']
if not any(x in name_text.lower() for x in skip_words):
patientName = name_text patientName = name_text
print(f"[DDMA step2] Found patient name on detail page: {patientName}") print(f"[DDMA step2] Found patient name on detail page: {patientName}")
break break
except: except:
continue continue
# As a last resort, try to find name in page text using patterns
if not patientName:
try:
# Look for text that looks like a name (First Last format)
import re
page_text = self.driver.find_element(By.TAG_NAME, "body").text
# Look for "Member Name:" or "Patient Name:" followed by text
name_patterns = [
r'Member Name[:\s]+([A-Z][a-z]+\s+[A-Z][a-z]+)',
r'Patient Name[:\s]+([A-Z][a-z]+\s+[A-Z][a-z]+)',
r'Name[:\s]+([A-Z][a-z]+\s+[A-Z][a-z]+)',
]
for pattern in name_patterns:
match = re.search(pattern, page_text, re.IGNORECASE)
if match:
patientName = match.group(1).strip()
print(f"[DDMA step2] Found patient name via pattern match: {patientName}")
break
except:
pass
else: else:
print("[DDMA step2] Warning: Could not click on patient, capturing search results page") print("[DDMA step2] Warning: Could not click on patient, capturing search results page")
# Still try to get patient name from search results # Still try to get patient name from search results
if not patientName: try:
name_selectors = [ name_elem = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]//td[1]")
"(//tbody//tr)[1]//td[1]", # First column of first row patientName = name_elem.text.strip()
"(//table//tbody//tr)[1]//td[1]", except:
"(//tbody//tr)[1]//a", # Link in first row pass
]
for selector in name_selectors:
try:
name_elem = self.driver.find_element(By.XPATH, selector)
text = name_elem.text.strip()
if text and len(text) > 1 and not any(x in text.lower() for x in ['active', 'inactive', 'view', 'details']):
patientName = text
print(f"[DDMA step2] Got patient name from search results: {patientName}")
break
except:
continue
if not patientName: if not patientName:
print("[DDMA step2] Could not extract patient name") print("[DDMA step2] Could not extract patient name")

View File

@@ -3,6 +3,7 @@ from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys 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.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.chrome import ChromeDriverManager
@@ -22,6 +23,8 @@ class AutomationDentaQuestEligibilityCheck:
# Flatten values for convenience # Flatten values for convenience
self.memberId = self.data.get("memberId", "") self.memberId = self.data.get("memberId", "")
self.dateOfBirth = self.data.get("dateOfBirth", "") self.dateOfBirth = self.data.get("dateOfBirth", "")
self.firstName = self.data.get("firstName", "")
self.lastName = self.data.get("lastName", "")
self.dentaquest_username = self.data.get("dentaquestUsername", "") self.dentaquest_username = self.data.get("dentaquestUsername", "")
self.dentaquest_password = self.data.get("dentaquestPassword", "") self.dentaquest_password = self.data.get("dentaquestPassword", "")
@@ -247,11 +250,20 @@ class AutomationDentaQuestEligibilityCheck:
return f"ERROR:LOGIN FAILED: {e}" return f"ERROR:LOGIN FAILED: {e}"
def step1(self): def step1(self):
"""Navigate to member search and enter member ID + DOB""" """Navigate to member search - fills all available fields (Member ID, First Name, Last Name, DOB)"""
wait = WebDriverWait(self.driver, 30) wait = WebDriverWait(self.driver, 30)
try: try:
print(f"[DentaQuest step1] Starting member search for ID: {self.memberId}, DOB: {self.dateOfBirth}") # Log what fields are available for search
fields = []
if self.memberId:
fields.append(f"ID: {self.memberId}")
if self.firstName:
fields.append(f"FirstName: {self.firstName}")
if self.lastName:
fields.append(f"LastName: {self.lastName}")
fields.append(f"DOB: {self.dateOfBirth}")
print(f"[DentaQuest step1] Starting member search with: {', '.join(fields)}")
# Wait for page to be ready # Wait for page to be ready
time.sleep(2) time.sleep(2)
@@ -267,14 +279,6 @@ class AutomationDentaQuestEligibilityCheck:
print(f"[DentaQuest step1] Error parsing DOB: {e}") print(f"[DentaQuest step1] Error parsing DOB: {e}")
return "ERROR: PARSING DOB" return "ERROR: PARSING DOB"
# Get today's date for Date of Service
from datetime import datetime
today = datetime.now()
service_month = str(today.month).zfill(2)
service_day = str(today.day).zfill(2)
service_year = str(today.year)
print(f"[DentaQuest step1] Service date: {service_month}/{service_day}/{service_year}")
# Helper function to fill contenteditable date spans within a specific container # Helper function to fill contenteditable date spans within a specific container
def fill_date_by_testid(testid, month_val, day_val, year_val, field_name): def fill_date_by_testid(testid, month_val, day_val, year_val, field_name):
try: try:
@@ -285,55 +289,127 @@ class AutomationDentaQuestEligibilityCheck:
def replace_with_sendkeys(el, value): def replace_with_sendkeys(el, value):
el.click() el.click()
time.sleep(0.1) time.sleep(0.05)
# Clear existing content
el.send_keys(Keys.CONTROL, "a") el.send_keys(Keys.CONTROL, "a")
time.sleep(0.05)
el.send_keys(Keys.BACKSPACE) el.send_keys(Keys.BACKSPACE)
time.sleep(0.05)
# Type new value
el.send_keys(value) el.send_keys(value)
time.sleep(0.1)
# Fill month
replace_with_sendkeys(month_elem, month_val) replace_with_sendkeys(month_elem, month_val)
# Tab to day field
month_elem.send_keys(Keys.TAB)
time.sleep(0.1) time.sleep(0.1)
# Fill day
replace_with_sendkeys(day_elem, day_val) replace_with_sendkeys(day_elem, day_val)
# Tab to year field
day_elem.send_keys(Keys.TAB)
time.sleep(0.1) time.sleep(0.1)
# Fill year
replace_with_sendkeys(year_elem, year_val) replace_with_sendkeys(year_elem, year_val)
# Tab out of the field to trigger validation
year_elem.send_keys(Keys.TAB)
time.sleep(0.2)
print(f"[DentaQuest step1] Filled {field_name}: {month_val}/{day_val}/{year_val}") print(f"[DentaQuest step1] Filled {field_name}: {month_val}/{day_val}/{year_val}")
return True return True
except Exception as e: except Exception as e:
print(f"[DentaQuest step1] Error filling {field_name}: {e}") print(f"[DentaQuest step1] Error filling {field_name}: {e}")
return False return False
# 1. Fill Date of Service with TODAY's date using specific data-testid # 1. Select Provider from dropdown (required field)
fill_date_by_testid("member-search_date-of-service", service_month, service_day, service_year, "Date of Service") try:
time.sleep(0.5) print("[DentaQuest step1] Selecting Provider...")
# Try to find and click Provider dropdown
provider_selectors = [
"//label[contains(text(),'Provider')]/following-sibling::*//div[contains(@class,'select')]",
"//div[contains(@data-testid,'provider')]//div[contains(@class,'select')]",
"//*[@aria-label='Provider']",
"//select[contains(@name,'provider') or contains(@id,'provider')]",
"//div[contains(@class,'provider')]//input",
"//label[contains(text(),'Provider')]/..//div[contains(@class,'control')]"
]
provider_clicked = False
for selector in provider_selectors:
try:
provider_dropdown = WebDriverWait(self.driver, 3).until(
EC.element_to_be_clickable((By.XPATH, selector))
)
provider_dropdown.click()
print(f"[DentaQuest step1] Clicked provider dropdown with selector: {selector}")
time.sleep(0.5)
provider_clicked = True
break
except TimeoutException:
continue
if provider_clicked:
# Select first available provider option
option_selectors = [
"//div[contains(@class,'option') and not(contains(@class,'disabled'))]",
"//li[contains(@class,'option')]",
"//option[not(@disabled)]",
"//*[@role='option']"
]
for opt_selector in option_selectors:
try:
options = self.driver.find_elements(By.XPATH, opt_selector)
if options:
# Select first non-placeholder option
for opt in options:
opt_text = opt.text.strip()
if opt_text and "select" not in opt_text.lower():
opt.click()
print(f"[DentaQuest step1] Selected provider: {opt_text}")
break
break
except:
continue
# Close dropdown if still open
ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
time.sleep(0.3)
else:
print("[DentaQuest step1] Warning: Could not find Provider dropdown")
except Exception as e:
print(f"[DentaQuest step1] Error selecting provider: {e}")
time.sleep(0.3)
# 2. Fill Date of Birth with patient's DOB using specific data-testid # 2. Fill Date of Birth with patient's DOB using specific data-testid
fill_date_by_testid("member-search_date-of-birth", dob_month, dob_day, dob_year, "Date of Birth") fill_date_by_testid("member-search_date-of-birth", dob_month, dob_day, dob_year, "Date of Birth")
time.sleep(0.5) time.sleep(0.3)
# 3. Fill Member ID # 3. Fill ALL available search fields (flexible search)
member_id_input = wait.until(EC.presence_of_element_located( # Fill Member ID if provided
(By.XPATH, '//input[@placeholder="Search by member ID"]') if self.memberId:
)) try:
member_id_input.clear() member_id_input = wait.until(EC.presence_of_element_located(
member_id_input.send_keys(self.memberId) (By.XPATH, '//input[@placeholder="Search by member ID"]')
print(f"[DentaQuest step1] Entered member ID: {self.memberId}") ))
member_id_input.clear()
member_id_input.send_keys(self.memberId)
print(f"[DentaQuest step1] Entered member ID: {self.memberId}")
time.sleep(0.2)
except Exception as e:
print(f"[DentaQuest step1] Warning: Could not fill member ID: {e}")
# Fill First Name if provided
if self.firstName:
try:
first_name_input = wait.until(EC.presence_of_element_located(
(By.XPATH, '//input[@placeholder="First name - 1 char minimum" or contains(@placeholder,"first name") or contains(@name,"firstName") or contains(@id,"firstName")]')
))
first_name_input.clear()
first_name_input.send_keys(self.firstName)
print(f"[DentaQuest step1] Entered first name: {self.firstName}")
time.sleep(0.2)
except Exception as e:
print(f"[DentaQuest step1] Warning: Could not fill first name: {e}")
# Fill Last Name if provided
if self.lastName:
try:
last_name_input = wait.until(EC.presence_of_element_located(
(By.XPATH, '//input[@placeholder="Last name - 2 char minimum" or contains(@placeholder,"last name") or contains(@name,"lastName") or contains(@id,"lastName")]')
))
last_name_input.clear()
last_name_input.send_keys(self.lastName)
print(f"[DentaQuest step1] Entered last name: {self.lastName}")
time.sleep(0.2)
except Exception as e:
print(f"[DentaQuest step1] Warning: Could not fill last name: {e}")
time.sleep(0.3) time.sleep(0.3)
@@ -351,7 +427,8 @@ class AutomationDentaQuestEligibilityCheck:
search_btn.click() search_btn.click()
print("[DentaQuest step1] Clicked search button (fallback)") print("[DentaQuest step1] Clicked search button (fallback)")
except: except:
member_id_input.send_keys(Keys.RETURN) # Press Enter on the last input field
ActionChains(self.driver).send_keys(Keys.RETURN).perform()
print("[DentaQuest step1] Pressed Enter to search") print("[DentaQuest step1] Pressed Enter to search")
time.sleep(5) time.sleep(5)
@@ -363,7 +440,7 @@ class AutomationDentaQuestEligibilityCheck:
)) ))
if error_msg and error_msg.is_displayed(): if error_msg and error_msg.is_displayed():
print("[DentaQuest step1] No results found") print("[DentaQuest step1] No results found")
return "ERROR: INVALID MEMBERID OR DOB" return "ERROR: INVALID SEARCH CRITERIA"
except TimeoutException: except TimeoutException:
pass pass
@@ -390,8 +467,54 @@ class AutomationDentaQuestEligibilityCheck:
except TimeoutException: except TimeoutException:
print("[DentaQuest step2] Warning: Results table not found within timeout") print("[DentaQuest step2] Warning: Results table not found within timeout")
# 1) Find and extract eligibility status from search results # 1) Find and extract eligibility status and Member ID from search results
eligibilityText = "unknown" eligibilityText = "unknown"
foundMemberId = ""
# Try to extract Member ID from the search results
import re
try:
# Look for Member ID in various places
member_id_selectors = [
"(//tbody//tr)[1]//td[contains(text(),'ID:') or contains(@data-testid,'member-id')]",
"//span[contains(text(),'Member ID')]/..//span",
"//div[contains(text(),'Member ID')]",
"(//tbody//tr)[1]//td[2]", # Often Member ID is in second column
]
page_text = self.driver.find_element(By.TAG_NAME, "body").text
# Try regex patterns to find Member ID
patterns = [
r'Member ID[:\s]+([A-Z0-9]+)',
r'ID[:\s]+([A-Z0-9]{5,})',
r'MemberID[:\s]+([A-Z0-9]+)',
]
for pattern in patterns:
match = re.search(pattern, page_text, re.IGNORECASE)
if match:
foundMemberId = match.group(1).strip()
print(f"[DentaQuest step2] Extracted Member ID: {foundMemberId}")
break
if not foundMemberId:
for selector in member_id_selectors:
try:
elem = self.driver.find_element(By.XPATH, selector)
text = elem.text.strip()
# Extract just the ID part
id_match = re.search(r'([A-Z0-9]{5,})', text)
if id_match:
foundMemberId = id_match.group(1)
print(f"[DentaQuest step2] Found Member ID via selector: {foundMemberId}")
break
except:
continue
except Exception as e:
print(f"[DentaQuest step2] Error extracting Member ID: {e}")
# Extract eligibility status
status_selectors = [ status_selectors = [
"(//tbody//tr)[1]//a[contains(@href, 'eligibility')]", "(//tbody//tr)[1]//a[contains(@href, 'eligibility')]",
"//a[contains(@href,'eligibility')]", "//a[contains(@href,'eligibility')]",
@@ -424,25 +547,6 @@ class AutomationDentaQuestEligibilityCheck:
current_url_before = self.driver.current_url current_url_before = self.driver.current_url
print(f"[DentaQuest step2] Current URL before: {current_url_before}") print(f"[DentaQuest step2] Current URL before: {current_url_before}")
# Try to extract patient name from search results first
name_extraction_selectors = [
"(//tbody//tr)[1]//td[1]", # First column of first row
"(//table//tbody//tr)[1]//td[1]",
"//table//tr[2]//td[1]", # Skip header row
"(//tbody//tr)[1]//a", # Link in first row
]
for selector in name_extraction_selectors:
try:
elem = self.driver.find_element(By.XPATH, selector)
text = elem.text.strip()
if text and len(text) > 1 and len(text) < 100:
if not any(x in text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'view', 'details', 'status']):
patientName = text
print(f"[DentaQuest step2] Extracted patient name from search results: '{patientName}'")
break
except:
continue
# Find all links in first row and log them # Find all links in first row and log them
try: try:
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a") all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
@@ -619,7 +723,8 @@ class AutomationDentaQuestEligibilityCheck:
"eligibility": eligibilityText, "eligibility": eligibilityText,
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility "ss_path": pdf_path, # Keep key as ss_path for backward compatibility
"pdf_path": pdf_path, # Also add explicit pdf_path "pdf_path": pdf_path, # Also add explicit pdf_path
"patientName": patientName "patientName": patientName,
"memberId": foundMemberId # Member ID extracted from the page
} }
print(f"[DentaQuest step2] Success: {output}") print(f"[DentaQuest step2] Success: {output}")
return output return output

View File

@@ -267,13 +267,9 @@ class AutomationUnitedSCOEligibilityCheck:
""" """
Navigate to Eligibility page and fill the Patient Information form. Navigate to Eligibility page and fill the Patient Information form.
FLEXIBLE INPUT SUPPORT: Workflow based on actual DOM testing:
- If Member ID is provided: Fill Subscriber ID + DOB (+ optional First/Last Name)
- If no Member ID but First/Last Name provided: Fill First Name + Last Name + DOB
Workflow:
1. Navigate directly to eligibility page 1. Navigate directly to eligibility page
2. Fill available fields based on input 2. Fill First Name (id='firstName_Back'), Last Name (id='lastName_Back'), DOB (id='dateOfBirth_Back')
3. Select Payer: "UnitedHealthcare Massachusetts" from ng-select dropdown 3. Select Payer: "UnitedHealthcare Massachusetts" from ng-select dropdown
4. Click Continue 4. Click Continue
5. Handle Practitioner & Location page - click paymentGroupId dropdown, select Summit Dental Care 5. Handle Practitioner & Location page - click paymentGroupId dropdown, select Summit Dental Care
@@ -282,17 +278,7 @@ class AutomationUnitedSCOEligibilityCheck:
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
try: try:
# Determine which input mode to use print(f"[UnitedSCO step1] Starting eligibility search for: {self.firstName} {self.lastName}, DOB: {self.dateOfBirth}")
has_member_id = bool(self.memberId and self.memberId.strip())
has_name = bool(self.firstName and self.firstName.strip() and self.lastName and self.lastName.strip())
if has_member_id:
print(f"[UnitedSCO step1] Using Member ID mode: ID={self.memberId}, DOB={self.dateOfBirth}")
elif has_name:
print(f"[UnitedSCO step1] Using Name mode: {self.firstName} {self.lastName}, DOB={self.dateOfBirth}")
else:
print("[UnitedSCO step1] ERROR: Need either Member ID or First Name + Last Name")
return "ERROR: Missing required input (Member ID or Name)"
# Navigate directly to eligibility page # Navigate directly to eligibility page
print("[UnitedSCO step1] Navigating to eligibility page...") print("[UnitedSCO step1] Navigating to eligibility page...")
@@ -305,51 +291,37 @@ class AutomationUnitedSCOEligibilityCheck:
# Step 1.1: Fill the Patient Information form # Step 1.1: Fill the Patient Information form
print("[UnitedSCO step1] Filling Patient Information form...") print("[UnitedSCO step1] Filling Patient Information form...")
# Wait for form to load # Wait for form to load - look for First Name field (id='firstName_Back')
try: try:
WebDriverWait(self.driver, 10).until( WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.ID, "subscriberId_Front")) EC.presence_of_element_located((By.ID, "firstName_Back"))
) )
print("[UnitedSCO step1] Patient Information form loaded") print("[UnitedSCO step1] Patient Information form loaded")
except TimeoutException: except TimeoutException:
print("[UnitedSCO step1] Patient Information form not found") print("[UnitedSCO step1] Patient Information form not found")
return "ERROR: Patient Information form not found" return "ERROR: Patient Information form not found"
# Fill Subscriber ID if provided (id='subscriberId_Front') # Fill First Name (id='firstName_Back')
if has_member_id: try:
try: first_name_input = self.driver.find_element(By.ID, "firstName_Back")
subscriber_id_input = self.driver.find_element(By.ID, "subscriberId_Front") first_name_input.clear()
subscriber_id_input.clear() first_name_input.send_keys(self.firstName)
subscriber_id_input.send_keys(self.memberId) print(f"[UnitedSCO step1] Entered First Name: {self.firstName}")
print(f"[UnitedSCO step1] Entered Subscriber ID: {self.memberId}") except Exception as e:
except Exception as e: print(f"[UnitedSCO step1] Error entering First Name: {e}")
print(f"[UnitedSCO step1] Error entering Subscriber ID: {e}") return "ERROR: Could not enter First Name"
# Fill First Name if provided (id='firstName_Back') # Fill Last Name (id='lastName_Back')
if self.firstName and self.firstName.strip(): try:
try: last_name_input = self.driver.find_element(By.ID, "lastName_Back")
first_name_input = self.driver.find_element(By.ID, "firstName_Back") last_name_input.clear()
first_name_input.clear() last_name_input.send_keys(self.lastName)
first_name_input.send_keys(self.firstName) print(f"[UnitedSCO step1] Entered Last Name: {self.lastName}")
print(f"[UnitedSCO step1] Entered First Name: {self.firstName}") except Exception as e:
except Exception as e: print(f"[UnitedSCO step1] Error entering Last Name: {e}")
print(f"[UnitedSCO step1] Error entering First Name: {e}") return "ERROR: Could not enter Last Name"
if not has_member_id: # Only fail if we're relying on name
return "ERROR: Could not enter First Name"
# Fill Last Name if provided (id='lastName_Back') # Fill Date of Birth (id='dateOfBirth_Back', format: MM/DD/YYYY)
if self.lastName and self.lastName.strip():
try:
last_name_input = self.driver.find_element(By.ID, "lastName_Back")
last_name_input.clear()
last_name_input.send_keys(self.lastName)
print(f"[UnitedSCO step1] Entered Last Name: {self.lastName}")
except Exception as e:
print(f"[UnitedSCO step1] Error entering Last Name: {e}")
if not has_member_id: # Only fail if we're relying on name
return "ERROR: Could not enter Last Name"
# Fill Date of Birth (id='dateOfBirth_Back', format: MM/DD/YYYY) - always required
try: try:
dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back") dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back")
dob_input.clear() dob_input.clear()