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
*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);
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 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);
} catch (err) {
return res
@@ -115,6 +140,25 @@ router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
.status(404)
.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();
} catch (err) {
return res

View File

@@ -136,12 +136,17 @@ async function handleDentaQuestCompletedJob(
// We'll wrap the processing in try/catch/finally so cleanup always runs
try {
// 1) ensuring memberid.
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) {
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)
const patientNameFromResult =
@@ -149,23 +154,93 @@ async function handleDentaQuestCompletedJob(
? seleniumResult.patientName.trim()
: 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({
insuranceId,
firstName,
lastName,
dob: insuranceEligibilityData.dateOfBirth,
userId: job.userId,
});
// Create or update patient if we have an insurance ID
if (insuranceId) {
await createOrUpdatePatientByInsuranceId({
insuranceId,
firstName,
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
const patient = await storage.getPatientByInsuranceId(
insuranceEligibilityData.memberId
);
// First try to find by insurance ID, then by name + DOB
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) {
outputResult.patientUpdateStatus =
"Patient not found; no update performed";
"Patient not found and could not be created";
return {
patientUpdateStatus: outputResult.patientUpdateStatus,
pdfUploadStatus: "none",

View File

@@ -135,6 +135,7 @@ async function handleUnitedSCOCompletedJob(
seleniumResult: any
) {
let createdPdfFileId: number | null = null;
let generatedPdfPath: string | null = null;
const outputResult: any = {};
// 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
let pdfBuffer: Buffer | null = null;
let generatedPdfPath: string | null = null;
if (
seleniumResult &&
@@ -233,7 +233,8 @@ async function handleUnitedSCOCompletedJob(
// Convert image to PDF
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(
path.dirname(seleniumResult.ss_path),
pdfFileName
@@ -287,18 +288,25 @@ async function handleUnitedSCOCompletedJob(
"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 {
patientUpdateStatus: outputResult.patientUpdateStatus,
pdfUploadStatus: outputResult.pdfUploadStatus,
pdfFileId: createdPdfFileId,
pdfFilename,
};
} catch (err: any) {
// Get filename for frontend preview if available
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
return {
patientUpdateStatus: outputResult.patientUpdateStatus,
pdfUploadStatus:
outputResult.pdfUploadStatus ??
`Failed to process United SCO job: ${err?.message ?? String(err)}`,
pdfFileId: createdPdfFileId,
pdfFilename,
error: err?.message ?? String(err),
};
} finally {

View File

@@ -127,6 +127,11 @@ export function DentaQuestEligibilityButton({
const [isStarting, setIsStarting] = 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
useEffect(() => {
return () => {
@@ -370,10 +375,13 @@ export function DentaQuestEligibilityButton({
};
const startDentaQuestEligibility = async () => {
if (!memberId || !dateOfBirth) {
// Flexible search - DOB required plus at least one identifier
const hasAnyIdentifier = memberId || firstName || lastName;
if (!dateOfBirth || !hasAnyIdentifier) {
toast({
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",
});
return;
@@ -538,7 +546,7 @@ export function DentaQuestEligibilityButton({
<Button
className="w-full"
variant="outline"
disabled={isFormIncomplete || isStarting}
disabled={isDentaQuestFormIncomplete || isStarting}
onClick={startDentaQuestEligibility}
>
{isStarting ? (

View File

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

View File

@@ -18,6 +18,7 @@ const SITE_KEY_LABELS: Record<string, string> = {
MH: "MassHealth",
DDMA: "Delta Dental MA",
DENTAQUEST: "Tufts SCO / DentaQuest",
UNITEDSCO: "United SCO",
};
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()
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
current_url = driver.current_url.lower()
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()
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
current_url = driver.current_url.lower()
print(f"[DentaQuest OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")

View File

@@ -391,8 +391,8 @@ class AutomationDeltaDentalMAEligibilityCheck:
except:
pass
# 2) Extract patient name and click to navigate to detailed patient page
print("[DDMA step2] Extracting patient name and finding detail link...")
# 2) Click on patient name to navigate to detailed patient page
print("[DDMA step2] Clicking on patient name to open detailed page...")
patient_name_clicked = False
patientName = ""
@@ -400,29 +400,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
current_url_before = self.driver.current_url
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:
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"
text = link.text.strip() or "(empty 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:
print(f"[DDMA step2] Error listing links: {e}")
@@ -452,14 +424,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
patient_link = WebDriverWait(self.driver, 5).until(
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")
print(f"[DDMA step2] Found patient link: text='{link_text}', 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
print(f"[DDMA step2] Found patient link: text='{patientName}', href={href}")
if href and "member-details" in href:
detail_url = href
@@ -540,70 +507,30 @@ class AutomationDeltaDentalMAEligibilityCheck:
# Try to extract patient name from detailed page if not already found
if not patientName:
detail_name_selectors = [
"//*[contains(@class,'member-name')]",
"//*[contains(@class,'patient-name')]",
"//h1[not(contains(@class,'page-title'))]",
"//h2[not(contains(@class,'section-title'))]",
"//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')]",
"//h1",
"//h2",
"//*[contains(@class,'patient-name') or contains(@class,'member-name')]",
"//div[contains(@class,'header')]//span",
]
for selector in detail_name_selectors:
try:
name_elem = self.driver.find_element(By.XPATH, selector)
name_text = name_elem.text.strip()
if name_text and len(name_text) > 2 and len(name_text) < 100:
# Filter out common non-name text
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):
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:
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:
print("[DDMA step2] Warning: Could not click on patient, capturing search results page")
# Still try to get patient name from search results
if not patientName:
name_selectors = [
"(//tbody//tr)[1]//td[1]", # First column of first row
"(//table//tbody//tr)[1]//td[1]",
"(//tbody//tr)[1]//a", # Link in first row
]
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
try:
name_elem = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]//td[1]")
patientName = name_elem.text.strip()
except:
pass
if not patientName:
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.common.by import By
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 import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager
@@ -22,6 +23,8 @@ class AutomationDentaQuestEligibilityCheck:
# Flatten values for convenience
self.memberId = self.data.get("memberId", "")
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_password = self.data.get("dentaquestPassword", "")
@@ -247,11 +250,20 @@ class AutomationDentaQuestEligibilityCheck:
return f"ERROR:LOGIN FAILED: {e}"
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)
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
time.sleep(2)
@@ -267,14 +279,6 @@ class AutomationDentaQuestEligibilityCheck:
print(f"[DentaQuest step1] Error parsing DOB: {e}")
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
def fill_date_by_testid(testid, month_val, day_val, year_val, field_name):
try:
@@ -285,55 +289,127 @@ class AutomationDentaQuestEligibilityCheck:
def replace_with_sendkeys(el, value):
el.click()
time.sleep(0.1)
# Clear existing content
time.sleep(0.05)
el.send_keys(Keys.CONTROL, "a")
time.sleep(0.05)
el.send_keys(Keys.BACKSPACE)
time.sleep(0.05)
# Type new value
el.send_keys(value)
time.sleep(0.1)
# Fill month
replace_with_sendkeys(month_elem, month_val)
# Tab to day field
month_elem.send_keys(Keys.TAB)
time.sleep(0.1)
# Fill day
replace_with_sendkeys(day_elem, day_val)
# Tab to year field
day_elem.send_keys(Keys.TAB)
time.sleep(0.1)
# Fill year
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}")
return True
except Exception as e:
print(f"[DentaQuest step1] Error filling {field_name}: {e}")
return False
# 1. Fill Date of Service with TODAY's date using specific data-testid
fill_date_by_testid("member-search_date-of-service", service_month, service_day, service_year, "Date of Service")
time.sleep(0.5)
# 1. Select Provider from dropdown (required field)
try:
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
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
member_id_input = wait.until(EC.presence_of_element_located(
(By.XPATH, '//input[@placeholder="Search by member ID"]')
))
member_id_input.clear()
member_id_input.send_keys(self.memberId)
print(f"[DentaQuest step1] Entered member ID: {self.memberId}")
# 3. Fill ALL available search fields (flexible search)
# Fill Member ID if provided
if self.memberId:
try:
member_id_input = wait.until(EC.presence_of_element_located(
(By.XPATH, '//input[@placeholder="Search by member ID"]')
))
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)
@@ -351,7 +427,8 @@ class AutomationDentaQuestEligibilityCheck:
search_btn.click()
print("[DentaQuest step1] Clicked search button (fallback)")
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")
time.sleep(5)
@@ -363,7 +440,7 @@ class AutomationDentaQuestEligibilityCheck:
))
if error_msg and error_msg.is_displayed():
print("[DentaQuest step1] No results found")
return "ERROR: INVALID MEMBERID OR DOB"
return "ERROR: INVALID SEARCH CRITERIA"
except TimeoutException:
pass
@@ -390,8 +467,54 @@ class AutomationDentaQuestEligibilityCheck:
except TimeoutException:
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"
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 = [
"(//tbody//tr)[1]//a[contains(@href, 'eligibility')]",
"//a[contains(@href,'eligibility')]",
@@ -424,25 +547,6 @@ class AutomationDentaQuestEligibilityCheck:
current_url_before = self.driver.current_url
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
try:
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
@@ -619,7 +723,8 @@ class AutomationDentaQuestEligibilityCheck:
"eligibility": eligibilityText,
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility
"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}")
return output

View File

@@ -267,13 +267,9 @@ class AutomationUnitedSCOEligibilityCheck:
"""
Navigate to Eligibility page and fill the Patient Information form.
FLEXIBLE INPUT SUPPORT:
- 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:
Workflow based on actual DOM testing:
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
4. Click Continue
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
try:
# Determine which input mode to use
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)"
print(f"[UnitedSCO step1] Starting eligibility search for: {self.firstName} {self.lastName}, DOB: {self.dateOfBirth}")
# Navigate directly to eligibility page
print("[UnitedSCO step1] Navigating to eligibility page...")
@@ -305,51 +291,37 @@ class AutomationUnitedSCOEligibilityCheck:
# Step 1.1: Fill the 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:
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")
except TimeoutException:
print("[UnitedSCO step1] Patient Information form not found")
return "ERROR: Patient Information form not found"
# Fill Subscriber ID if provided (id='subscriberId_Front')
if has_member_id:
try:
subscriber_id_input = self.driver.find_element(By.ID, "subscriberId_Front")
subscriber_id_input.clear()
subscriber_id_input.send_keys(self.memberId)
print(f"[UnitedSCO step1] Entered Subscriber ID: {self.memberId}")
except Exception as e:
print(f"[UnitedSCO step1] Error entering Subscriber ID: {e}")
# Fill First Name (id='firstName_Back')
try:
first_name_input = self.driver.find_element(By.ID, "firstName_Back")
first_name_input.clear()
first_name_input.send_keys(self.firstName)
print(f"[UnitedSCO step1] Entered First Name: {self.firstName}")
except Exception as e:
print(f"[UnitedSCO step1] Error entering First Name: {e}")
return "ERROR: Could not enter First Name"
# Fill First Name if provided (id='firstName_Back')
if self.firstName and self.firstName.strip():
try:
first_name_input = self.driver.find_element(By.ID, "firstName_Back")
first_name_input.clear()
first_name_input.send_keys(self.firstName)
print(f"[UnitedSCO step1] Entered First Name: {self.firstName}")
except Exception as e:
print(f"[UnitedSCO step1] Error entering First Name: {e}")
if not has_member_id: # Only fail if we're relying on name
return "ERROR: Could not enter First Name"
# Fill Last Name (id='lastName_Back')
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}")
return "ERROR: Could not enter Last Name"
# Fill Last Name if provided (id='lastName_Back')
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
# Fill Date of Birth (id='dateOfBirth_Back', format: MM/DD/YYYY)
try:
dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back")
dob_input.clear()