feat(eligibility-check) - enhance OTP handling and eligibility status retrieval for DDMA and DentaQuest; improved file processing logic for screenshots and PDFs, and updated frontend components for better user experience

This commit is contained in:
2026-01-29 21:25:18 -05:00
parent 279a6b8dbc
commit 5370a0e445
8 changed files with 737 additions and 309 deletions

View File

@@ -179,43 +179,56 @@ async function handleDdmaCompletedJob(
await storage.updatePatient(patient.id, { status: newStatus });
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// convert screenshot -> pdf if available
// Handle PDF or convert screenshot -> pdf if available
let pdfBuffer: Buffer | null = null;
let generatedPdfPath: string | null = null;
if (
seleniumResult &&
seleniumResult.ss_path &&
typeof seleniumResult.ss_path === "string" &&
(seleniumResult.ss_path.endsWith(".png") ||
seleniumResult.ss_path.endsWith(".jpg") ||
seleniumResult.ss_path.endsWith(".jpeg"))
typeof seleniumResult.ss_path === "string"
) {
try {
if (!fsSync.existsSync(seleniumResult.ss_path)) {
throw new Error(
`Screenshot file not found: ${seleniumResult.ss_path}`
`File not found: ${seleniumResult.ss_path}`
);
}
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
// Check if the file is already a PDF (from Page.printToPDF)
if (seleniumResult.ss_path.endsWith(".pdf")) {
// Read PDF directly
pdfBuffer = await fs.readFile(seleniumResult.ss_path);
generatedPdfPath = seleniumResult.ss_path;
seleniumResult.pdf_path = generatedPdfPath;
console.log(`[ddma-eligibility] Using PDF directly from Selenium: ${generatedPdfPath}`);
} else if (
seleniumResult.ss_path.endsWith(".png") ||
seleniumResult.ss_path.endsWith(".jpg") ||
seleniumResult.ss_path.endsWith(".jpeg")
) {
// Convert image to PDF
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
const pdfFileName = `ddma_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
generatedPdfPath = path.join(
path.dirname(seleniumResult.ss_path),
pdfFileName
);
await fs.writeFile(generatedPdfPath, pdfBuffer);
// ensure cleanup uses this
seleniumResult.pdf_path = generatedPdfPath;
const pdfFileName = `ddma_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
generatedPdfPath = path.join(
path.dirname(seleniumResult.ss_path),
pdfFileName
);
await fs.writeFile(generatedPdfPath, pdfBuffer);
seleniumResult.pdf_path = generatedPdfPath;
console.log(`[ddma-eligibility] Converted screenshot to PDF: ${generatedPdfPath}`);
} else {
outputResult.pdfUploadStatus =
`Unsupported file format: ${seleniumResult.ss_path}`;
}
} catch (err: any) {
console.error("Failed to convert screenshot to PDF:", err);
outputResult.pdfUploadStatus = `Failed to convert screenshot to PDF: ${String(err)}`;
console.error("Failed to process PDF/screenshot:", err);
outputResult.pdfUploadStatus = `Failed to process file: ${String(err)}`;
}
} else {
outputResult.pdfUploadStatus =
"No valid screenshot (ss_path) provided by Selenium; nothing to upload.";
"No valid file path (ss_path) provided by Selenium; nothing to upload.";
}
if (pdfBuffer && generatedPdfPath) {

View File

@@ -179,43 +179,56 @@ async function handleDentaQuestCompletedJob(
await storage.updatePatient(patient.id, { status: newStatus });
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// convert screenshot -> pdf if available
// Handle PDF or convert screenshot -> pdf if available
let pdfBuffer: Buffer | null = null;
let generatedPdfPath: string | null = null;
if (
seleniumResult &&
seleniumResult.ss_path &&
typeof seleniumResult.ss_path === "string" &&
(seleniumResult.ss_path.endsWith(".png") ||
seleniumResult.ss_path.endsWith(".jpg") ||
seleniumResult.ss_path.endsWith(".jpeg"))
typeof seleniumResult.ss_path === "string"
) {
try {
if (!fsSync.existsSync(seleniumResult.ss_path)) {
throw new Error(
`Screenshot file not found: ${seleniumResult.ss_path}`
`File not found: ${seleniumResult.ss_path}`
);
}
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
// Check if the file is already a PDF (from Page.printToPDF)
if (seleniumResult.ss_path.endsWith(".pdf")) {
// Read PDF directly
pdfBuffer = await fs.readFile(seleniumResult.ss_path);
generatedPdfPath = seleniumResult.ss_path;
seleniumResult.pdf_path = generatedPdfPath;
console.log(`[dentaquest-eligibility] Using PDF directly from Selenium: ${generatedPdfPath}`);
} else if (
seleniumResult.ss_path.endsWith(".png") ||
seleniumResult.ss_path.endsWith(".jpg") ||
seleniumResult.ss_path.endsWith(".jpeg")
) {
// Convert image to PDF
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
const pdfFileName = `dentaquest_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
generatedPdfPath = path.join(
path.dirname(seleniumResult.ss_path),
pdfFileName
);
await fs.writeFile(generatedPdfPath, pdfBuffer);
// ensure cleanup uses this
seleniumResult.pdf_path = generatedPdfPath;
const pdfFileName = `dentaquest_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
generatedPdfPath = path.join(
path.dirname(seleniumResult.ss_path),
pdfFileName
);
await fs.writeFile(generatedPdfPath, pdfBuffer);
seleniumResult.pdf_path = generatedPdfPath;
console.log(`[dentaquest-eligibility] Converted screenshot to PDF: ${generatedPdfPath}`);
} else {
outputResult.pdfUploadStatus =
`Unsupported file format: ${seleniumResult.ss_path}`;
}
} catch (err: any) {
console.error("Failed to convert screenshot to PDF:", err);
outputResult.pdfUploadStatus = `Failed to convert screenshot to PDF: ${String(err)}`;
console.error("Failed to process PDF/screenshot:", err);
outputResult.pdfUploadStatus = `Failed to process file: ${String(err)}`;
}
} else {
outputResult.pdfUploadStatus =
"No valid screenshot (ss_path) provided by Selenium; nothing to upload.";
"No valid file path (ss_path) provided by Selenium; nothing to upload.";
}
if (pdfBuffer && generatedPdfPath) {

View File

@@ -14,6 +14,13 @@ type CredentialFormProps = {
};
};
// Available site keys - must match exactly what the automation buttons expect
const SITE_KEY_OPTIONS = [
{ value: "MH", label: "MassHealth" },
{ value: "DDMA", label: "Delta Dental MA" },
{ value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" },
];
export function CredentialForm({ onClose, userId, defaultValues }: CredentialFormProps) {
const [siteKey, setSiteKey] = useState(defaultValues?.siteKey || "");
const [username, setUsername] = useState(defaultValues?.username || "");
@@ -91,14 +98,19 @@ export function CredentialForm({ onClose, userId, defaultValues }: CredentialFor
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium">Site Key</label>
<input
type="text"
<label className="block text-sm font-medium">Insurance Provider</label>
<select
value={siteKey}
onChange={(e) => setSiteKey(e.target.value)}
className="mt-1 p-2 border rounded w-full"
placeholder="e.g., MH, Delta MA, (keep the site key exact same)"
/>
className="mt-1 p-2 border rounded w-full bg-white"
>
<option value="">Select a provider...</option>
{SITE_KEY_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium">Username</label>

View File

@@ -13,6 +13,17 @@ type Credential = {
password: string;
};
// Map site keys to friendly labels
const SITE_KEY_LABELS: Record<string, string> = {
MH: "MassHealth",
DDMA: "Delta Dental MA",
DENTAQUEST: "Tufts SCO / DentaQuest",
};
function getSiteKeyLabel(siteKey: string): string {
return SITE_KEY_LABELS[siteKey] || siteKey;
}
export function CredentialTable() {
const queryClient = useQueryClient();
@@ -108,7 +119,7 @@ export function CredentialTable() {
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Site Key
Provider
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Username
@@ -141,7 +152,7 @@ export function CredentialTable() {
) : (
currentCredentials.map((cred) => (
<tr key={cred.id}>
<td className="px-4 py-2">{cred.siteKey}</td>
<td className="px-4 py-2">{getSiteKeyLabel(cred.siteKey)}</td>
<td className="px-4 py-2">{cred.username}</td>
<td className="px-4 py-2"></td>
<td className="px-4 py-2 text-right">
@@ -227,7 +238,7 @@ export function CredentialTable() {
isOpen={isDeleteDialogOpen}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
entityName={credentialToDelete?.siteKey}
entityName={credentialToDelete ? getSiteKeyLabel(credentialToDelete.siteKey) : undefined}
/>
</div>
);

View File

@@ -5,7 +5,7 @@ from typing import Dict, Any
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium_DDMA_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
@@ -127,74 +127,91 @@ async def start_ddma_run(sid: str, data: dict, url: str):
s["message"] = "Session persisted"
# Continue to step1 below
# OTP required path
# OTP required path - POLL THE BROWSER to detect when user enters OTP
elif isinstance(login_result, str) and login_result == "OTP_REQUIRED":
s["status"] = "waiting_for_otp"
s["message"] = "OTP required for login"
s["message"] = "OTP required for login - please enter OTP in browser"
s["last_activity"] = time.time()
try:
await asyncio.wait_for(s["otp_event"].wait(), timeout=SESSION_OTP_TIMEOUT)
except asyncio.TimeoutError:
s["status"] = "error"
s["message"] = "OTP timeout"
await cleanup_session(sid)
return {"status": "error", "message": "OTP not provided in time"}
driver = s["driver"]
otp_value = s.get("otp_value")
if not otp_value:
s["status"] = "error"
s["message"] = "OTP missing after event"
await cleanup_session(sid)
return {"status": "error", "message": "OTP missing after event"}
# Poll the browser to detect when OTP is completed (user enters it directly)
# We check every 1 second for up to SESSION_OTP_TIMEOUT seconds (faster response)
max_polls = SESSION_OTP_TIMEOUT
login_success = False
# Submit OTP - check if it's in a popup window
try:
driver = s["driver"]
wait = WebDriverWait(driver, 30)
print(f"[OTP] Waiting for user to enter OTP (polling browser for {SESSION_OTP_TIMEOUT}s)...")
# Check if there's a popup window and switch to it
original_window = driver.current_window_handle
all_windows = driver.window_handles
if len(all_windows) > 1:
for window in all_windows:
if window != original_window:
driver.switch_to.window(window)
print(f"[OTP] Switched to popup window for OTP entry")
break
otp_input = wait.until(
EC.presence_of_element_located(
(By.XPATH, "//input[contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code')]")
)
)
otp_input.clear()
otp_input.send_keys(otp_value)
for poll in range(max_polls):
await asyncio.sleep(1)
s["last_activity"] = time.time()
try:
submit_btn = wait.until(
EC.element_to_be_clickable(
(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
# 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]}...")
# Check if we've navigated away from login/OTP pages
if "member" in current_url or "dashboard" in current_url or "eligibility" in current_url:
# Verify by checking for member search input
try:
member_search = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
)
print("[OTP] Member search input found - login successful!")
login_success = True
break
except TimeoutException:
print("[OTP] On member page but search input not found, continuing to poll...")
# Also check if OTP input is still visible
try:
otp_input = driver.find_element(By.XPATH,
"//input[contains(@aria-label,'Verification') or contains(@placeholder,'verification') or @type='tel']"
)
# OTP input still visible - user hasn't entered OTP yet
print(f"[OTP Poll {poll+1}] OTP input still visible - waiting...")
except:
# OTP input not found - might mean login is in progress or succeeded
# Try navigating to members page
if "onboarding" in current_url or "start" in current_url:
print("[OTP] OTP input gone, trying to navigate to members page...")
try:
driver.get("https://providers.deltadentalma.com/members")
await asyncio.sleep(2)
except:
pass
except Exception as poll_err:
print(f"[OTP Poll {poll+1}] Error: {poll_err}")
if not login_success:
# Final attempt - navigate to members page and check
try:
print("[OTP] Final attempt - navigating to members page...")
driver.get("https://providers.deltadentalma.com/members")
await asyncio.sleep(3)
member_search = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
)
submit_btn.click()
except Exception:
otp_input.send_keys("\n")
print("[OTP] Member search input found - login successful!")
login_success = True
except TimeoutException:
s["status"] = "error"
s["message"] = "OTP timeout - login not completed"
await cleanup_session(sid)
return {"status": "error", "message": "OTP not completed in time"}
except Exception as final_err:
s["status"] = "error"
s["message"] = f"OTP verification failed: {final_err}"
await cleanup_session(sid)
return {"status": "error", "message": s["message"]}
# Wait for verification and switch back to main window if needed
await asyncio.sleep(2)
if len(driver.window_handles) > 0:
driver.switch_to.window(driver.window_handles[0])
s["status"] = "otp_submitted"
s["last_activity"] = time.time()
await asyncio.sleep(0.5)
except Exception as e:
s["status"] = "error"
s["message"] = f"Failed to submit OTP into page: {e}"
await cleanup_session(sid)
return {"status": "error", "message": s["message"]}
if login_success:
s["status"] = "running"
s["message"] = "Login successful after OTP"
print("[OTP] Proceeding to step1...")
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
s["status"] = "error"
@@ -202,6 +219,13 @@ async def start_ddma_run(sid: str, data: dict, url: str):
await cleanup_session(sid)
return {"status": "error", "message": login_result}
# Login succeeded without OTP (SUCCESS)
elif isinstance(login_result, str) and login_result == "SUCCESS":
print("[start_ddma_run] Login succeeded without OTP")
s["status"] = "running"
s["message"] = "Login succeeded"
# Continue to step1 below
# Step 1
step1_result = bot.step1()
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):

View File

@@ -5,7 +5,7 @@ from typing import Dict, Any
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import WebDriverException
from selenium.common.exceptions import WebDriverException, TimeoutException
from selenium_DentaQuest_eligibilityCheckWorker import AutomationDentaQuestEligibilityCheck
@@ -126,74 +126,91 @@ async def start_dentaquest_run(sid: str, data: dict, url: str):
s["message"] = "Session persisted"
# Continue to step1 below
# OTP required path
# OTP required path - POLL THE BROWSER to detect when user enters OTP
elif isinstance(login_result, str) and login_result == "OTP_REQUIRED":
s["status"] = "waiting_for_otp"
s["message"] = "OTP required for login"
s["message"] = "OTP required for login - please enter OTP in browser"
s["last_activity"] = time.time()
try:
await asyncio.wait_for(s["otp_event"].wait(), timeout=SESSION_OTP_TIMEOUT)
except asyncio.TimeoutError:
s["status"] = "error"
s["message"] = "OTP timeout"
await cleanup_session(sid)
return {"status": "error", "message": "OTP not provided in time"}
driver = s["driver"]
otp_value = s.get("otp_value")
if not otp_value:
s["status"] = "error"
s["message"] = "OTP missing after event"
await cleanup_session(sid)
return {"status": "error", "message": "OTP missing after event"}
# Poll the browser to detect when OTP is completed (user enters it directly)
# We check every 1 second for up to SESSION_OTP_TIMEOUT seconds (faster response)
max_polls = SESSION_OTP_TIMEOUT
login_success = False
# Submit OTP
try:
driver = s["driver"]
wait = WebDriverWait(driver, 30)
print(f"[DentaQuest OTP] Waiting for user to enter OTP (polling browser for {SESSION_OTP_TIMEOUT}s)...")
# Check if there's a popup window and switch to it
original_window = driver.current_window_handle
all_windows = driver.window_handles
if len(all_windows) > 1:
for window in all_windows:
if window != original_window:
driver.switch_to.window(window)
break
# DentaQuest OTP input field - adjust selectors as needed
otp_input = wait.until(
EC.presence_of_element_located(
(By.XPATH, "//input[contains(@name,'otp') or contains(@name,'code') or contains(@placeholder,'code') or contains(@id,'otp') or @type='tel']")
)
)
otp_input.clear()
otp_input.send_keys(otp_value)
for poll in range(max_polls):
await asyncio.sleep(1)
s["last_activity"] = time.time()
try:
submit_btn = wait.until(
EC.element_to_be_clickable(
(By.XPATH, "//button[contains(text(),'Verify') or contains(text(),'Submit') or contains(text(),'Continue') or @type='submit']")
# 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]}...")
# Check if we've navigated away from login/OTP pages
if "member" in current_url or "dashboard" in current_url or "eligibility" in current_url:
# Verify by checking for member search input
try:
member_search = WebDriverWait(driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
)
print("[DentaQuest OTP] Member search input found - login successful!")
login_success = True
break
except TimeoutException:
print("[DentaQuest OTP] On member page but search input not found, continuing to poll...")
# Also check if OTP input is still visible
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') or contains(@placeholder,'Code')]"
)
# OTP input still visible - user hasn't entered OTP yet
print(f"[DentaQuest OTP Poll {poll+1}] OTP input still visible - waiting...")
except:
# OTP input not found - might mean login is in progress or succeeded
# Try navigating to members page (like Delta MA)
if "onboarding" in current_url or "start" in current_url or "login" in current_url:
print("[DentaQuest OTP] OTP input gone, trying to navigate to members page...")
try:
driver.get("https://providers.dentaquest.com/members")
await asyncio.sleep(2)
except:
pass
except Exception as poll_err:
print(f"[DentaQuest OTP Poll {poll+1}] Error: {poll_err}")
if not login_success:
# Final attempt - navigate to members page and check (like Delta MA)
try:
print("[DentaQuest OTP] Final attempt - navigating to members page...")
driver.get("https://providers.dentaquest.com/members")
await asyncio.sleep(3)
member_search = WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
)
submit_btn.click()
except Exception:
otp_input.send_keys("\n")
print("[DentaQuest OTP] Member search input found - login successful!")
login_success = True
except TimeoutException:
s["status"] = "error"
s["message"] = "OTP timeout - login not completed"
await cleanup_session(sid)
return {"status": "error", "message": "OTP not completed in time"}
except Exception as final_err:
s["status"] = "error"
s["message"] = f"OTP verification failed: {final_err}"
await cleanup_session(sid)
return {"status": "error", "message": s["message"]}
# Wait for verification and switch back to main window if needed
await asyncio.sleep(2)
if len(driver.window_handles) > 0:
driver.switch_to.window(driver.window_handles[0])
s["status"] = "otp_submitted"
s["last_activity"] = time.time()
await asyncio.sleep(0.5)
except Exception as e:
s["status"] = "error"
s["message"] = f"Failed to submit OTP into page: {e}"
await cleanup_session(sid)
return {"status": "error", "message": s["message"]}
if login_success:
s["status"] = "running"
s["message"] = "Login successful after OTP"
print("[DentaQuest OTP] Proceeding to step1...")
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
s["status"] = "error"
@@ -201,6 +218,13 @@ async def start_dentaquest_run(sid: str, data: dict, url: str):
await cleanup_session(sid)
return {"status": "error", "message": login_result}
# Login succeeded without OTP (SUCCESS)
elif isinstance(login_result, str) and login_result == "SUCCESS":
print("[start_dentaquest_run] Login succeeded without OTP")
s["status"] = "running"
s["message"] = "Login succeeded"
# Continue to step1 below
# Step 1
step1_result = bot.step1()
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):

View File

@@ -237,7 +237,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
if self.massddma_username:
browser_manager.save_credentials_hash(self.massddma_username)
# OTP detection
# OTP detection - wait up to 30 seconds for OTP input to appear
try:
otp_candidate = WebDriverWait(self.driver, 30).until(
EC.presence_of_element_located(
@@ -249,6 +249,36 @@ class AutomationDeltaDentalMAEligibilityCheck:
return "OTP_REQUIRED"
except TimeoutException:
print("[login] No OTP input detected in allowed time.")
# Check if we're now on the member search page (login succeeded without OTP)
try:
current_url = self.driver.current_url.lower()
if "member" in current_url or "dashboard" in current_url:
member_search = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
)
print("[login] Login successful - now on member search page")
return "SUCCESS"
except TimeoutException:
pass
# Check for error messages on page
try:
error_elem = WebDriverWait(self.driver, 3).until(
EC.presence_of_element_located((By.XPATH, "//*[contains(@class,'error') or contains(text(),'invalid') or contains(text(),'failed')]"))
)
print(f"[login] Login failed - error detected: {error_elem.text}")
return f"ERROR:LOGIN FAILED: {error_elem.text}"
except TimeoutException:
pass
# If still on login page, login failed
if "onboarding" in self.driver.current_url.lower() or "login" in self.driver.current_url.lower():
print("[login] Login failed - still on login page")
return "ERROR:LOGIN FAILED: Still on login page"
# Otherwise assume success (might be on an intermediate page)
print("[login] Assuming login succeeded (no errors detected)")
return "SUCCESS"
except Exception as e:
print("[login] Exception during login:", e)
return f"ERROR:LOGIN FAILED: {e}"
@@ -329,73 +359,220 @@ class AutomationDeltaDentalMAEligibilityCheck:
wait = WebDriverWait(self.driver, 90)
try:
# 1) find the eligibility <a> inside the correct cell
status_link = wait.until(EC.presence_of_element_located((
By.XPATH,
"(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]"
)))
# Wait for results table to load (use explicit wait instead of fixed sleep)
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
)
except TimeoutException:
print("[DDMA step2] Warning: Results table not found within timeout")
eligibilityText = status_link.text.strip().lower()
# 1) Find and extract eligibility status from search results (use short timeout - not critical)
eligibilityText = "unknown"
try:
# Use short timeout (3s) since this is just for status extraction
short_wait = WebDriverWait(self.driver, 3)
status_link = short_wait.until(EC.presence_of_element_located((
By.XPATH,
"(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]"
)))
eligibilityText = status_link.text.strip().lower()
print(f"[DDMA step2] Found eligibility status: {eligibilityText}")
except Exception as e:
print(f"[DDMA step2] Eligibility link not found, trying alternative...")
try:
alt_status = self.driver.find_element(By.XPATH, "//*[contains(text(),'Active') or contains(text(),'Inactive') or contains(text(),'Eligible')]")
eligibilityText = alt_status.text.strip().lower()
if "active" in eligibilityText or "eligible" in eligibilityText:
eligibilityText = "active"
elif "inactive" in eligibilityText:
eligibilityText = "inactive"
print(f"[DDMA step2] Found eligibility via alternative: {eligibilityText}")
except:
pass
# 2) finding patient name.
patient_name_div = wait.until(EC.presence_of_element_located((
By.XPATH,
'//div[@class="flex flex-row w-full items-center"]'
)))
# 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 = ""
patientName = patient_name_div.text.strip().lower()
# First, let's print what we see on the page for debugging
current_url_before = self.driver.current_url
print(f"[DDMA step2] Current URL before click: {current_url_before}")
# 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")
print(f"[DDMA step2] Found {len(all_links)} links in first row:")
for i, link in enumerate(all_links):
href = link.get_attribute("href") or "no-href"
text = link.text.strip() or "(empty text)"
print(f" Link {i}: href={href[:80]}..., text={text}")
except Exception as e:
print(f"[DDMA step2] Error listing links: {e}")
# Find the patient detail link and navigate DIRECTLY to it
detail_url = None
patient_link_selectors = [
"(//table//tbody//tr)[1]//td[1]//a", # First column link
"(//tbody//tr)[1]//a[contains(@href, 'member-details')]", # member-details link
"(//tbody//tr)[1]//a[contains(@href, 'member')]", # Any member link
]
for selector in patient_link_selectors:
try:
patient_link = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, selector))
)
patientName = patient_link.text.strip()
href = patient_link.get_attribute("href")
print(f"[DDMA step2] Found patient link: text='{patientName}', href={href}")
if href and "member-details" in href:
detail_url = href
patient_name_clicked = True
print(f"[DDMA step2] Will navigate directly to: {detail_url}")
break
except Exception as e:
print(f"[DDMA step2] Selector '{selector}' failed: {e}")
continue
if not detail_url:
# Fallback: Try to find ANY link to member-details
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] Found member-details link: {detail_url}")
except Exception as e:
print(f"[DDMA step2] Could not find member-details link: {e}")
# Navigate to detail page DIRECTLY instead of clicking (which may open new tab/fail)
if patient_name_clicked and detail_url:
print(f"[DDMA step2] Navigating directly to detail page: {detail_url}")
self.driver.get(detail_url)
time.sleep(3) # Wait for page to load
current_url_after = self.driver.current_url
print(f"[DDMA step2] Current URL after navigation: {current_url_after}")
if "member-details" in current_url_after:
print("[DDMA step2] Successfully navigated to member details page!")
else:
print(f"[DDMA step2] WARNING: Navigation might have redirected. Current URL: {current_url_after}")
# Wait for page to be ready
try:
WebDriverWait(self.driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
except Exception:
print("[DDMA step2] Warning: document.readyState did not become 'complete'")
# Wait for member details content to load (wait for specific elements)
print("[DDMA step2] Waiting for member details content to fully load...")
content_loaded = False
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:
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, selector))
)
content_loaded = True
print(f"[DDMA step2] Content element found: {selector}")
break
except:
continue
if not content_loaded:
print("[DDMA step2] Warning: Could not verify content loaded, waiting extra time...")
# Additional wait for dynamic content and animations
time.sleep(5) # Increased from 2 to 5 seconds
# Print page title for debugging
try:
page_title = self.driver.title
print(f"[DDMA step2] Page title: {page_title}")
except:
pass
# Try to extract patient name from detailed page if not already found
if not patientName:
detail_name_selectors = [
"//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) > 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
else:
print("[DDMA step2] Warning: Could not click on patient, capturing search results page")
# Still try to get patient name from search results
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")
else:
print(f"[DDMA step2] Patient name: {patientName}")
# Wait for page to fully load before generating PDF
try:
WebDriverWait(self.driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
except Exception:
print("Warning: document.readyState did not become 'complete' within timeout")
# Give some time for lazy content to finish rendering (adjust if needed)
time.sleep(0.6)
# Get total page size and DPR
total_width = int(self.driver.execute_script(
"return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.documentElement.clientWidth);"
))
total_height = int(self.driver.execute_script(
"return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight);"
))
dpr = float(self.driver.execute_script("return window.devicePixelRatio || 1;"))
# Set device metrics to the full page size so Page.captureScreenshot captures everything
# Note: Some pages are extremely tall; if you hit memory limits, you can capture in chunks.
self.driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {
"mobile": False,
"width": total_width,
"height": total_height,
"deviceScaleFactor": dpr,
"screenOrientation": {"angle": 0, "type": "portraitPrimary"}
})
# Small pause for layout to settle after emulation change
time.sleep(0.15)
# Capture screenshot (base64 PNG)
result = self.driver.execute_cdp_cmd("Page.captureScreenshot", {"format": "png", "fromSurface": True})
image_data = base64.b64decode(result.get('data', ''))
screenshot_path = os.path.join(self.download_dir, f"ss_{self.memberId}.png")
with open(screenshot_path, "wb") as f:
f.write(image_data)
# Restore original metrics to avoid affecting further interactions
try:
self.driver.execute_cdp_cmd('Emulation.clearDeviceMetricsOverride', {})
except Exception:
# non-fatal: continue
pass
print("Screenshot saved at:", screenshot_path)
time.sleep(1)
# Close the browser window after screenshot (session preserved in profile)
# Generate PDF of the detailed patient page using Chrome DevTools Protocol
print("[DDMA step2] Generating PDF of patient detail page...")
pdf_options = {
"landscape": False,
"displayHeaderFooter": False,
"printBackground": True,
"preferCSSPageSize": True,
"paperWidth": 8.5, # Letter size in inches
"paperHeight": 11,
"marginTop": 0.4,
"marginBottom": 0.4,
"marginLeft": 0.4,
"marginRight": 0.4,
"scale": 0.9, # Slightly scale down to fit content
}
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
pdf_data = base64.b64decode(result.get('data', ''))
pdf_path = os.path.join(self.download_dir, f"eligibility_{self.memberId}.pdf")
with open(pdf_path, "wb") as f:
f.write(pdf_data)
print(f"[DDMA step2] PDF saved at: {pdf_path}")
# Close the browser window after PDF generation (session preserved in profile)
try:
from ddma_browser_manager import get_browser_manager
get_browser_manager().quit_driver()
@@ -406,8 +583,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
output = {
"status": "success",
"eligibility": eligibilityText,
"ss_path": screenshot_path,
"patientName":patientName
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility
"pdf_path": pdf_path, # Also add explicit pdf_path
"patientName": patientName
}
return output
except Exception as e:

View File

@@ -198,25 +198,48 @@ class AutomationDentaQuestEligibilityCheck:
if self.dentaquest_username:
browser_manager.save_credentials_hash(self.dentaquest_username)
time.sleep(5)
# Check for OTP after login
# OTP detection - wait up to 30 seconds for OTP input to appear (like Delta MA)
# Use comprehensive XPath to detect various OTP input patterns
try:
otp_input = WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//input[@type='tel' or contains(@placeholder,'code') or contains(@aria-label,'Verification')]"))
otp_input = WebDriverWait(self.driver, 30).until(
EC.presence_of_element_located((By.XPATH,
"//input[@type='tel' or contains(@placeholder,'code') or contains(@placeholder,'Code') or "
"contains(@aria-label,'Verification') or contains(@aria-label,'verification') or "
"contains(@aria-label,'Code') or contains(@aria-label,'code') or "
"contains(@placeholder,'verification') or contains(@placeholder,'Verification') or "
"contains(@name,'otp') or contains(@name,'code') or contains(@id,'otp') or contains(@id,'code')]"
))
)
print("[DentaQuest login] OTP input detected -> OTP_REQUIRED")
return "OTP_REQUIRED"
except TimeoutException:
pass
print("[DentaQuest login] No OTP input detected in 30 seconds")
# Check if login succeeded
if "dashboard" in self.driver.current_url.lower():
return "SUCCESS"
# Check if login succeeded (redirected to dashboard or member search)
current_url_after_login = self.driver.current_url.lower()
print(f"[DentaQuest login] After login URL: {current_url_after_login}")
if "dashboard" in current_url_after_login or "member" in current_url_after_login:
# Verify by checking for member search input
try:
member_search = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]'))
)
print("[DentaQuest login] Login successful - now on member search page")
return "SUCCESS"
except TimeoutException:
pass
# Still on login page - login failed
if "onboarding" in current_url_after_login or "login" in current_url_after_login:
print("[DentaQuest login] Login failed - still on login page")
return "ERROR: Login failed - check credentials"
except TimeoutException:
print("[DentaQuest login] Login form elements not found")
return "ERROR: Login form not found"
# If we got here without going through login, we're already logged in
return "SUCCESS"
except Exception as e:
@@ -335,45 +358,184 @@ class AutomationDentaQuestEligibilityCheck:
def step2(self):
"""Get eligibility status and capture screenshot"""
"""Get eligibility status, navigate to detail page, and capture PDF"""
wait = WebDriverWait(self.driver, 90)
try:
print("[DentaQuest step2] Starting eligibility capture")
# Wait for results to load
time.sleep(3)
# Try to find eligibility status from the results
eligibilityText = "unknown"
# Wait for results table to load (use explicit wait instead of fixed sleep)
try:
# Look for a link or element with eligibility status
status_elem = wait.until(EC.presence_of_element_located((
By.XPATH,
"//a[contains(@href,'eligibility')] | //*[contains(@class,'status')] | //*[contains(text(),'Active') or contains(text(),'Inactive') or contains(text(),'Eligible')]"
)))
eligibilityText = status_elem.text.strip().lower()
print(f"[DentaQuest step2] Found status element: {eligibilityText}")
# Normalize status
if "active" in eligibilityText or "eligible" in eligibilityText:
eligibilityText = "active"
elif "inactive" in eligibilityText or "ineligible" in eligibilityText:
eligibilityText = "inactive"
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
)
except TimeoutException:
print("[DentaQuest step2] Could not find specific eligibility status")
print("[DentaQuest step2] Warning: Results table not found within timeout")
# Try to find patient name
# 1) Find and extract eligibility status from search results
eligibilityText = "unknown"
status_selectors = [
"(//tbody//tr)[1]//a[contains(@href, 'eligibility')]",
"//a[contains(@href,'eligibility')]",
"//*[contains(@class,'status')]",
"//*[contains(text(),'Active') or contains(text(),'Inactive') or contains(text(),'Eligible')]"
]
for selector in status_selectors:
try:
status_elem = self.driver.find_element(By.XPATH, selector)
status_text = status_elem.text.strip().lower()
if status_text:
print(f"[DentaQuest step2] Found status with selector '{selector}': {status_text}")
if "active" in status_text or "eligible" in status_text:
eligibilityText = "active"
break
elif "inactive" in status_text or "ineligible" in status_text:
eligibilityText = "inactive"
break
except:
continue
print(f"[DentaQuest step2] Final eligibility status: {eligibilityText}")
# 2) Find the patient detail link and navigate DIRECTLY to it
print("[DentaQuest step2] Looking for patient detail link...")
patient_name_clicked = False
patientName = ""
try:
# Look for the patient name in the results
name_elem = self.driver.find_element(By.XPATH, "//h1 | //div[contains(@class,'name')] | //*[contains(@class,'member-name') or contains(@class,'patient-name')]")
patientName = name_elem.text.strip()
print(f"[DentaQuest step2] Found patient name: {patientName}")
except:
pass
detail_url = None
current_url_before = self.driver.current_url
print(f"[DentaQuest step2] Current URL before: {current_url_before}")
# Wait for page to fully load
# Find all links in first row and log them
try:
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
print(f"[DentaQuest step2] Found {len(all_links)} links in first row:")
for i, link in enumerate(all_links):
href = link.get_attribute("href") or "no-href"
text = link.text.strip() or "(empty text)"
print(f" Link {i}: href={href[:80]}..., text={text}")
except Exception as e:
print(f"[DentaQuest step2] Error listing links: {e}")
# Find the patient detail link
patient_link_selectors = [
"(//table//tbody//tr)[1]//td[1]//a", # First column link
"(//tbody//tr)[1]//a[contains(@href, 'member-details')]", # member-details link
"(//tbody//tr)[1]//a[contains(@href, 'member')]", # Any member link
]
for selector in patient_link_selectors:
try:
patient_link = WebDriverWait(self.driver, 5).until(
EC.presence_of_element_located((By.XPATH, selector))
)
patientName = patient_link.text.strip()
href = patient_link.get_attribute("href")
print(f"[DentaQuest step2] Found patient link: text='{patientName}', href={href}")
if href and ("member-details" in href or "member" in href):
detail_url = href
patient_name_clicked = True
print(f"[DentaQuest step2] Will navigate directly to: {detail_url}")
break
except Exception as e:
print(f"[DentaQuest step2] Selector '{selector}' failed: {e}")
continue
if not detail_url:
# Fallback: Try to find ANY link to member-details
try:
all_links = self.driver.find_elements(By.XPATH, "//a[contains(@href, 'member')]")
if all_links:
detail_url = all_links[0].get_attribute("href")
patient_name_clicked = True
print(f"[DentaQuest step2] Found member link: {detail_url}")
except Exception as e:
print(f"[DentaQuest step2] Could not find member link: {e}")
# Navigate to detail page DIRECTLY
if patient_name_clicked and detail_url:
print(f"[DentaQuest step2] Navigating directly to detail page: {detail_url}")
self.driver.get(detail_url)
time.sleep(3) # Wait for page to load
current_url_after = self.driver.current_url
print(f"[DentaQuest step2] Current URL after navigation: {current_url_after}")
if "member-details" in current_url_after or "member" in current_url_after:
print("[DentaQuest step2] Successfully navigated to member details page!")
else:
print(f"[DentaQuest step2] WARNING: Navigation might have redirected. Current URL: {current_url_after}")
# Wait for page to be ready
try:
WebDriverWait(self.driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
except Exception:
print("[DentaQuest step2] Warning: document.readyState did not become 'complete'")
# Wait for member details content to load
print("[DentaQuest step2] Waiting for member details content to fully load...")
content_loaded = False
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:
try:
WebDriverWait(self.driver, 10).until(
EC.presence_of_element_located((By.XPATH, selector))
)
content_loaded = True
print(f"[DentaQuest step2] Content element found: {selector}")
break
except:
continue
if not content_loaded:
print("[DentaQuest step2] Warning: Could not verify content loaded, waiting extra time...")
# Additional wait for dynamic content
time.sleep(5)
# Try to extract patient name from detailed page if not already found
if not patientName:
detail_name_selectors = [
"//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) > 1:
if not any(x in name_text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'date', 'print', 'member id']):
patientName = name_text
print(f"[DentaQuest step2] Found patient name on detail page: {patientName}")
break
except:
continue
else:
print("[DentaQuest step2] Warning: Could not find detail URL, capturing search results page")
# Still try to get patient name from search results
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("[DentaQuest step2] Could not extract patient name")
else:
print(f"[DentaQuest step2] Patient name: {patientName}")
# Wait for page to fully load before generating PDF
try:
WebDriverWait(self.driver, 30).until(
lambda d: d.execute_script("return document.readyState") == "complete"
@@ -383,41 +545,31 @@ class AutomationDentaQuestEligibilityCheck:
time.sleep(1)
# Capture full page screenshot
print("[DentaQuest step2] Capturing screenshot")
total_width = int(self.driver.execute_script(
"return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.documentElement.clientWidth);"
))
total_height = int(self.driver.execute_script(
"return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight);"
))
dpr = float(self.driver.execute_script("return window.devicePixelRatio || 1;"))
# Generate PDF of the detailed patient page using Chrome DevTools Protocol
print("[DentaQuest step2] Generating PDF of patient detail page...")
self.driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {
"mobile": False,
"width": total_width,
"height": total_height,
"deviceScaleFactor": dpr,
"screenOrientation": {"angle": 0, "type": "portraitPrimary"}
})
pdf_options = {
"landscape": False,
"displayHeaderFooter": False,
"printBackground": True,
"preferCSSPageSize": True,
"paperWidth": 8.5, # Letter size in inches
"paperHeight": 11,
"marginTop": 0.4,
"marginBottom": 0.4,
"marginLeft": 0.4,
"marginRight": 0.4,
"scale": 0.9, # Slightly scale down to fit content
}
time.sleep(0.2)
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
pdf_data = base64.b64decode(result.get('data', ''))
pdf_path = os.path.join(self.download_dir, f"dentaquest_eligibility_{self.memberId}_{int(time.time())}.pdf")
with open(pdf_path, "wb") as f:
f.write(pdf_data)
print(f"[DentaQuest step2] PDF saved: {pdf_path}")
# Capture screenshot
result = self.driver.execute_cdp_cmd("Page.captureScreenshot", {"format": "png", "fromSurface": True})
image_data = base64.b64decode(result.get('data', ''))
screenshot_path = os.path.join(self.download_dir, f"dentaquest_ss_{self.memberId}_{int(time.time())}.png")
with open(screenshot_path, "wb") as f:
f.write(image_data)
print(f"[DentaQuest step2] Screenshot saved: {screenshot_path}")
# Restore original metrics
try:
self.driver.execute_cdp_cmd('Emulation.clearDeviceMetricsOverride', {})
except Exception:
pass
# Close the browser window after screenshot
# Close the browser window after PDF generation
try:
from dentaquest_browser_manager import get_browser_manager
get_browser_manager().quit_driver()
@@ -428,7 +580,8 @@ class AutomationDentaQuestEligibilityCheck:
output = {
"status": "success",
"eligibility": eligibilityText,
"ss_path": screenshot_path,
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility
"pdf_path": pdf_path, # Also add explicit pdf_path
"patientName": patientName
}
print(f"[DentaQuest step2] Success: {output}")