- | {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}")
|