feat(eligibility-check) - enhance DentaQuest and United SCO workflows with flexible input handling; added Selenium session clearing on credential updates and deletions; improved patient name extraction and eligibility checks across services
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -39,3 +39,4 @@ dist/
|
|||||||
*.env
|
*.env
|
||||||
*chrome_profile_ddma*
|
*chrome_profile_ddma*
|
||||||
*chrome_profile_dentaquest*
|
*chrome_profile_dentaquest*
|
||||||
|
*chrome_profile_unitedsco*
|
||||||
|
|||||||
@@ -76,8 +76,33 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
if (isNaN(id)) return res.status(400).send("Invalid credential ID");
|
if (isNaN(id)) return res.status(400).send("Invalid credential ID");
|
||||||
|
|
||||||
|
// Get existing credential to know its siteKey
|
||||||
|
const existing = await storage.getInsuranceCredential(id);
|
||||||
|
if (!existing) {
|
||||||
|
return res.status(404).json({ message: "Credential not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const updates = req.body as Partial<InsuranceCredential>;
|
const updates = req.body as Partial<InsuranceCredential>;
|
||||||
const credential = await storage.updateInsuranceCredential(id, updates);
|
const credential = await storage.updateInsuranceCredential(id, updates);
|
||||||
|
|
||||||
|
// Clear Selenium browser session when credentials are changed
|
||||||
|
const seleniumAgentUrl = process.env.SELENIUM_AGENT_URL || "http://localhost:5002";
|
||||||
|
try {
|
||||||
|
if (existing.siteKey === "DDMA") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-ddma-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared DDMA browser session after credential update");
|
||||||
|
} else if (existing.siteKey === "DENTAQUEST") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-dentaquest-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared DentaQuest browser session after credential update");
|
||||||
|
} else if (existing.siteKey === "UNITEDSCO") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared United SCO browser session after credential update");
|
||||||
|
}
|
||||||
|
} catch (seleniumErr) {
|
||||||
|
// Don't fail the update if Selenium session clear fails
|
||||||
|
console.error("[insuranceCreds] Failed to clear Selenium session:", seleniumErr);
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json(credential);
|
return res.status(200).json(credential);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res
|
return res
|
||||||
@@ -115,6 +140,25 @@ router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
.status(404)
|
.status(404)
|
||||||
.json({ message: "Credential not found or already deleted" });
|
.json({ message: "Credential not found or already deleted" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4) Clear Selenium browser session for this provider
|
||||||
|
const seleniumAgentUrl = process.env.SELENIUM_AGENT_URL || "http://localhost:5002";
|
||||||
|
try {
|
||||||
|
if (existing.siteKey === "DDMA") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-ddma-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared DDMA browser session after credential deletion");
|
||||||
|
} else if (existing.siteKey === "DENTAQUEST") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-dentaquest-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared DentaQuest browser session after credential deletion");
|
||||||
|
} else if (existing.siteKey === "UNITEDSCO") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared United SCO browser session after credential deletion");
|
||||||
|
}
|
||||||
|
} catch (seleniumErr) {
|
||||||
|
// Don't fail the delete if Selenium session clear fails
|
||||||
|
console.error("[insuranceCreds] Failed to clear Selenium session:", seleniumErr);
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(204).send();
|
return res.status(204).send();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -136,21 +136,37 @@ async function handleDentaQuestCompletedJob(
|
|||||||
|
|
||||||
// We'll wrap the processing in try/catch/finally so cleanup always runs
|
// We'll wrap the processing in try/catch/finally so cleanup always runs
|
||||||
try {
|
try {
|
||||||
// 1) ensuring memberid.
|
|
||||||
const insuranceEligibilityData = job.insuranceEligibilityData;
|
const insuranceEligibilityData = job.insuranceEligibilityData;
|
||||||
const insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
|
|
||||||
|
// 1) Get Member ID - prefer the one extracted from the page by Selenium,
|
||||||
|
// since we now allow searching by name only
|
||||||
|
let insuranceId = String(seleniumResult?.memberId ?? "").trim();
|
||||||
if (!insuranceId) {
|
if (!insuranceId) {
|
||||||
throw new Error("Missing memberId for DentaQuest job");
|
// Fallback to the one provided in the request
|
||||||
|
insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[dentaquest-eligibility] Insurance ID: ${insuranceId || "(none)"}`);
|
||||||
|
|
||||||
// 2) Create or update patient (with name from selenium result if available)
|
// 2) Create or update patient (with name from selenium result if available)
|
||||||
const patientNameFromResult =
|
const patientNameFromResult =
|
||||||
typeof seleniumResult?.patientName === "string"
|
typeof seleniumResult?.patientName === "string"
|
||||||
? seleniumResult.patientName.trim()
|
? seleniumResult.patientName.trim()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const { firstName, lastName } = splitName(patientNameFromResult);
|
// Get name from request data as fallback
|
||||||
|
let firstName = insuranceEligibilityData.firstName || "";
|
||||||
|
let lastName = insuranceEligibilityData.lastName || "";
|
||||||
|
|
||||||
|
// Override with name from Selenium result if available
|
||||||
|
if (patientNameFromResult) {
|
||||||
|
const parsedName = splitName(patientNameFromResult);
|
||||||
|
firstName = parsedName.firstName || firstName;
|
||||||
|
lastName = parsedName.lastName || lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create or update patient if we have an insurance ID
|
||||||
|
if (insuranceId) {
|
||||||
await createOrUpdatePatientByInsuranceId({
|
await createOrUpdatePatientByInsuranceId({
|
||||||
insuranceId,
|
insuranceId,
|
||||||
firstName,
|
firstName,
|
||||||
@@ -158,14 +174,73 @@ async function handleDentaQuestCompletedJob(
|
|||||||
dob: insuranceEligibilityData.dateOfBirth,
|
dob: insuranceEligibilityData.dateOfBirth,
|
||||||
userId: job.userId,
|
userId: job.userId,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.log("[dentaquest-eligibility] No Member ID available - will try to find patient by name/DOB");
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Update patient status + PDF upload
|
// 3) Update patient status + PDF upload
|
||||||
const patient = await storage.getPatientByInsuranceId(
|
// First try to find by insurance ID, then by name + DOB
|
||||||
insuranceEligibilityData.memberId
|
let patient = insuranceId
|
||||||
);
|
? await storage.getPatientByInsuranceId(insuranceId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// If not found by ID and we have name + DOB, try to find by those
|
||||||
|
if (!patient && firstName && lastName) {
|
||||||
|
console.log(`[dentaquest-eligibility] Looking up patient by name: ${firstName} ${lastName}`);
|
||||||
|
const patients = await storage.getPatientsByUserId(job.userId);
|
||||||
|
patient = patients.find(p =>
|
||||||
|
p.firstName?.toLowerCase() === firstName.toLowerCase() &&
|
||||||
|
p.lastName?.toLowerCase() === lastName.toLowerCase()
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
// If found and we now have the insurance ID, update the patient record
|
||||||
|
if (patient && insuranceId) {
|
||||||
|
await storage.updatePatient(patient.id, { insuranceId });
|
||||||
|
console.log(`[dentaquest-eligibility] Updated patient ${patient.id} with insuranceId: ${insuranceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If still no patient found, CREATE a new one with the data we have
|
||||||
|
if (!patient?.id && firstName && lastName) {
|
||||||
|
console.log(`[dentaquest-eligibility] Creating new patient: ${firstName} ${lastName}`);
|
||||||
|
|
||||||
|
const createPayload: any = {
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
dateOfBirth: insuranceEligibilityData.dateOfBirth || null,
|
||||||
|
gender: "",
|
||||||
|
phone: "",
|
||||||
|
userId: job.userId,
|
||||||
|
insuranceId: insuranceId || null,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const patientData = insertPatientSchema.parse(createPayload);
|
||||||
|
const newPatient = await storage.createPatient(patientData);
|
||||||
|
if (newPatient) {
|
||||||
|
patient = newPatient;
|
||||||
|
console.log(`[dentaquest-eligibility] Created new patient with ID: ${patient.id}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
// Try without dateOfBirth if it fails
|
||||||
|
try {
|
||||||
|
const safePayload = { ...createPayload };
|
||||||
|
delete safePayload.dateOfBirth;
|
||||||
|
const patientData = insertPatientSchema.parse(safePayload);
|
||||||
|
const newPatient = await storage.createPatient(patientData);
|
||||||
|
if (newPatient) {
|
||||||
|
patient = newPatient;
|
||||||
|
console.log(`[dentaquest-eligibility] Created new patient (no DOB) with ID: ${patient.id}`);
|
||||||
|
}
|
||||||
|
} catch (err2: any) {
|
||||||
|
console.error(`[dentaquest-eligibility] Failed to create patient: ${err2?.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!patient?.id) {
|
if (!patient?.id) {
|
||||||
outputResult.patientUpdateStatus =
|
outputResult.patientUpdateStatus =
|
||||||
"Patient not found; no update performed";
|
"Patient not found and could not be created";
|
||||||
return {
|
return {
|
||||||
patientUpdateStatus: outputResult.patientUpdateStatus,
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
pdfUploadStatus: "none",
|
pdfUploadStatus: "none",
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ async function handleUnitedSCOCompletedJob(
|
|||||||
seleniumResult: any
|
seleniumResult: any
|
||||||
) {
|
) {
|
||||||
let createdPdfFileId: number | null = null;
|
let createdPdfFileId: number | null = null;
|
||||||
|
let generatedPdfPath: string | null = null;
|
||||||
const outputResult: any = {};
|
const outputResult: any = {};
|
||||||
|
|
||||||
// We'll wrap the processing in try/catch/finally so cleanup always runs
|
// We'll wrap the processing in try/catch/finally so cleanup always runs
|
||||||
@@ -204,7 +205,6 @@ async function handleUnitedSCOCompletedJob(
|
|||||||
|
|
||||||
// Handle PDF or convert screenshot -> pdf if available
|
// Handle PDF or convert screenshot -> pdf if available
|
||||||
let pdfBuffer: Buffer | null = null;
|
let pdfBuffer: Buffer | null = null;
|
||||||
let generatedPdfPath: string | null = null;
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
seleniumResult &&
|
seleniumResult &&
|
||||||
@@ -233,7 +233,8 @@ async function handleUnitedSCOCompletedJob(
|
|||||||
// Convert image to PDF
|
// Convert image to PDF
|
||||||
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
|
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
|
||||||
|
|
||||||
const pdfFileName = `unitedsco_eligibility_${insuranceEligibilityData.memberId}_${Date.now()}.pdf`;
|
// Use insuranceId (which may come from Selenium result) for filename
|
||||||
|
const pdfFileName = `unitedsco_eligibility_${insuranceId || "unknown"}_${Date.now()}.pdf`;
|
||||||
generatedPdfPath = path.join(
|
generatedPdfPath = path.join(
|
||||||
path.dirname(seleniumResult.ss_path),
|
path.dirname(seleniumResult.ss_path),
|
||||||
pdfFileName
|
pdfFileName
|
||||||
@@ -287,18 +288,25 @@ async function handleUnitedSCOCompletedJob(
|
|||||||
"No valid PDF path provided by Selenium, Couldn't upload pdf to server.";
|
"No valid PDF path provided by Selenium, Couldn't upload pdf to server.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get filename for frontend preview
|
||||||
|
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
patientUpdateStatus: outputResult.patientUpdateStatus,
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
pdfUploadStatus: outputResult.pdfUploadStatus,
|
pdfUploadStatus: outputResult.pdfUploadStatus,
|
||||||
pdfFileId: createdPdfFileId,
|
pdfFileId: createdPdfFileId,
|
||||||
|
pdfFilename,
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
|
// Get filename for frontend preview if available
|
||||||
|
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
|
||||||
return {
|
return {
|
||||||
patientUpdateStatus: outputResult.patientUpdateStatus,
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
pdfUploadStatus:
|
pdfUploadStatus:
|
||||||
outputResult.pdfUploadStatus ??
|
outputResult.pdfUploadStatus ??
|
||||||
`Failed to process United SCO job: ${err?.message ?? String(err)}`,
|
`Failed to process United SCO job: ${err?.message ?? String(err)}`,
|
||||||
pdfFileId: createdPdfFileId,
|
pdfFileId: createdPdfFileId,
|
||||||
|
pdfFilename,
|
||||||
error: err?.message ?? String(err),
|
error: err?.message ?? String(err),
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -127,6 +127,11 @@ export function DentaQuestEligibilityButton({
|
|||||||
const [isStarting, setIsStarting] = useState(false);
|
const [isStarting, setIsStarting] = useState(false);
|
||||||
const [isSubmittingOtp, setIsSubmittingOtp] = useState(false);
|
const [isSubmittingOtp, setIsSubmittingOtp] = useState(false);
|
||||||
|
|
||||||
|
// DentaQuest allows flexible search - only DOB is required, plus at least one identifier
|
||||||
|
// Can use: memberId, firstName, lastName, or any combination
|
||||||
|
const hasAnyIdentifier = memberId || firstName || lastName;
|
||||||
|
const isDentaQuestFormIncomplete = !dateOfBirth || !hasAnyIdentifier;
|
||||||
|
|
||||||
// Clean up socket on unmount
|
// Clean up socket on unmount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
@@ -370,10 +375,13 @@ export function DentaQuestEligibilityButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDentaQuestEligibility = async () => {
|
const startDentaQuestEligibility = async () => {
|
||||||
if (!memberId || !dateOfBirth) {
|
// Flexible search - DOB required plus at least one identifier
|
||||||
|
const hasAnyIdentifier = memberId || firstName || lastName;
|
||||||
|
|
||||||
|
if (!dateOfBirth || !hasAnyIdentifier) {
|
||||||
toast({
|
toast({
|
||||||
title: "Missing fields",
|
title: "Missing fields",
|
||||||
description: "Member ID and Date of Birth are required.",
|
description: "Please provide Date of Birth and at least one of: Member ID, First Name, or Last Name.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -538,7 +546,7 @@ export function DentaQuestEligibilityButton({
|
|||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isFormIncomplete || isStarting}
|
disabled={isDentaQuestFormIncomplete || isStarting}
|
||||||
onClick={startDentaQuestEligibility}
|
onClick={startDentaQuestEligibility}
|
||||||
>
|
>
|
||||||
{isStarting ? (
|
{isStarting ? (
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const SITE_KEY_OPTIONS = [
|
|||||||
{ value: "MH", label: "MassHealth" },
|
{ value: "MH", label: "MassHealth" },
|
||||||
{ value: "DDMA", label: "Delta Dental MA" },
|
{ value: "DDMA", label: "Delta Dental MA" },
|
||||||
{ value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" },
|
{ value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" },
|
||||||
|
{ value: "UNITEDSCO", label: "United SCO" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function CredentialForm({ onClose, userId, defaultValues }: CredentialFormProps) {
|
export function CredentialForm({ onClose, userId, defaultValues }: CredentialFormProps) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const SITE_KEY_LABELS: Record<string, string> = {
|
|||||||
MH: "MassHealth",
|
MH: "MassHealth",
|
||||||
DDMA: "Delta Dental MA",
|
DDMA: "Delta Dental MA",
|
||||||
DENTAQUEST: "Tufts SCO / DentaQuest",
|
DENTAQUEST: "Tufts SCO / DentaQuest",
|
||||||
|
UNITEDSCO: "United SCO",
|
||||||
};
|
};
|
||||||
|
|
||||||
function getSiteKeyLabel(siteKey: string): string {
|
function getSiteKeyLabel(siteKey: string): string {
|
||||||
|
|||||||
@@ -147,6 +147,28 @@ async def start_ddma_run(sid: str, data: dict, url: str):
|
|||||||
s["last_activity"] = time.time()
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Check if OTP was submitted via API (from app)
|
||||||
|
otp_value = s.get("otp_value")
|
||||||
|
if otp_value:
|
||||||
|
print(f"[OTP] OTP received from app: {otp_value}")
|
||||||
|
try:
|
||||||
|
otp_input = driver.find_element(By.XPATH,
|
||||||
|
"//input[contains(@aria-label,'Verification') or contains(@placeholder,'verification') or @type='tel']"
|
||||||
|
)
|
||||||
|
otp_input.clear()
|
||||||
|
otp_input.send_keys(otp_value)
|
||||||
|
# Click verify button
|
||||||
|
try:
|
||||||
|
verify_btn = driver.find_element(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
|
||||||
|
verify_btn.click()
|
||||||
|
except:
|
||||||
|
otp_input.send_keys("\n") # Press Enter as fallback
|
||||||
|
print("[OTP] OTP typed and submitted via app")
|
||||||
|
s["otp_value"] = None # Clear so we don't submit again
|
||||||
|
await asyncio.sleep(3) # Wait for verification
|
||||||
|
except Exception as type_err:
|
||||||
|
print(f"[OTP] Failed to type OTP from app: {type_err}")
|
||||||
|
|
||||||
# Check current URL - if we're on member search page, login succeeded
|
# Check current URL - if we're on member search page, login succeeded
|
||||||
current_url = driver.current_url.lower()
|
current_url = driver.current_url.lower()
|
||||||
print(f"[OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")
|
print(f"[OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")
|
||||||
|
|||||||
@@ -146,6 +146,36 @@ async def start_dentaquest_run(sid: str, data: dict, url: str):
|
|||||||
s["last_activity"] = time.time()
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# Check if OTP was submitted via API (from app)
|
||||||
|
otp_value = s.get("otp_value")
|
||||||
|
if otp_value:
|
||||||
|
print(f"[DentaQuest OTP] OTP received from app: {otp_value}")
|
||||||
|
try:
|
||||||
|
otp_input = driver.find_element(By.XPATH,
|
||||||
|
"//input[contains(@name,'otp') or contains(@name,'code') or @type='tel' or contains(@aria-label,'Verification') or contains(@placeholder,'code')]"
|
||||||
|
)
|
||||||
|
otp_input.clear()
|
||||||
|
otp_input.send_keys(otp_value)
|
||||||
|
# Click verify button - use same pattern as Delta MA
|
||||||
|
try:
|
||||||
|
verify_btn = driver.find_element(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
|
||||||
|
verify_btn.click()
|
||||||
|
print("[DentaQuest OTP] Clicked verify button (aria-label)")
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
# Fallback: try other button patterns
|
||||||
|
verify_btn = driver.find_element(By.XPATH, "//button[contains(text(),'Verify') or contains(text(),'Submit') or @type='submit']")
|
||||||
|
verify_btn.click()
|
||||||
|
print("[DentaQuest OTP] Clicked verify button (text/type)")
|
||||||
|
except:
|
||||||
|
otp_input.send_keys("\n") # Press Enter as fallback
|
||||||
|
print("[DentaQuest OTP] Pressed Enter as fallback")
|
||||||
|
print("[DentaQuest OTP] OTP typed and submitted via app")
|
||||||
|
s["otp_value"] = None # Clear so we don't submit again
|
||||||
|
await asyncio.sleep(3) # Wait for verification
|
||||||
|
except Exception as type_err:
|
||||||
|
print(f"[DentaQuest OTP] Failed to type OTP from app: {type_err}")
|
||||||
|
|
||||||
# Check current URL - if we're on dashboard/member page, login succeeded
|
# Check current URL - if we're on dashboard/member page, login succeeded
|
||||||
current_url = driver.current_url.lower()
|
current_url = driver.current_url.lower()
|
||||||
print(f"[DentaQuest OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")
|
print(f"[DentaQuest OTP Poll {poll+1}/{max_polls}] URL: {current_url[:60]}...")
|
||||||
|
|||||||
@@ -391,8 +391,8 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 2) Extract patient name and click to navigate to detailed patient page
|
# 2) Click on patient name to navigate to detailed patient page
|
||||||
print("[DDMA step2] Extracting patient name and finding detail link...")
|
print("[DDMA step2] Clicking on patient name to open detailed page...")
|
||||||
patient_name_clicked = False
|
patient_name_clicked = False
|
||||||
patientName = ""
|
patientName = ""
|
||||||
|
|
||||||
@@ -400,29 +400,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
current_url_before = self.driver.current_url
|
current_url_before = self.driver.current_url
|
||||||
print(f"[DDMA step2] Current URL before click: {current_url_before}")
|
print(f"[DDMA step2] Current URL before click: {current_url_before}")
|
||||||
|
|
||||||
# Try to extract patient name from the first row of search results
|
|
||||||
# This is more reliable than extracting from link text
|
|
||||||
name_extraction_selectors = [
|
|
||||||
"(//tbody//tr)[1]//td[1]", # First column of first row (usually name)
|
|
||||||
"(//table//tbody//tr)[1]//td[1]", # Alternative table structure
|
|
||||||
"//table//tr[2]//td[1]", # Skip header row
|
|
||||||
"(//tbody//tr)[1]//td[contains(@class,'name')]", # Name column by class
|
|
||||||
"(//tbody//tr)[1]//a", # Link in first row (might contain name)
|
|
||||||
]
|
|
||||||
|
|
||||||
for selector in name_extraction_selectors:
|
|
||||||
try:
|
|
||||||
elem = self.driver.find_element(By.XPATH, selector)
|
|
||||||
text = elem.text.strip()
|
|
||||||
# Filter out non-name text
|
|
||||||
if text and len(text) > 1 and len(text) < 100:
|
|
||||||
if not any(x in text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'date', 'print', 'view', 'details', 'status']):
|
|
||||||
patientName = text
|
|
||||||
print(f"[DDMA step2] Extracted patient name from search results: '{patientName}'")
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Try to find all links in the first row and print them for debugging
|
# Try to find all links in the first row and print them for debugging
|
||||||
try:
|
try:
|
||||||
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
|
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
|
||||||
@@ -431,11 +408,6 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
href = link.get_attribute("href") or "no-href"
|
href = link.get_attribute("href") or "no-href"
|
||||||
text = link.text.strip() or "(empty text)"
|
text = link.text.strip() or "(empty text)"
|
||||||
print(f" Link {i}: href={href[:80]}..., text={text}")
|
print(f" Link {i}: href={href[:80]}..., text={text}")
|
||||||
# Also try to get name from link if we haven't found it yet
|
|
||||||
if not patientName and text and len(text) > 1:
|
|
||||||
if not any(x in text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'view', 'details']):
|
|
||||||
patientName = text
|
|
||||||
print(f"[DDMA step2] Got patient name from link text: '{patientName}'")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DDMA step2] Error listing links: {e}")
|
print(f"[DDMA step2] Error listing links: {e}")
|
||||||
|
|
||||||
@@ -452,14 +424,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
patient_link = WebDriverWait(self.driver, 5).until(
|
patient_link = WebDriverWait(self.driver, 5).until(
|
||||||
EC.presence_of_element_located((By.XPATH, selector))
|
EC.presence_of_element_located((By.XPATH, selector))
|
||||||
)
|
)
|
||||||
link_text = patient_link.text.strip()
|
patientName = patient_link.text.strip()
|
||||||
href = patient_link.get_attribute("href")
|
href = patient_link.get_attribute("href")
|
||||||
print(f"[DDMA step2] Found patient link: text='{link_text}', href={href}")
|
print(f"[DDMA step2] Found patient link: text='{patientName}', href={href}")
|
||||||
|
|
||||||
# Use link text as name if we don't have one yet
|
|
||||||
if not patientName and link_text and len(link_text) > 1:
|
|
||||||
if not any(x in link_text.lower() for x in ['active', 'inactive', 'view', 'details']):
|
|
||||||
patientName = link_text
|
|
||||||
|
|
||||||
if href and "member-details" in href:
|
if href and "member-details" in href:
|
||||||
detail_url = href
|
detail_url = href
|
||||||
@@ -540,70 +507,30 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
# Try to extract patient name from detailed page if not already found
|
# Try to extract patient name from detailed page if not already found
|
||||||
if not patientName:
|
if not patientName:
|
||||||
detail_name_selectors = [
|
detail_name_selectors = [
|
||||||
"//*[contains(@class,'member-name')]",
|
"//h1",
|
||||||
"//*[contains(@class,'patient-name')]",
|
"//h2",
|
||||||
"//h1[not(contains(@class,'page-title'))]",
|
"//*[contains(@class,'patient-name') or contains(@class,'member-name')]",
|
||||||
"//h2[not(contains(@class,'section-title'))]",
|
"//div[contains(@class,'header')]//span",
|
||||||
"//div[contains(@class,'header')]//span[string-length(text()) > 2]",
|
|
||||||
"//div[contains(@class,'member-info')]//span",
|
|
||||||
"//div[contains(@class,'patient-info')]//span",
|
|
||||||
"//span[contains(@class,'name')]",
|
|
||||||
]
|
]
|
||||||
for selector in detail_name_selectors:
|
for selector in detail_name_selectors:
|
||||||
try:
|
try:
|
||||||
name_elem = self.driver.find_element(By.XPATH, selector)
|
name_elem = self.driver.find_element(By.XPATH, selector)
|
||||||
name_text = name_elem.text.strip()
|
name_text = name_elem.text.strip()
|
||||||
if name_text and len(name_text) > 2 and len(name_text) < 100:
|
if name_text and len(name_text) > 1:
|
||||||
# Filter out common non-name text
|
if not any(x in name_text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'date', 'print']):
|
||||||
skip_words = ['active', 'inactive', 'eligible', 'search', 'date', 'print',
|
|
||||||
'view', 'details', 'member', 'patient', 'status', 'eligibility',
|
|
||||||
'welcome', 'home', 'logout', 'menu', 'close', 'expand']
|
|
||||||
if not any(x in name_text.lower() for x in skip_words):
|
|
||||||
patientName = name_text
|
patientName = name_text
|
||||||
print(f"[DDMA step2] Found patient name on detail page: {patientName}")
|
print(f"[DDMA step2] Found patient name on detail page: {patientName}")
|
||||||
break
|
break
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# As a last resort, try to find name in page text using patterns
|
|
||||||
if not patientName:
|
|
||||||
try:
|
|
||||||
# Look for text that looks like a name (First Last format)
|
|
||||||
import re
|
|
||||||
page_text = self.driver.find_element(By.TAG_NAME, "body").text
|
|
||||||
# Look for "Member Name:" or "Patient Name:" followed by text
|
|
||||||
name_patterns = [
|
|
||||||
r'Member Name[:\s]+([A-Z][a-z]+\s+[A-Z][a-z]+)',
|
|
||||||
r'Patient Name[:\s]+([A-Z][a-z]+\s+[A-Z][a-z]+)',
|
|
||||||
r'Name[:\s]+([A-Z][a-z]+\s+[A-Z][a-z]+)',
|
|
||||||
]
|
|
||||||
for pattern in name_patterns:
|
|
||||||
match = re.search(pattern, page_text, re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
patientName = match.group(1).strip()
|
|
||||||
print(f"[DDMA step2] Found patient name via pattern match: {patientName}")
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
print("[DDMA step2] Warning: Could not click on patient, capturing search results page")
|
print("[DDMA step2] Warning: Could not click on patient, capturing search results page")
|
||||||
# Still try to get patient name from search results
|
# Still try to get patient name from search results
|
||||||
if not patientName:
|
|
||||||
name_selectors = [
|
|
||||||
"(//tbody//tr)[1]//td[1]", # First column of first row
|
|
||||||
"(//table//tbody//tr)[1]//td[1]",
|
|
||||||
"(//tbody//tr)[1]//a", # Link in first row
|
|
||||||
]
|
|
||||||
for selector in name_selectors:
|
|
||||||
try:
|
try:
|
||||||
name_elem = self.driver.find_element(By.XPATH, selector)
|
name_elem = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]//td[1]")
|
||||||
text = name_elem.text.strip()
|
patientName = name_elem.text.strip()
|
||||||
if text and len(text) > 1 and not any(x in text.lower() for x in ['active', 'inactive', 'view', 'details']):
|
|
||||||
patientName = text
|
|
||||||
print(f"[DDMA step2] Got patient name from search results: {patientName}")
|
|
||||||
break
|
|
||||||
except:
|
except:
|
||||||
continue
|
pass
|
||||||
|
|
||||||
if not patientName:
|
if not patientName:
|
||||||
print("[DDMA step2] Could not extract patient name")
|
print("[DDMA step2] Could not extract patient name")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from selenium.common.exceptions import WebDriverException, TimeoutException
|
|||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.chrome.service import Service
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
from selenium.webdriver.common.keys import Keys
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
from selenium.webdriver.support import expected_conditions as EC
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
@@ -22,6 +23,8 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
# Flatten values for convenience
|
# Flatten values for convenience
|
||||||
self.memberId = self.data.get("memberId", "")
|
self.memberId = self.data.get("memberId", "")
|
||||||
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||||
|
self.firstName = self.data.get("firstName", "")
|
||||||
|
self.lastName = self.data.get("lastName", "")
|
||||||
self.dentaquest_username = self.data.get("dentaquestUsername", "")
|
self.dentaquest_username = self.data.get("dentaquestUsername", "")
|
||||||
self.dentaquest_password = self.data.get("dentaquestPassword", "")
|
self.dentaquest_password = self.data.get("dentaquestPassword", "")
|
||||||
|
|
||||||
@@ -247,11 +250,20 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
return f"ERROR:LOGIN FAILED: {e}"
|
return f"ERROR:LOGIN FAILED: {e}"
|
||||||
|
|
||||||
def step1(self):
|
def step1(self):
|
||||||
"""Navigate to member search and enter member ID + DOB"""
|
"""Navigate to member search - fills all available fields (Member ID, First Name, Last Name, DOB)"""
|
||||||
wait = WebDriverWait(self.driver, 30)
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
print(f"[DentaQuest step1] Starting member search for ID: {self.memberId}, DOB: {self.dateOfBirth}")
|
# Log what fields are available for search
|
||||||
|
fields = []
|
||||||
|
if self.memberId:
|
||||||
|
fields.append(f"ID: {self.memberId}")
|
||||||
|
if self.firstName:
|
||||||
|
fields.append(f"FirstName: {self.firstName}")
|
||||||
|
if self.lastName:
|
||||||
|
fields.append(f"LastName: {self.lastName}")
|
||||||
|
fields.append(f"DOB: {self.dateOfBirth}")
|
||||||
|
print(f"[DentaQuest step1] Starting member search with: {', '.join(fields)}")
|
||||||
|
|
||||||
# Wait for page to be ready
|
# Wait for page to be ready
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
@@ -267,14 +279,6 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
print(f"[DentaQuest step1] Error parsing DOB: {e}")
|
print(f"[DentaQuest step1] Error parsing DOB: {e}")
|
||||||
return "ERROR: PARSING DOB"
|
return "ERROR: PARSING DOB"
|
||||||
|
|
||||||
# Get today's date for Date of Service
|
|
||||||
from datetime import datetime
|
|
||||||
today = datetime.now()
|
|
||||||
service_month = str(today.month).zfill(2)
|
|
||||||
service_day = str(today.day).zfill(2)
|
|
||||||
service_year = str(today.year)
|
|
||||||
print(f"[DentaQuest step1] Service date: {service_month}/{service_day}/{service_year}")
|
|
||||||
|
|
||||||
# Helper function to fill contenteditable date spans within a specific container
|
# Helper function to fill contenteditable date spans within a specific container
|
||||||
def fill_date_by_testid(testid, month_val, day_val, year_val, field_name):
|
def fill_date_by_testid(testid, month_val, day_val, year_val, field_name):
|
||||||
try:
|
try:
|
||||||
@@ -285,55 +289,127 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
|
|
||||||
def replace_with_sendkeys(el, value):
|
def replace_with_sendkeys(el, value):
|
||||||
el.click()
|
el.click()
|
||||||
time.sleep(0.1)
|
time.sleep(0.05)
|
||||||
# Clear existing content
|
|
||||||
el.send_keys(Keys.CONTROL, "a")
|
el.send_keys(Keys.CONTROL, "a")
|
||||||
time.sleep(0.05)
|
|
||||||
el.send_keys(Keys.BACKSPACE)
|
el.send_keys(Keys.BACKSPACE)
|
||||||
time.sleep(0.05)
|
|
||||||
# Type new value
|
|
||||||
el.send_keys(value)
|
el.send_keys(value)
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# Fill month
|
|
||||||
replace_with_sendkeys(month_elem, month_val)
|
replace_with_sendkeys(month_elem, month_val)
|
||||||
# Tab to day field
|
|
||||||
month_elem.send_keys(Keys.TAB)
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
# Fill day
|
|
||||||
replace_with_sendkeys(day_elem, day_val)
|
replace_with_sendkeys(day_elem, day_val)
|
||||||
# Tab to year field
|
|
||||||
day_elem.send_keys(Keys.TAB)
|
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|
||||||
# Fill year
|
|
||||||
replace_with_sendkeys(year_elem, year_val)
|
replace_with_sendkeys(year_elem, year_val)
|
||||||
# Tab out of the field to trigger validation
|
|
||||||
year_elem.send_keys(Keys.TAB)
|
|
||||||
time.sleep(0.2)
|
|
||||||
|
|
||||||
print(f"[DentaQuest step1] Filled {field_name}: {month_val}/{day_val}/{year_val}")
|
print(f"[DentaQuest step1] Filled {field_name}: {month_val}/{day_val}/{year_val}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DentaQuest step1] Error filling {field_name}: {e}")
|
print(f"[DentaQuest step1] Error filling {field_name}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# 1. Fill Date of Service with TODAY's date using specific data-testid
|
# 1. Select Provider from dropdown (required field)
|
||||||
fill_date_by_testid("member-search_date-of-service", service_month, service_day, service_year, "Date of Service")
|
try:
|
||||||
|
print("[DentaQuest step1] Selecting Provider...")
|
||||||
|
# Try to find and click Provider dropdown
|
||||||
|
provider_selectors = [
|
||||||
|
"//label[contains(text(),'Provider')]/following-sibling::*//div[contains(@class,'select')]",
|
||||||
|
"//div[contains(@data-testid,'provider')]//div[contains(@class,'select')]",
|
||||||
|
"//*[@aria-label='Provider']",
|
||||||
|
"//select[contains(@name,'provider') or contains(@id,'provider')]",
|
||||||
|
"//div[contains(@class,'provider')]//input",
|
||||||
|
"//label[contains(text(),'Provider')]/..//div[contains(@class,'control')]"
|
||||||
|
]
|
||||||
|
|
||||||
|
provider_clicked = False
|
||||||
|
for selector in provider_selectors:
|
||||||
|
try:
|
||||||
|
provider_dropdown = WebDriverWait(self.driver, 3).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, selector))
|
||||||
|
)
|
||||||
|
provider_dropdown.click()
|
||||||
|
print(f"[DentaQuest step1] Clicked provider dropdown with selector: {selector}")
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
provider_clicked = True
|
||||||
|
break
|
||||||
|
except TimeoutException:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if provider_clicked:
|
||||||
|
# Select first available provider option
|
||||||
|
option_selectors = [
|
||||||
|
"//div[contains(@class,'option') and not(contains(@class,'disabled'))]",
|
||||||
|
"//li[contains(@class,'option')]",
|
||||||
|
"//option[not(@disabled)]",
|
||||||
|
"//*[@role='option']"
|
||||||
|
]
|
||||||
|
|
||||||
|
for opt_selector in option_selectors:
|
||||||
|
try:
|
||||||
|
options = self.driver.find_elements(By.XPATH, opt_selector)
|
||||||
|
if options:
|
||||||
|
# Select first non-placeholder option
|
||||||
|
for opt in options:
|
||||||
|
opt_text = opt.text.strip()
|
||||||
|
if opt_text and "select" not in opt_text.lower():
|
||||||
|
opt.click()
|
||||||
|
print(f"[DentaQuest step1] Selected provider: {opt_text}")
|
||||||
|
break
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Close dropdown if still open
|
||||||
|
ActionChains(self.driver).send_keys(Keys.ESCAPE).perform()
|
||||||
|
time.sleep(0.3)
|
||||||
|
else:
|
||||||
|
print("[DentaQuest step1] Warning: Could not find Provider dropdown")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DentaQuest step1] Error selecting provider: {e}")
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
# 2. Fill Date of Birth with patient's DOB using specific data-testid
|
# 2. Fill Date of Birth with patient's DOB using specific data-testid
|
||||||
fill_date_by_testid("member-search_date-of-birth", dob_month, dob_day, dob_year, "Date of Birth")
|
fill_date_by_testid("member-search_date-of-birth", dob_month, dob_day, dob_year, "Date of Birth")
|
||||||
time.sleep(0.5)
|
time.sleep(0.3)
|
||||||
|
|
||||||
# 3. Fill Member ID
|
# 3. Fill ALL available search fields (flexible search)
|
||||||
|
# Fill Member ID if provided
|
||||||
|
if self.memberId:
|
||||||
|
try:
|
||||||
member_id_input = wait.until(EC.presence_of_element_located(
|
member_id_input = wait.until(EC.presence_of_element_located(
|
||||||
(By.XPATH, '//input[@placeholder="Search by member ID"]')
|
(By.XPATH, '//input[@placeholder="Search by member ID"]')
|
||||||
))
|
))
|
||||||
member_id_input.clear()
|
member_id_input.clear()
|
||||||
member_id_input.send_keys(self.memberId)
|
member_id_input.send_keys(self.memberId)
|
||||||
print(f"[DentaQuest step1] Entered member ID: {self.memberId}")
|
print(f"[DentaQuest step1] Entered member ID: {self.memberId}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DentaQuest step1] Warning: Could not fill member ID: {e}")
|
||||||
|
|
||||||
|
# Fill First Name if provided
|
||||||
|
if self.firstName:
|
||||||
|
try:
|
||||||
|
first_name_input = wait.until(EC.presence_of_element_located(
|
||||||
|
(By.XPATH, '//input[@placeholder="First name - 1 char minimum" or contains(@placeholder,"first name") or contains(@name,"firstName") or contains(@id,"firstName")]')
|
||||||
|
))
|
||||||
|
first_name_input.clear()
|
||||||
|
first_name_input.send_keys(self.firstName)
|
||||||
|
print(f"[DentaQuest step1] Entered first name: {self.firstName}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DentaQuest step1] Warning: Could not fill first name: {e}")
|
||||||
|
|
||||||
|
# Fill Last Name if provided
|
||||||
|
if self.lastName:
|
||||||
|
try:
|
||||||
|
last_name_input = wait.until(EC.presence_of_element_located(
|
||||||
|
(By.XPATH, '//input[@placeholder="Last name - 2 char minimum" or contains(@placeholder,"last name") or contains(@name,"lastName") or contains(@id,"lastName")]')
|
||||||
|
))
|
||||||
|
last_name_input.clear()
|
||||||
|
last_name_input.send_keys(self.lastName)
|
||||||
|
print(f"[DentaQuest step1] Entered last name: {self.lastName}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DentaQuest step1] Warning: Could not fill last name: {e}")
|
||||||
|
|
||||||
time.sleep(0.3)
|
time.sleep(0.3)
|
||||||
|
|
||||||
@@ -351,7 +427,8 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
search_btn.click()
|
search_btn.click()
|
||||||
print("[DentaQuest step1] Clicked search button (fallback)")
|
print("[DentaQuest step1] Clicked search button (fallback)")
|
||||||
except:
|
except:
|
||||||
member_id_input.send_keys(Keys.RETURN)
|
# Press Enter on the last input field
|
||||||
|
ActionChains(self.driver).send_keys(Keys.RETURN).perform()
|
||||||
print("[DentaQuest step1] Pressed Enter to search")
|
print("[DentaQuest step1] Pressed Enter to search")
|
||||||
|
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
@@ -363,7 +440,7 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
))
|
))
|
||||||
if error_msg and error_msg.is_displayed():
|
if error_msg and error_msg.is_displayed():
|
||||||
print("[DentaQuest step1] No results found")
|
print("[DentaQuest step1] No results found")
|
||||||
return "ERROR: INVALID MEMBERID OR DOB"
|
return "ERROR: INVALID SEARCH CRITERIA"
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -390,8 +467,54 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
print("[DentaQuest step2] Warning: Results table not found within timeout")
|
print("[DentaQuest step2] Warning: Results table not found within timeout")
|
||||||
|
|
||||||
# 1) Find and extract eligibility status from search results
|
# 1) Find and extract eligibility status and Member ID from search results
|
||||||
eligibilityText = "unknown"
|
eligibilityText = "unknown"
|
||||||
|
foundMemberId = ""
|
||||||
|
|
||||||
|
# Try to extract Member ID from the search results
|
||||||
|
import re
|
||||||
|
try:
|
||||||
|
# Look for Member ID in various places
|
||||||
|
member_id_selectors = [
|
||||||
|
"(//tbody//tr)[1]//td[contains(text(),'ID:') or contains(@data-testid,'member-id')]",
|
||||||
|
"//span[contains(text(),'Member ID')]/..//span",
|
||||||
|
"//div[contains(text(),'Member ID')]",
|
||||||
|
"(//tbody//tr)[1]//td[2]", # Often Member ID is in second column
|
||||||
|
]
|
||||||
|
|
||||||
|
page_text = self.driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
|
||||||
|
# Try regex patterns to find Member ID
|
||||||
|
patterns = [
|
||||||
|
r'Member ID[:\s]+([A-Z0-9]+)',
|
||||||
|
r'ID[:\s]+([A-Z0-9]{5,})',
|
||||||
|
r'MemberID[:\s]+([A-Z0-9]+)',
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
match = re.search(pattern, page_text, re.IGNORECASE)
|
||||||
|
if match:
|
||||||
|
foundMemberId = match.group(1).strip()
|
||||||
|
print(f"[DentaQuest step2] Extracted Member ID: {foundMemberId}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not foundMemberId:
|
||||||
|
for selector in member_id_selectors:
|
||||||
|
try:
|
||||||
|
elem = self.driver.find_element(By.XPATH, selector)
|
||||||
|
text = elem.text.strip()
|
||||||
|
# Extract just the ID part
|
||||||
|
id_match = re.search(r'([A-Z0-9]{5,})', text)
|
||||||
|
if id_match:
|
||||||
|
foundMemberId = id_match.group(1)
|
||||||
|
print(f"[DentaQuest step2] Found Member ID via selector: {foundMemberId}")
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DentaQuest step2] Error extracting Member ID: {e}")
|
||||||
|
|
||||||
|
# Extract eligibility status
|
||||||
status_selectors = [
|
status_selectors = [
|
||||||
"(//tbody//tr)[1]//a[contains(@href, 'eligibility')]",
|
"(//tbody//tr)[1]//a[contains(@href, 'eligibility')]",
|
||||||
"//a[contains(@href,'eligibility')]",
|
"//a[contains(@href,'eligibility')]",
|
||||||
@@ -424,25 +547,6 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
current_url_before = self.driver.current_url
|
current_url_before = self.driver.current_url
|
||||||
print(f"[DentaQuest step2] Current URL before: {current_url_before}")
|
print(f"[DentaQuest step2] Current URL before: {current_url_before}")
|
||||||
|
|
||||||
# Try to extract patient name from search results first
|
|
||||||
name_extraction_selectors = [
|
|
||||||
"(//tbody//tr)[1]//td[1]", # First column of first row
|
|
||||||
"(//table//tbody//tr)[1]//td[1]",
|
|
||||||
"//table//tr[2]//td[1]", # Skip header row
|
|
||||||
"(//tbody//tr)[1]//a", # Link in first row
|
|
||||||
]
|
|
||||||
for selector in name_extraction_selectors:
|
|
||||||
try:
|
|
||||||
elem = self.driver.find_element(By.XPATH, selector)
|
|
||||||
text = elem.text.strip()
|
|
||||||
if text and len(text) > 1 and len(text) < 100:
|
|
||||||
if not any(x in text.lower() for x in ['active', 'inactive', 'eligible', 'search', 'view', 'details', 'status']):
|
|
||||||
patientName = text
|
|
||||||
print(f"[DentaQuest step2] Extracted patient name from search results: '{patientName}'")
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Find all links in first row and log them
|
# Find all links in first row and log them
|
||||||
try:
|
try:
|
||||||
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
|
all_links = self.driver.find_elements(By.XPATH, "(//tbody//tr)[1]//a")
|
||||||
@@ -619,7 +723,8 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
"eligibility": eligibilityText,
|
"eligibility": eligibilityText,
|
||||||
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility
|
"ss_path": pdf_path, # Keep key as ss_path for backward compatibility
|
||||||
"pdf_path": pdf_path, # Also add explicit pdf_path
|
"pdf_path": pdf_path, # Also add explicit pdf_path
|
||||||
"patientName": patientName
|
"patientName": patientName,
|
||||||
|
"memberId": foundMemberId # Member ID extracted from the page
|
||||||
}
|
}
|
||||||
print(f"[DentaQuest step2] Success: {output}")
|
print(f"[DentaQuest step2] Success: {output}")
|
||||||
return output
|
return output
|
||||||
|
|||||||
@@ -267,13 +267,9 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
"""
|
"""
|
||||||
Navigate to Eligibility page and fill the Patient Information form.
|
Navigate to Eligibility page and fill the Patient Information form.
|
||||||
|
|
||||||
FLEXIBLE INPUT SUPPORT:
|
Workflow based on actual DOM testing:
|
||||||
- If Member ID is provided: Fill Subscriber ID + DOB (+ optional First/Last Name)
|
|
||||||
- If no Member ID but First/Last Name provided: Fill First Name + Last Name + DOB
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. Navigate directly to eligibility page
|
1. Navigate directly to eligibility page
|
||||||
2. Fill available fields based on input
|
2. Fill First Name (id='firstName_Back'), Last Name (id='lastName_Back'), DOB (id='dateOfBirth_Back')
|
||||||
3. Select Payer: "UnitedHealthcare Massachusetts" from ng-select dropdown
|
3. Select Payer: "UnitedHealthcare Massachusetts" from ng-select dropdown
|
||||||
4. Click Continue
|
4. Click Continue
|
||||||
5. Handle Practitioner & Location page - click paymentGroupId dropdown, select Summit Dental Care
|
5. Handle Practitioner & Location page - click paymentGroupId dropdown, select Summit Dental Care
|
||||||
@@ -282,17 +278,7 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
from selenium.webdriver.common.action_chains import ActionChains
|
from selenium.webdriver.common.action_chains import ActionChains
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Determine which input mode to use
|
print(f"[UnitedSCO step1] Starting eligibility search for: {self.firstName} {self.lastName}, DOB: {self.dateOfBirth}")
|
||||||
has_member_id = bool(self.memberId and self.memberId.strip())
|
|
||||||
has_name = bool(self.firstName and self.firstName.strip() and self.lastName and self.lastName.strip())
|
|
||||||
|
|
||||||
if has_member_id:
|
|
||||||
print(f"[UnitedSCO step1] Using Member ID mode: ID={self.memberId}, DOB={self.dateOfBirth}")
|
|
||||||
elif has_name:
|
|
||||||
print(f"[UnitedSCO step1] Using Name mode: {self.firstName} {self.lastName}, DOB={self.dateOfBirth}")
|
|
||||||
else:
|
|
||||||
print("[UnitedSCO step1] ERROR: Need either Member ID or First Name + Last Name")
|
|
||||||
return "ERROR: Missing required input (Member ID or Name)"
|
|
||||||
|
|
||||||
# Navigate directly to eligibility page
|
# Navigate directly to eligibility page
|
||||||
print("[UnitedSCO step1] Navigating to eligibility page...")
|
print("[UnitedSCO step1] Navigating to eligibility page...")
|
||||||
@@ -305,28 +291,17 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
# Step 1.1: Fill the Patient Information form
|
# Step 1.1: Fill the Patient Information form
|
||||||
print("[UnitedSCO step1] Filling Patient Information form...")
|
print("[UnitedSCO step1] Filling Patient Information form...")
|
||||||
|
|
||||||
# Wait for form to load
|
# Wait for form to load - look for First Name field (id='firstName_Back')
|
||||||
try:
|
try:
|
||||||
WebDriverWait(self.driver, 10).until(
|
WebDriverWait(self.driver, 10).until(
|
||||||
EC.presence_of_element_located((By.ID, "subscriberId_Front"))
|
EC.presence_of_element_located((By.ID, "firstName_Back"))
|
||||||
)
|
)
|
||||||
print("[UnitedSCO step1] Patient Information form loaded")
|
print("[UnitedSCO step1] Patient Information form loaded")
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
print("[UnitedSCO step1] Patient Information form not found")
|
print("[UnitedSCO step1] Patient Information form not found")
|
||||||
return "ERROR: Patient Information form not found"
|
return "ERROR: Patient Information form not found"
|
||||||
|
|
||||||
# Fill Subscriber ID if provided (id='subscriberId_Front')
|
# Fill First Name (id='firstName_Back')
|
||||||
if has_member_id:
|
|
||||||
try:
|
|
||||||
subscriber_id_input = self.driver.find_element(By.ID, "subscriberId_Front")
|
|
||||||
subscriber_id_input.clear()
|
|
||||||
subscriber_id_input.send_keys(self.memberId)
|
|
||||||
print(f"[UnitedSCO step1] Entered Subscriber ID: {self.memberId}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"[UnitedSCO step1] Error entering Subscriber ID: {e}")
|
|
||||||
|
|
||||||
# Fill First Name if provided (id='firstName_Back')
|
|
||||||
if self.firstName and self.firstName.strip():
|
|
||||||
try:
|
try:
|
||||||
first_name_input = self.driver.find_element(By.ID, "firstName_Back")
|
first_name_input = self.driver.find_element(By.ID, "firstName_Back")
|
||||||
first_name_input.clear()
|
first_name_input.clear()
|
||||||
@@ -334,11 +309,9 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
print(f"[UnitedSCO step1] Entered First Name: {self.firstName}")
|
print(f"[UnitedSCO step1] Entered First Name: {self.firstName}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[UnitedSCO step1] Error entering First Name: {e}")
|
print(f"[UnitedSCO step1] Error entering First Name: {e}")
|
||||||
if not has_member_id: # Only fail if we're relying on name
|
|
||||||
return "ERROR: Could not enter First Name"
|
return "ERROR: Could not enter First Name"
|
||||||
|
|
||||||
# Fill Last Name if provided (id='lastName_Back')
|
# Fill Last Name (id='lastName_Back')
|
||||||
if self.lastName and self.lastName.strip():
|
|
||||||
try:
|
try:
|
||||||
last_name_input = self.driver.find_element(By.ID, "lastName_Back")
|
last_name_input = self.driver.find_element(By.ID, "lastName_Back")
|
||||||
last_name_input.clear()
|
last_name_input.clear()
|
||||||
@@ -346,10 +319,9 @@ class AutomationUnitedSCOEligibilityCheck:
|
|||||||
print(f"[UnitedSCO step1] Entered Last Name: {self.lastName}")
|
print(f"[UnitedSCO step1] Entered Last Name: {self.lastName}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[UnitedSCO step1] Error entering Last Name: {e}")
|
print(f"[UnitedSCO step1] Error entering Last Name: {e}")
|
||||||
if not has_member_id: # Only fail if we're relying on name
|
|
||||||
return "ERROR: Could not enter Last Name"
|
return "ERROR: Could not enter Last Name"
|
||||||
|
|
||||||
# Fill Date of Birth (id='dateOfBirth_Back', format: MM/DD/YYYY) - always required
|
# Fill Date of Birth (id='dateOfBirth_Back', format: MM/DD/YYYY)
|
||||||
try:
|
try:
|
||||||
dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back")
|
dob_input = self.driver.find_element(By.ID, "dateOfBirth_Back")
|
||||||
dob_input.clear()
|
dob_input.clear()
|
||||||
|
|||||||
Reference in New Issue
Block a user