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:
@@ -179,25 +179,35 @@ async function handleDdmaCompletedJob(
|
|||||||
await storage.updatePatient(patient.id, { status: newStatus });
|
await storage.updatePatient(patient.id, { status: newStatus });
|
||||||
outputResult.patientUpdateStatus = `Patient status updated to ${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 pdfBuffer: Buffer | null = null;
|
||||||
let generatedPdfPath: string | null = null;
|
let generatedPdfPath: string | null = null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seleniumResult &&
|
seleniumResult &&
|
||||||
seleniumResult.ss_path &&
|
seleniumResult.ss_path &&
|
||||||
typeof seleniumResult.ss_path === "string" &&
|
typeof seleniumResult.ss_path === "string"
|
||||||
(seleniumResult.ss_path.endsWith(".png") ||
|
|
||||||
seleniumResult.ss_path.endsWith(".jpg") ||
|
|
||||||
seleniumResult.ss_path.endsWith(".jpeg"))
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (!fsSync.existsSync(seleniumResult.ss_path)) {
|
if (!fsSync.existsSync(seleniumResult.ss_path)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Screenshot file not found: ${seleniumResult.ss_path}`
|
`File not found: ${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);
|
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
|
||||||
|
|
||||||
const pdfFileName = `ddma_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
|
const pdfFileName = `ddma_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
|
||||||
@@ -206,16 +216,19 @@ async function handleDdmaCompletedJob(
|
|||||||
pdfFileName
|
pdfFileName
|
||||||
);
|
);
|
||||||
await fs.writeFile(generatedPdfPath, pdfBuffer);
|
await fs.writeFile(generatedPdfPath, pdfBuffer);
|
||||||
|
|
||||||
// ensure cleanup uses this
|
|
||||||
seleniumResult.pdf_path = generatedPdfPath;
|
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) {
|
} catch (err: any) {
|
||||||
console.error("Failed to convert screenshot to PDF:", err);
|
console.error("Failed to process PDF/screenshot:", err);
|
||||||
outputResult.pdfUploadStatus = `Failed to convert screenshot to PDF: ${String(err)}`;
|
outputResult.pdfUploadStatus = `Failed to process file: ${String(err)}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
outputResult.pdfUploadStatus =
|
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) {
|
if (pdfBuffer && generatedPdfPath) {
|
||||||
|
|||||||
@@ -179,25 +179,35 @@ async function handleDentaQuestCompletedJob(
|
|||||||
await storage.updatePatient(patient.id, { status: newStatus });
|
await storage.updatePatient(patient.id, { status: newStatus });
|
||||||
outputResult.patientUpdateStatus = `Patient status updated to ${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 pdfBuffer: Buffer | null = null;
|
||||||
let generatedPdfPath: string | null = null;
|
let generatedPdfPath: string | null = null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seleniumResult &&
|
seleniumResult &&
|
||||||
seleniumResult.ss_path &&
|
seleniumResult.ss_path &&
|
||||||
typeof seleniumResult.ss_path === "string" &&
|
typeof seleniumResult.ss_path === "string"
|
||||||
(seleniumResult.ss_path.endsWith(".png") ||
|
|
||||||
seleniumResult.ss_path.endsWith(".jpg") ||
|
|
||||||
seleniumResult.ss_path.endsWith(".jpeg"))
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (!fsSync.existsSync(seleniumResult.ss_path)) {
|
if (!fsSync.existsSync(seleniumResult.ss_path)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Screenshot file not found: ${seleniumResult.ss_path}`
|
`File not found: ${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);
|
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
|
||||||
|
|
||||||
const pdfFileName = `dentaquest_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
|
const pdfFileName = `dentaquest_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
|
||||||
@@ -206,16 +216,19 @@ async function handleDentaQuestCompletedJob(
|
|||||||
pdfFileName
|
pdfFileName
|
||||||
);
|
);
|
||||||
await fs.writeFile(generatedPdfPath, pdfBuffer);
|
await fs.writeFile(generatedPdfPath, pdfBuffer);
|
||||||
|
|
||||||
// ensure cleanup uses this
|
|
||||||
seleniumResult.pdf_path = generatedPdfPath;
|
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) {
|
} catch (err: any) {
|
||||||
console.error("Failed to convert screenshot to PDF:", err);
|
console.error("Failed to process PDF/screenshot:", err);
|
||||||
outputResult.pdfUploadStatus = `Failed to convert screenshot to PDF: ${String(err)}`;
|
outputResult.pdfUploadStatus = `Failed to process file: ${String(err)}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
outputResult.pdfUploadStatus =
|
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) {
|
if (pdfBuffer && generatedPdfPath) {
|
||||||
|
|||||||
@@ -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) {
|
export function CredentialForm({ onClose, userId, defaultValues }: CredentialFormProps) {
|
||||||
const [siteKey, setSiteKey] = useState(defaultValues?.siteKey || "");
|
const [siteKey, setSiteKey] = useState(defaultValues?.siteKey || "");
|
||||||
const [username, setUsername] = useState(defaultValues?.username || "");
|
const [username, setUsername] = useState(defaultValues?.username || "");
|
||||||
@@ -91,14 +98,19 @@ export function CredentialForm({ onClose, userId, defaultValues }: CredentialFor
|
|||||||
</h2>
|
</h2>
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium">Site Key</label>
|
<label className="block text-sm font-medium">Insurance Provider</label>
|
||||||
<input
|
<select
|
||||||
type="text"
|
|
||||||
value={siteKey}
|
value={siteKey}
|
||||||
onChange={(e) => setSiteKey(e.target.value)}
|
onChange={(e) => setSiteKey(e.target.value)}
|
||||||
className="mt-1 p-2 border rounded w-full"
|
className="mt-1 p-2 border rounded w-full bg-white"
|
||||||
placeholder="e.g., MH, Delta MA, (keep the site key exact same)"
|
>
|
||||||
/>
|
<option value="">Select a provider...</option>
|
||||||
|
{SITE_KEY_OPTIONS.map((opt) => (
|
||||||
|
<option key={opt.value} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium">Username</label>
|
<label className="block text-sm font-medium">Username</label>
|
||||||
|
|||||||
@@ -13,6 +13,17 @@ type Credential = {
|
|||||||
password: string;
|
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() {
|
export function CredentialTable() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -108,7 +119,7 @@ export function CredentialTable() {
|
|||||||
<thead className="bg-gray-50">
|
<thead className="bg-gray-50">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Site Key
|
Provider
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
Username
|
Username
|
||||||
@@ -141,7 +152,7 @@ export function CredentialTable() {
|
|||||||
) : (
|
) : (
|
||||||
currentCredentials.map((cred) => (
|
currentCredentials.map((cred) => (
|
||||||
<tr key={cred.id}>
|
<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">{cred.username}</td>
|
||||||
<td className="px-4 py-2">••••••••</td>
|
<td className="px-4 py-2">••••••••</td>
|
||||||
<td className="px-4 py-2 text-right">
|
<td className="px-4 py-2 text-right">
|
||||||
@@ -227,7 +238,7 @@ export function CredentialTable() {
|
|||||||
isOpen={isDeleteDialogOpen}
|
isOpen={isDeleteDialogOpen}
|
||||||
onConfirm={handleConfirmDelete}
|
onConfirm={handleConfirmDelete}
|
||||||
onCancel={handleCancelDelete}
|
onCancel={handleCancelDelete}
|
||||||
entityName={credentialToDelete?.siteKey}
|
entityName={credentialToDelete ? getSiteKeyLabel(credentialToDelete.siteKey) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Dict, Any
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
from selenium.common.exceptions import WebDriverException
|
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||||
|
|
||||||
from selenium_DDMA_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
from selenium_DDMA_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
||||||
|
|
||||||
@@ -127,81 +127,105 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
|||||||
s["message"] = "Session persisted"
|
s["message"] = "Session persisted"
|
||||||
# Continue to step1 below
|
# 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":
|
elif isinstance(login_result, str) and login_result == "OTP_REQUIRED":
|
||||||
s["status"] = "waiting_for_otp"
|
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()
|
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"]
|
driver = s["driver"]
|
||||||
wait = WebDriverWait(driver, 30)
|
|
||||||
|
|
||||||
# Check if there's a popup window and switch to it
|
# Poll the browser to detect when OTP is completed (user enters it directly)
|
||||||
original_window = driver.current_window_handle
|
# We check every 1 second for up to SESSION_OTP_TIMEOUT seconds (faster response)
|
||||||
all_windows = driver.window_handles
|
max_polls = SESSION_OTP_TIMEOUT
|
||||||
if len(all_windows) > 1:
|
login_success = False
|
||||||
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(
|
print(f"[OTP] Waiting for user to enter OTP (polling browser for {SESSION_OTP_TIMEOUT}s)...")
|
||||||
EC.presence_of_element_located(
|
|
||||||
(By.XPATH, "//input[contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code')]")
|
for poll in range(max_polls):
|
||||||
)
|
await asyncio.sleep(1)
|
||||||
)
|
s["last_activity"] = time.time()
|
||||||
otp_input.clear()
|
|
||||||
otp_input.send_keys(otp_value)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
submit_btn = wait.until(
|
# Check current URL - if we're on member search page, login succeeded
|
||||||
EC.element_to_be_clickable(
|
current_url = driver.current_url.lower()
|
||||||
(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
|
print(f"[OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")
|
||||||
)
|
|
||||||
)
|
|
||||||
submit_btn.click()
|
|
||||||
except Exception:
|
|
||||||
otp_input.send_keys("\n")
|
|
||||||
|
|
||||||
# Wait for verification and switch back to main window if needed
|
# 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)
|
await asyncio.sleep(2)
|
||||||
if len(driver.window_handles) > 0:
|
except:
|
||||||
driver.switch_to.window(driver.window_handles[0])
|
pass
|
||||||
|
|
||||||
s["status"] = "otp_submitted"
|
except Exception as poll_err:
|
||||||
s["last_activity"] = time.time()
|
print(f"[OTP Poll {poll+1}] Error: {poll_err}")
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
except Exception as e:
|
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["status"] = "error"
|
||||||
s["message"] = f"Failed to submit OTP into page: {e}"
|
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)
|
await cleanup_session(sid)
|
||||||
return {"status": "error", "message": s["message"]}
|
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"):
|
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
|
||||||
s["status"] = "error"
|
s["status"] = "error"
|
||||||
s["message"] = login_result
|
s["message"] = login_result
|
||||||
await cleanup_session(sid)
|
await cleanup_session(sid)
|
||||||
return {"status": "error", "message": login_result}
|
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
|
# Step 1
|
||||||
step1_result = bot.step1()
|
step1_result = bot.step1()
|
||||||
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Dict, Any
|
|||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
from selenium.common.exceptions import WebDriverException
|
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||||
|
|
||||||
from selenium_DentaQuest_eligibilityCheckWorker import AutomationDentaQuestEligibilityCheck
|
from selenium_DentaQuest_eligibilityCheckWorker import AutomationDentaQuestEligibilityCheck
|
||||||
|
|
||||||
@@ -126,81 +126,105 @@ async def start_dentaquest_run(sid: str, data: dict, url: str):
|
|||||||
s["message"] = "Session persisted"
|
s["message"] = "Session persisted"
|
||||||
# Continue to step1 below
|
# 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":
|
elif isinstance(login_result, str) and login_result == "OTP_REQUIRED":
|
||||||
s["status"] = "waiting_for_otp"
|
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()
|
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"]
|
driver = s["driver"]
|
||||||
wait = WebDriverWait(driver, 30)
|
|
||||||
|
|
||||||
# Check if there's a popup window and switch to it
|
# Poll the browser to detect when OTP is completed (user enters it directly)
|
||||||
original_window = driver.current_window_handle
|
# We check every 1 second for up to SESSION_OTP_TIMEOUT seconds (faster response)
|
||||||
all_windows = driver.window_handles
|
max_polls = SESSION_OTP_TIMEOUT
|
||||||
if len(all_windows) > 1:
|
login_success = False
|
||||||
for window in all_windows:
|
|
||||||
if window != original_window:
|
|
||||||
driver.switch_to.window(window)
|
|
||||||
break
|
|
||||||
|
|
||||||
# DentaQuest OTP input field - adjust selectors as needed
|
print(f"[DentaQuest OTP] Waiting for user to enter OTP (polling browser for {SESSION_OTP_TIMEOUT}s)...")
|
||||||
otp_input = wait.until(
|
|
||||||
EC.presence_of_element_located(
|
for poll in range(max_polls):
|
||||||
(By.XPATH, "//input[contains(@name,'otp') or contains(@name,'code') or contains(@placeholder,'code') or contains(@id,'otp') or @type='tel']")
|
await asyncio.sleep(1)
|
||||||
)
|
s["last_activity"] = time.time()
|
||||||
)
|
|
||||||
otp_input.clear()
|
|
||||||
otp_input.send_keys(otp_value)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
submit_btn = wait.until(
|
# Check current URL - if we're on dashboard/member page, login succeeded
|
||||||
EC.element_to_be_clickable(
|
current_url = driver.current_url.lower()
|
||||||
(By.XPATH, "//button[contains(text(),'Verify') or contains(text(),'Submit') or contains(text(),'Continue') or @type='submit']")
|
print(f"[DentaQuest OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")
|
||||||
)
|
|
||||||
)
|
|
||||||
submit_btn.click()
|
|
||||||
except Exception:
|
|
||||||
otp_input.send_keys("\n")
|
|
||||||
|
|
||||||
# Wait for verification and switch back to main window if needed
|
# 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)
|
await asyncio.sleep(2)
|
||||||
if len(driver.window_handles) > 0:
|
except:
|
||||||
driver.switch_to.window(driver.window_handles[0])
|
pass
|
||||||
|
|
||||||
s["status"] = "otp_submitted"
|
except Exception as poll_err:
|
||||||
s["last_activity"] = time.time()
|
print(f"[DentaQuest OTP Poll {poll+1}] Error: {poll_err}")
|
||||||
await asyncio.sleep(0.5)
|
|
||||||
|
|
||||||
except Exception as e:
|
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["status"] = "error"
|
||||||
s["message"] = f"Failed to submit OTP into page: {e}"
|
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)
|
await cleanup_session(sid)
|
||||||
return {"status": "error", "message": s["message"]}
|
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"):
|
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
|
||||||
s["status"] = "error"
|
s["status"] = "error"
|
||||||
s["message"] = login_result
|
s["message"] = login_result
|
||||||
await cleanup_session(sid)
|
await cleanup_session(sid)
|
||||||
return {"status": "error", "message": login_result}
|
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
|
# Step 1
|
||||||
step1_result = bot.step1()
|
step1_result = bot.step1()
|
||||||
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
if self.massddma_username:
|
if self.massddma_username:
|
||||||
browser_manager.save_credentials_hash(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:
|
try:
|
||||||
otp_candidate = WebDriverWait(self.driver, 30).until(
|
otp_candidate = WebDriverWait(self.driver, 30).until(
|
||||||
EC.presence_of_element_located(
|
EC.presence_of_element_located(
|
||||||
@@ -249,6 +249,36 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
return "OTP_REQUIRED"
|
return "OTP_REQUIRED"
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
print("[login] No OTP input detected in allowed time.")
|
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:
|
except Exception as e:
|
||||||
print("[login] Exception during login:", e)
|
print("[login] Exception during login:", e)
|
||||||
return f"ERROR:LOGIN FAILED: {e}"
|
return f"ERROR:LOGIN FAILED: {e}"
|
||||||
@@ -329,73 +359,220 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
wait = WebDriverWait(self.driver, 90)
|
wait = WebDriverWait(self.driver, 90)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 1) find the eligibility <a> inside the correct cell
|
# Wait for results table to load (use explicit wait instead of fixed sleep)
|
||||||
status_link = wait.until(EC.presence_of_element_located((
|
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,
|
By.XPATH,
|
||||||
"(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]"
|
"(//tbody//tr)[1]//a[contains(@href, 'member-eligibility-search')]"
|
||||||
)))
|
)))
|
||||||
|
|
||||||
eligibilityText = status_link.text.strip().lower()
|
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.
|
# 2) Click on patient name to navigate to detailed patient page
|
||||||
patient_name_div = wait.until(EC.presence_of_element_located((
|
print("[DDMA step2] Clicking on patient name to open detailed page...")
|
||||||
By.XPATH,
|
patient_name_clicked = False
|
||||||
'//div[@class="flex flex-row w-full items-center"]'
|
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:
|
try:
|
||||||
WebDriverWait(self.driver, 30).until(
|
WebDriverWait(self.driver, 30).until(
|
||||||
lambda d: d.execute_script("return document.readyState") == "complete"
|
lambda d: d.execute_script("return document.readyState") == "complete"
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
print("Warning: document.readyState did not become 'complete' within timeout")
|
print("[DDMA step2] Warning: document.readyState did not become 'complete'")
|
||||||
|
|
||||||
# Give some time for lazy content to finish rendering (adjust if needed)
|
# Wait for member details content to load (wait for specific elements)
|
||||||
time.sleep(0.6)
|
print("[DDMA step2] Waiting for member details content to fully load...")
|
||||||
|
content_loaded = False
|
||||||
# Get total page size and DPR
|
content_selectors = [
|
||||||
total_width = int(self.driver.execute_script(
|
"//div[contains(@class,'member') or contains(@class,'detail') or contains(@class,'patient')]",
|
||||||
"return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.documentElement.clientWidth);"
|
"//h1",
|
||||||
))
|
"//h2",
|
||||||
total_height = int(self.driver.execute_script(
|
"//table",
|
||||||
"return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.documentElement.clientHeight);"
|
"//*[contains(text(),'Member ID') or contains(text(),'Name') or contains(text(),'Date of Birth')]",
|
||||||
))
|
]
|
||||||
dpr = float(self.driver.execute_script("return window.devicePixelRatio || 1;"))
|
for selector in content_selectors:
|
||||||
|
|
||||||
# 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:
|
try:
|
||||||
self.driver.execute_cdp_cmd('Emulation.clearDeviceMetricsOverride', {})
|
WebDriverWait(self.driver, 10).until(
|
||||||
except Exception:
|
EC.presence_of_element_located((By.XPATH, selector))
|
||||||
# non-fatal: continue
|
)
|
||||||
|
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
|
pass
|
||||||
|
|
||||||
print("Screenshot saved at:", screenshot_path)
|
# 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
|
||||||
|
|
||||||
# Close the browser window after screenshot (session preserved in profile)
|
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:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
from ddma_browser_manager import get_browser_manager
|
from ddma_browser_manager import get_browser_manager
|
||||||
get_browser_manager().quit_driver()
|
get_browser_manager().quit_driver()
|
||||||
@@ -406,8 +583,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
output = {
|
output = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"eligibility": eligibilityText,
|
"eligibility": eligibilityText,
|
||||||
"ss_path": screenshot_path,
|
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility
|
||||||
"patientName":patientName
|
"pdf_path": pdf_path, # Also add explicit pdf_path
|
||||||
|
"patientName": patientName
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -198,25 +198,48 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
if self.dentaquest_username:
|
if self.dentaquest_username:
|
||||||
browser_manager.save_credentials_hash(self.dentaquest_username)
|
browser_manager.save_credentials_hash(self.dentaquest_username)
|
||||||
|
|
||||||
time.sleep(5)
|
# OTP detection - wait up to 30 seconds for OTP input to appear (like Delta MA)
|
||||||
|
# Use comprehensive XPath to detect various OTP input patterns
|
||||||
# Check for OTP after login
|
|
||||||
try:
|
try:
|
||||||
otp_input = WebDriverWait(self.driver, 10).until(
|
otp_input = WebDriverWait(self.driver, 30).until(
|
||||||
EC.presence_of_element_located((By.XPATH, "//input[@type='tel' or contains(@placeholder,'code') or contains(@aria-label,'Verification')]"))
|
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"
|
return "OTP_REQUIRED"
|
||||||
|
except TimeoutException:
|
||||||
|
print("[DentaQuest login] No OTP input detected in 30 seconds")
|
||||||
|
|
||||||
|
# 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:
|
except TimeoutException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Check if login succeeded
|
# Still on login page - login failed
|
||||||
if "dashboard" in self.driver.current_url.lower():
|
if "onboarding" in current_url_after_login or "login" in current_url_after_login:
|
||||||
return "SUCCESS"
|
print("[DentaQuest login] Login failed - still on login page")
|
||||||
|
return "ERROR: Login failed - check credentials"
|
||||||
|
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
print("[DentaQuest login] Login form elements not found")
|
print("[DentaQuest login] Login form elements not found")
|
||||||
return "ERROR: Login form not found"
|
return "ERROR: Login form not found"
|
||||||
|
|
||||||
|
# If we got here without going through login, we're already logged in
|
||||||
return "SUCCESS"
|
return "SUCCESS"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -335,45 +358,184 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
|
|
||||||
|
|
||||||
def step2(self):
|
def step2(self):
|
||||||
"""Get eligibility status and capture screenshot"""
|
"""Get eligibility status, navigate to detail page, and capture PDF"""
|
||||||
wait = WebDriverWait(self.driver, 90)
|
wait = WebDriverWait(self.driver, 90)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print("[DentaQuest step2] Starting eligibility capture")
|
print("[DentaQuest step2] Starting eligibility capture")
|
||||||
|
|
||||||
# Wait for results to load
|
# Wait for results table to load (use explicit wait instead of fixed sleep)
|
||||||
time.sleep(3)
|
|
||||||
|
|
||||||
# Try to find eligibility status from the results
|
|
||||||
eligibilityText = "unknown"
|
|
||||||
try:
|
try:
|
||||||
# Look for a link or element with eligibility status
|
WebDriverWait(self.driver, 10).until(
|
||||||
status_elem = wait.until(EC.presence_of_element_located((
|
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
|
||||||
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"
|
|
||||||
except TimeoutException:
|
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
|
||||||
patientName = ""
|
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:
|
try:
|
||||||
# Look for the patient name in the results
|
status_elem = self.driver.find_element(By.XPATH, selector)
|
||||||
name_elem = self.driver.find_element(By.XPATH, "//h1 | //div[contains(@class,'name')] | //*[contains(@class,'member-name') or contains(@class,'patient-name')]")
|
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 = ""
|
||||||
|
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:
|
||||||
|
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()
|
patientName = name_elem.text.strip()
|
||||||
print(f"[DentaQuest step2] Found patient name: {patientName}")
|
|
||||||
except:
|
except:
|
||||||
pass
|
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:
|
try:
|
||||||
WebDriverWait(self.driver, 30).until(
|
WebDriverWait(self.driver, 30).until(
|
||||||
lambda d: d.execute_script("return document.readyState") == "complete"
|
lambda d: d.execute_script("return document.readyState") == "complete"
|
||||||
@@ -383,41 +545,31 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# Capture full page screenshot
|
# Generate PDF of the detailed patient page using Chrome DevTools Protocol
|
||||||
print("[DentaQuest step2] Capturing screenshot")
|
print("[DentaQuest step2] Generating PDF of patient detail page...")
|
||||||
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;"))
|
|
||||||
|
|
||||||
self.driver.execute_cdp_cmd('Emulation.setDeviceMetricsOverride', {
|
pdf_options = {
|
||||||
"mobile": False,
|
"landscape": False,
|
||||||
"width": total_width,
|
"displayHeaderFooter": False,
|
||||||
"height": total_height,
|
"printBackground": True,
|
||||||
"deviceScaleFactor": dpr,
|
"preferCSSPageSize": True,
|
||||||
"screenOrientation": {"angle": 0, "type": "portraitPrimary"}
|
"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
|
# Close the browser window after PDF generation
|
||||||
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
|
|
||||||
try:
|
try:
|
||||||
from dentaquest_browser_manager import get_browser_manager
|
from dentaquest_browser_manager import get_browser_manager
|
||||||
get_browser_manager().quit_driver()
|
get_browser_manager().quit_driver()
|
||||||
@@ -428,7 +580,8 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
output = {
|
output = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"eligibility": eligibilityText,
|
"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
|
"patientName": patientName
|
||||||
}
|
}
|
||||||
print(f"[DentaQuest step2] Success: {output}")
|
print(f"[DentaQuest step2] Success: {output}")
|
||||||
|
|||||||
Reference in New Issue
Block a user