diff --git a/apps/Backend/src/routes/insuranceStatusDDMA.ts b/apps/Backend/src/routes/insuranceStatusDDMA.ts index a5571ea..4398f8c 100644 --- a/apps/Backend/src/routes/insuranceStatusDDMA.ts +++ b/apps/Backend/src/routes/insuranceStatusDDMA.ts @@ -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) { diff --git a/apps/Backend/src/routes/insuranceStatusDentaQuest.ts b/apps/Backend/src/routes/insuranceStatusDentaQuest.ts index 544565d..3e252b9 100644 --- a/apps/Backend/src/routes/insuranceStatusDentaQuest.ts +++ b/apps/Backend/src/routes/insuranceStatusDentaQuest.ts @@ -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) { diff --git a/apps/Frontend/src/components/settings/InsuranceCredForm.tsx b/apps/Frontend/src/components/settings/InsuranceCredForm.tsx index cdb62f7..4eeca0f 100644 --- a/apps/Frontend/src/components/settings/InsuranceCredForm.tsx +++ b/apps/Frontend/src/components/settings/InsuranceCredForm.tsx @@ -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
- - Insurance Provider +
diff --git a/apps/Frontend/src/components/settings/insuranceCredTable.tsx b/apps/Frontend/src/components/settings/insuranceCredTable.tsx index f8d6e70..2babbcd 100644 --- a/apps/Frontend/src/components/settings/insuranceCredTable.tsx +++ b/apps/Frontend/src/components/settings/insuranceCredTable.tsx @@ -13,6 +13,17 @@ type Credential = { password: string; }; +// Map site keys to friendly labels +const SITE_KEY_LABELS: Record = { + 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() { - Site Key + Provider Username @@ -141,7 +152,7 @@ export function CredentialTable() { ) : ( currentCredentials.map((cred) => ( - {cred.siteKey} + {getSiteKeyLabel(cred.siteKey)} {cred.username} •••••••• @@ -227,7 +238,7 @@ export function CredentialTable() { isOpen={isDeleteDialogOpen} onConfirm={handleConfirmDelete} onCancel={handleCancelDelete} - entityName={credentialToDelete?.siteKey} + entityName={credentialToDelete ? getSiteKeyLabel(credentialToDelete.siteKey) : undefined} />
); diff --git a/apps/SeleniumService/helpers_ddma_eligibility.py b/apps/SeleniumService/helpers_ddma_eligibility.py index c9db23c..71a24af 100644 --- a/apps/SeleniumService/helpers_ddma_eligibility.py +++ b/apps/SeleniumService/helpers_ddma_eligibility.py @@ -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"} - - 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"} - - # Submit OTP - check if it's in a popup window - try: - driver = s["driver"] - wait = WebDriverWait(driver, 30) - - # 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) - - try: - submit_btn = wait.until( - EC.element_to_be_clickable( - (By.XPATH, "//button[@type='button' and @aria-label='Verify']") - ) - ) - submit_btn.click() - except Exception: - otp_input.send_keys("\n") - - # 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" + + driver = s["driver"] + + # 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 + + print(f"[OTP] Waiting for user to enter OTP (polling browser for {SESSION_OTP_TIMEOUT}s)...") + + for poll in range(max_polls): + await asyncio.sleep(1) 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"]} + + try: + # 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"]')) + ) + 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"]} + + 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"): diff --git a/apps/SeleniumService/helpers_dentaquest_eligibility.py b/apps/SeleniumService/helpers_dentaquest_eligibility.py index 58a5a2b..0bd1f20 100644 --- a/apps/SeleniumService/helpers_dentaquest_eligibility.py +++ b/apps/SeleniumService/helpers_dentaquest_eligibility.py @@ -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"} - - 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"} - - # Submit OTP - try: - driver = s["driver"] - wait = WebDriverWait(driver, 30) - - # 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) - - 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']") - ) - ) - submit_btn.click() - except Exception: - otp_input.send_keys("\n") - - # 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" + + driver = s["driver"] + + # 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 + + print(f"[DentaQuest OTP] Waiting for user to enter OTP (polling browser for {SESSION_OTP_TIMEOUT}s)...") + + for poll in range(max_polls): + await asyncio.sleep(1) 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"]} + + try: + # 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"]')) + ) + 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"]} + + 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"): diff --git a/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py index 85c060c..9e68866 100644 --- a/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py @@ -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 inside the correct cell - status_link = wait.until(EC.presence_of_element_located(( - By.XPATH, - "(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]" - ))) - - eligibilityText = status_link.text.strip().lower() - - # 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"]' - ))) - - patientName = patient_name_div.text.strip().lower() + # 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") + + # 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) 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 = "" + + # 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) - # Close the browser window after screenshot (session preserved in profile) + time.sleep(1) + + # 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: diff --git a/apps/SeleniumService/selenium_DentaQuest_eligibilityCheckWorker.py b/apps/SeleniumService/selenium_DentaQuest_eligibilityCheckWorker.py index 65e1f9f..c7c1dbe 100644 --- a/apps/SeleniumService/selenium_DentaQuest_eligibilityCheckWorker.py +++ b/apps/SeleniumService/selenium_DentaQuest_eligibilityCheckWorker.py @@ -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") + + # 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}") - # Try to find patient name + # 2) Find the patient detail link and navigate DIRECTLY to it + print("[DentaQuest step2] Looking for patient detail link...") + patient_name_clicked = False patientName = "" + detail_url = None + current_url_before = self.driver.current_url + print(f"[DentaQuest step2] Current URL before: {current_url_before}") + + # Find all links in first row and log them 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 + 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 - # Wait for page to fully load + 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...") + + 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"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}") - self.driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', { - "mobile": False, - "width": total_width, - "height": total_height, - "deviceScaleFactor": dpr, - "screenOrientation": {"angle": 0, "type": "portraitPrimary"} - }) - - time.sleep(0.2) - - # 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}")