feat(eligibility-check) - enhance DDMA and DentaQuest workflows with flexible input handling; added detailed logging for patient data processing and eligibility status updates; improved browser cache management in Selenium service
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
NODE_ENV="development"
|
NODE_ENV="development"
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=5000
|
PORT=5000
|
||||||
FRONTEND_URLS=http://localhost:3000,http://192.168.1.8:3000
|
# FRONTEND_URLS=http://localhost:3000,http://192.168.1.8:3000
|
||||||
|
FRONTEND_URLS=http://localhost:3000
|
||||||
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
||||||
JWT_SECRET = 'dentalsecret'
|
JWT_SECRET = 'dentalsecret'
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ function isOriginAllowed(origin?: string | null) {
|
|||||||
// Dev mode: allow localhost origins automatically
|
// Dev mode: allow localhost origins automatically
|
||||||
if (
|
if (
|
||||||
origin.startsWith("http://localhost") ||
|
origin.startsWith("http://localhost") ||
|
||||||
origin.startsWith("http://127.0.0.1")
|
origin.startsWith("http://127.0.0.1") ||
|
||||||
|
origin.startsWith("http://192.168.0.238")
|
||||||
)
|
)
|
||||||
return true;
|
return true;
|
||||||
// allow explicit FRONTEND_URLS if provided
|
// allow explicit FRONTEND_URLS if provided
|
||||||
|
|||||||
@@ -136,36 +136,135 @@ async function handleDdmaCompletedJob(
|
|||||||
|
|
||||||
// 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();
|
|
||||||
if (!insuranceId) {
|
|
||||||
throw new Error("Missing memberId for ddma job");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2) Create or update patient (with name from selenium result if available)
|
// DEBUG: Log the raw selenium result
|
||||||
|
console.log(`[ddma-eligibility] === DEBUG: Raw seleniumResult ===`);
|
||||||
|
console.log(`[ddma-eligibility] seleniumResult.patientName: '${seleniumResult?.patientName}'`);
|
||||||
|
console.log(`[ddma-eligibility] seleniumResult.memberId: '${seleniumResult?.memberId}'`);
|
||||||
|
console.log(`[ddma-eligibility] seleniumResult.status: '${seleniumResult?.status}'`);
|
||||||
|
|
||||||
|
// 1) Get insuranceId - prefer from Selenium result (flexible search support)
|
||||||
|
let insuranceId = String(seleniumResult?.memberId || "").trim();
|
||||||
|
if (!insuranceId) {
|
||||||
|
insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
|
||||||
|
}
|
||||||
|
console.log(`[ddma-eligibility] Resolved insuranceId: ${insuranceId || "(none)"}`);
|
||||||
|
|
||||||
|
// 2) Get patient name - prefer from Selenium result
|
||||||
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);
|
console.log(`[ddma-eligibility] patientNameFromResult: '${patientNameFromResult}'`);
|
||||||
|
|
||||||
await createOrUpdatePatientByInsuranceId({
|
// Get name from input data as fallback
|
||||||
insuranceId,
|
let firstName = String(insuranceEligibilityData.firstName || "").trim();
|
||||||
|
let lastName = String(insuranceEligibilityData.lastName || "").trim();
|
||||||
|
|
||||||
|
// Override with name from Selenium result if available
|
||||||
|
if (patientNameFromResult) {
|
||||||
|
const parsedName = splitName(patientNameFromResult);
|
||||||
|
console.log(`[ddma-eligibility] splitName result: firstName='${parsedName.firstName}', lastName='${parsedName.lastName}'`);
|
||||||
|
if (parsedName.firstName) firstName = parsedName.firstName;
|
||||||
|
if (parsedName.lastName) lastName = parsedName.lastName;
|
||||||
|
}
|
||||||
|
console.log(`[ddma-eligibility] Resolved name: firstName='${firstName}', lastName='${lastName}'`);
|
||||||
|
|
||||||
|
// 3) Find or create patient
|
||||||
|
let patient: any = null;
|
||||||
|
|
||||||
|
// First, try to find by insuranceId if available
|
||||||
|
if (insuranceId) {
|
||||||
|
patient = await storage.getPatientByInsuranceId(insuranceId);
|
||||||
|
if (patient) {
|
||||||
|
console.log(`[ddma-eligibility] Found patient by insuranceId: ${patient.id}`);
|
||||||
|
|
||||||
|
// Update name if we have better data
|
||||||
|
const updates: any = {};
|
||||||
|
if (firstName && String(patient.firstName ?? "").trim() !== firstName) {
|
||||||
|
updates.firstName = firstName;
|
||||||
|
}
|
||||||
|
if (lastName && String(patient.lastName ?? "").trim() !== lastName) {
|
||||||
|
updates.lastName = lastName;
|
||||||
|
}
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
await storage.updatePatient(patient.id, updates);
|
||||||
|
console.log(`[ddma-eligibility] Updated patient name to: ${firstName} ${lastName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found by ID, try to find by name
|
||||||
|
if (!patient && firstName && lastName) {
|
||||||
|
try {
|
||||||
|
console.log(`[ddma-eligibility] Looking up patient by name: ${firstName} ${lastName}`);
|
||||||
|
const patients = await storage.getPatientsByUserId(job.userId);
|
||||||
|
patient = patients.find(
|
||||||
|
(p: any) =>
|
||||||
|
String(p.firstName ?? "").toLowerCase() === firstName.toLowerCase() &&
|
||||||
|
String(p.lastName ?? "").toLowerCase() === lastName.toLowerCase()
|
||||||
|
) || null;
|
||||||
|
if (patient) {
|
||||||
|
console.log(`[ddma-eligibility] Found patient by name: ${patient.id}`);
|
||||||
|
// Update insuranceId if we have it
|
||||||
|
if (insuranceId && String(patient.insuranceId ?? "").trim() !== insuranceId) {
|
||||||
|
await storage.updatePatient(patient.id, { insuranceId });
|
||||||
|
console.log(`[ddma-eligibility] Updated patient insuranceId to: ${insuranceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log(`[ddma-eligibility] Error finding patient by name: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine eligibility status from Selenium result
|
||||||
|
const eligibilityStatus = seleniumResult.eligibility === "active" ? "ACTIVE" : "INACTIVE";
|
||||||
|
console.log(`[ddma-eligibility] Eligibility status from Delta MA: ${eligibilityStatus}`);
|
||||||
|
|
||||||
|
// If still not found, create new patient
|
||||||
|
console.log(`[ddma-eligibility] Patient creation check: patient=${patient?.id || 'null'}, firstName='${firstName}', lastName='${lastName}'`);
|
||||||
|
if (!patient && firstName && lastName) {
|
||||||
|
console.log(`[ddma-eligibility] Creating new patient: ${firstName} ${lastName} with status: ${eligibilityStatus}`);
|
||||||
|
try {
|
||||||
|
let parsedDob: Date | undefined = undefined;
|
||||||
|
if (insuranceEligibilityData.dateOfBirth) {
|
||||||
|
try {
|
||||||
|
parsedDob = new Date(insuranceEligibilityData.dateOfBirth);
|
||||||
|
if (isNaN(parsedDob.getTime())) parsedDob = undefined;
|
||||||
|
} catch {
|
||||||
|
parsedDob = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPatientData: InsertPatient = {
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
dob: insuranceEligibilityData.dateOfBirth,
|
dateOfBirth: parsedDob || new Date(), // Required field
|
||||||
userId: job.userId,
|
insuranceId: insuranceId || undefined,
|
||||||
});
|
insuranceProvider: "Delta MA", // Set insurance provider
|
||||||
|
gender: "Unknown", // Required field - default value
|
||||||
|
phone: "", // Required field - default empty
|
||||||
|
userId: job.userId, // Required field
|
||||||
|
status: eligibilityStatus, // Set status from eligibility check
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = insertPatientSchema.safeParse(newPatientData);
|
||||||
|
if (validation.success) {
|
||||||
|
patient = await storage.createPatient(validation.data);
|
||||||
|
console.log(`[ddma-eligibility] Created new patient: ${patient.id} with status: ${eligibilityStatus}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[ddma-eligibility] Patient validation failed: ${validation.error.message}`);
|
||||||
|
}
|
||||||
|
} catch (createErr: any) {
|
||||||
|
console.log(`[ddma-eligibility] Failed to create patient: ${createErr.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 3) Update patient status + PDF upload
|
|
||||||
const patient = await storage.getPatientByInsuranceId(
|
|
||||||
insuranceEligibilityData.memberId
|
|
||||||
);
|
|
||||||
if (!patient?.id) {
|
if (!patient?.id) {
|
||||||
outputResult.patientUpdateStatus =
|
outputResult.patientUpdateStatus =
|
||||||
"Patient not found; no update performed";
|
"Patient not found and could not be created; no update performed";
|
||||||
return {
|
return {
|
||||||
patientUpdateStatus: outputResult.patientUpdateStatus,
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
pdfUploadStatus: "none",
|
pdfUploadStatus: "none",
|
||||||
@@ -173,11 +272,10 @@ async function handleDdmaCompletedJob(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// update patient status.
|
// Update patient status from Delta MA eligibility result
|
||||||
const newStatus =
|
await storage.updatePatient(patient.id, { status: eligibilityStatus });
|
||||||
seleniumResult.eligibility === "active" ? "ACTIVE" : "INACTIVE";
|
outputResult.patientUpdateStatus = `Patient ${patient.id} status set to ${eligibilityStatus} (Delta MA eligibility: ${seleniumResult.eligibility})`;
|
||||||
await storage.updatePatient(patient.id, { status: newStatus });
|
console.log(`[ddma-eligibility] ${outputResult.patientUpdateStatus}`);
|
||||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
@@ -200,9 +200,13 @@ async function handleDentaQuestCompletedJob(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine eligibility status from Selenium result
|
||||||
|
const eligibilityStatus = seleniumResult.eligibility === "active" ? "ACTIVE" : "INACTIVE";
|
||||||
|
console.log(`[dentaquest-eligibility] Eligibility status from DentaQuest: ${eligibilityStatus}`);
|
||||||
|
|
||||||
// If still no patient found, CREATE a new one with the data we have
|
// If still no patient found, CREATE a new one with the data we have
|
||||||
if (!patient?.id && firstName && lastName) {
|
if (!patient?.id && firstName && lastName) {
|
||||||
console.log(`[dentaquest-eligibility] Creating new patient: ${firstName} ${lastName}`);
|
console.log(`[dentaquest-eligibility] Creating new patient: ${firstName} ${lastName} with status: ${eligibilityStatus}`);
|
||||||
|
|
||||||
const createPayload: any = {
|
const createPayload: any = {
|
||||||
firstName,
|
firstName,
|
||||||
@@ -212,6 +216,8 @@ async function handleDentaQuestCompletedJob(
|
|||||||
phone: "",
|
phone: "",
|
||||||
userId: job.userId,
|
userId: job.userId,
|
||||||
insuranceId: insuranceId || null,
|
insuranceId: insuranceId || null,
|
||||||
|
insuranceProvider: "DentaQuest", // Set insurance provider
|
||||||
|
status: eligibilityStatus, // Set status from eligibility check
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -219,7 +225,7 @@ async function handleDentaQuestCompletedJob(
|
|||||||
const newPatient = await storage.createPatient(patientData);
|
const newPatient = await storage.createPatient(patientData);
|
||||||
if (newPatient) {
|
if (newPatient) {
|
||||||
patient = newPatient;
|
patient = newPatient;
|
||||||
console.log(`[dentaquest-eligibility] Created new patient with ID: ${patient.id}`);
|
console.log(`[dentaquest-eligibility] Created new patient with ID: ${patient.id}, status: ${eligibilityStatus}`);
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// Try without dateOfBirth if it fails
|
// Try without dateOfBirth if it fails
|
||||||
@@ -230,7 +236,7 @@ async function handleDentaQuestCompletedJob(
|
|||||||
const newPatient = await storage.createPatient(patientData);
|
const newPatient = await storage.createPatient(patientData);
|
||||||
if (newPatient) {
|
if (newPatient) {
|
||||||
patient = newPatient;
|
patient = newPatient;
|
||||||
console.log(`[dentaquest-eligibility] Created new patient (no DOB) with ID: ${patient.id}`);
|
console.log(`[dentaquest-eligibility] Created new patient (no DOB) with ID: ${patient.id}, status: ${eligibilityStatus}`);
|
||||||
}
|
}
|
||||||
} catch (err2: any) {
|
} catch (err2: any) {
|
||||||
console.error(`[dentaquest-eligibility] Failed to create patient: ${err2?.message}`);
|
console.error(`[dentaquest-eligibility] Failed to create patient: ${err2?.message}`);
|
||||||
@@ -248,11 +254,10 @@ async function handleDentaQuestCompletedJob(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// update patient status.
|
// Update patient status from DentaQuest eligibility result
|
||||||
const newStatus =
|
await storage.updatePatient(patient.id, { status: eligibilityStatus });
|
||||||
seleniumResult.eligibility === "active" ? "ACTIVE" : "INACTIVE";
|
outputResult.patientUpdateStatus = `Patient ${patient.id} status set to ${eligibilityStatus} (DentaQuest eligibility: ${seleniumResult.eligibility})`;
|
||||||
await storage.updatePatient(patient.id, { status: newStatus });
|
console.log(`[dentaquest-eligibility] ${outputResult.patientUpdateStatus}`);
|
||||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
|
||||||
|
|
||||||
// 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;
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ export function DdmaEligibilityButton({
|
|||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
// Flexible validation: require DOB + at least one identifier (memberId OR firstName OR lastName)
|
||||||
|
const isDdmaFormIncomplete =
|
||||||
|
!dateOfBirth || (!memberId && !firstName && !lastName);
|
||||||
|
|
||||||
const socketRef = useRef<Socket | null>(null);
|
const socketRef = useRef<Socket | null>(null);
|
||||||
const connectingRef = useRef<Promise<void> | null>(null);
|
const connectingRef = useRef<Promise<void> | null>(null);
|
||||||
|
|
||||||
@@ -371,10 +375,11 @@ export function DdmaEligibilityButton({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const startDdmaEligibility = async () => {
|
const startDdmaEligibility = async () => {
|
||||||
if (!memberId || !dateOfBirth) {
|
// Flexible validation: require DOB + at least one identifier
|
||||||
|
if (!dateOfBirth || (!memberId && !firstName && !lastName)) {
|
||||||
toast({
|
toast({
|
||||||
title: "Missing fields",
|
title: "Missing fields",
|
||||||
description: "Member ID and Date of Birth are required.",
|
description: "Date of Birth and at least one identifier (Member ID, First Name, or Last Name) are required.",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -539,7 +544,7 @@ export function DdmaEligibilityButton({
|
|||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
variant="default"
|
variant="default"
|
||||||
disabled={isFormIncomplete || isStarting}
|
disabled={isDdmaFormIncomplete || isStarting}
|
||||||
onClick={startDdmaEligibility}
|
onClick={startDdmaEligibility}
|
||||||
>
|
>
|
||||||
{isStarting ? (
|
{isStarting ? (
|
||||||
|
|||||||
@@ -111,6 +111,26 @@ class DDMABrowserManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DDMA BrowserManager] Could not clear IndexedDB: {e}")
|
print(f"[DDMA BrowserManager] Could not clear IndexedDB: {e}")
|
||||||
|
|
||||||
|
# Clear browser cache (prevents corrupted cached responses)
|
||||||
|
cache_dirs = [
|
||||||
|
os.path.join(self.profile_dir, "Default", "Cache"),
|
||||||
|
os.path.join(self.profile_dir, "Default", "Code Cache"),
|
||||||
|
os.path.join(self.profile_dir, "Default", "GPUCache"),
|
||||||
|
os.path.join(self.profile_dir, "Default", "Service Worker"),
|
||||||
|
os.path.join(self.profile_dir, "Cache"),
|
||||||
|
os.path.join(self.profile_dir, "Code Cache"),
|
||||||
|
os.path.join(self.profile_dir, "GPUCache"),
|
||||||
|
os.path.join(self.profile_dir, "Service Worker"),
|
||||||
|
os.path.join(self.profile_dir, "ShaderCache"),
|
||||||
|
]
|
||||||
|
for cache_dir in cache_dirs:
|
||||||
|
if os.path.exists(cache_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(cache_dir)
|
||||||
|
print(f"[DDMA BrowserManager] Cleared {os.path.basename(cache_dir)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA BrowserManager] Could not clear {os.path.basename(cache_dir)}: {e}")
|
||||||
|
|
||||||
# Set flag to clear session via JavaScript after browser opens
|
# Set flag to clear session via JavaScript after browser opens
|
||||||
self._needs_session_clear = True
|
self._needs_session_clear = True
|
||||||
|
|
||||||
@@ -235,6 +255,12 @@ class DDMABrowserManager:
|
|||||||
options.add_argument("--no-sandbox")
|
options.add_argument("--no-sandbox")
|
||||||
options.add_argument("--disable-dev-shm-usage")
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
|
|
||||||
|
# Anti-detection options (prevent bot detection)
|
||||||
|
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
|
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||||
|
options.add_experimental_option("useAutomationExtension", False)
|
||||||
|
options.add_argument("--disable-infobars")
|
||||||
|
|
||||||
prefs = {
|
prefs = {
|
||||||
"download.default_directory": self.download_dir,
|
"download.default_directory": self.download_dir,
|
||||||
"plugins.always_open_pdf_externally": True,
|
"plugins.always_open_pdf_externally": True,
|
||||||
@@ -247,6 +273,12 @@ class DDMABrowserManager:
|
|||||||
self._driver = webdriver.Chrome(service=service, options=options)
|
self._driver = webdriver.Chrome(service=service, options=options)
|
||||||
self._driver.maximize_window()
|
self._driver.maximize_window()
|
||||||
|
|
||||||
|
# Remove webdriver property to avoid detection
|
||||||
|
try:
|
||||||
|
self._driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
# Reset the session clear flag (file-based clearing is done on startup)
|
# Reset the session clear flag (file-based clearing is done on startup)
|
||||||
self._needs_session_clear = False
|
self._needs_session_clear = False
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
# 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.massddma_username = self.data.get("massddmaUsername", "")
|
self.massddma_username = self.data.get("massddmaUsername", "")
|
||||||
self.massddma_password = self.data.get("massddmaPassword", "")
|
self.massddma_password = self.data.get("massddmaPassword", "")
|
||||||
|
|
||||||
@@ -284,58 +286,105 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
return f"ERROR:LOGIN FAILED: {e}"
|
return f"ERROR:LOGIN FAILED: {e}"
|
||||||
|
|
||||||
def step1(self):
|
def step1(self):
|
||||||
|
"""Fill search form with all available fields (flexible search)"""
|
||||||
wait = WebDriverWait(self.driver, 30)
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Fill Member ID
|
# Log what fields are available
|
||||||
member_id_input = wait.until(EC.presence_of_element_located((By.XPATH, '//input[@placeholder="Search by member ID"]')))
|
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}")
|
||||||
|
if self.dateOfBirth:
|
||||||
|
fields.append(f"DOB: {self.dateOfBirth}")
|
||||||
|
print(f"[DDMA step1] Starting search with: {', '.join(fields)}")
|
||||||
|
|
||||||
|
# Helper to click, select-all and type
|
||||||
|
def replace_with_sendkeys(el, value):
|
||||||
|
el.click()
|
||||||
|
el.send_keys(Keys.CONTROL, "a")
|
||||||
|
el.send_keys(Keys.BACKSPACE)
|
||||||
|
el.send_keys(value)
|
||||||
|
|
||||||
|
# 1. Fill Member ID if provided
|
||||||
|
if self.memberId:
|
||||||
|
try:
|
||||||
|
member_id_input = wait.until(EC.presence_of_element_located(
|
||||||
|
(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"[DDMA step1] Entered Member ID: {self.memberId}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA step1] Warning: Could not fill Member ID: {e}")
|
||||||
|
|
||||||
# Fill DOB parts
|
# 2. Fill DOB if provided
|
||||||
|
if self.dateOfBirth:
|
||||||
try:
|
try:
|
||||||
dob_parts = self.dateOfBirth.split("-")
|
dob_parts = self.dateOfBirth.split("-")
|
||||||
year = dob_parts[0] # "1964"
|
year = dob_parts[0]
|
||||||
month = dob_parts[1].zfill(2) # "04"
|
month = dob_parts[1].zfill(2)
|
||||||
day = dob_parts[2].zfill(2) # "17"
|
day = dob_parts[2].zfill(2)
|
||||||
except Exception as e:
|
|
||||||
print(f"Error parsing DOB: {e}")
|
|
||||||
return "ERROR: PARSING DOB"
|
|
||||||
|
|
||||||
# 1) locate the specific member DOB container
|
|
||||||
dob_container = wait.until(
|
dob_container = wait.until(
|
||||||
EC.presence_of_element_located(
|
EC.presence_of_element_located(
|
||||||
(By.XPATH, "//div[@data-testid='member-search_date-of-birth']")
|
(By.XPATH, "//div[@data-testid='member-search_date-of-birth']")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2) find the editable spans *inside that container* using relative XPaths
|
|
||||||
month_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']")
|
month_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='month' and @contenteditable='true']")
|
||||||
day_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']")
|
day_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='day' and @contenteditable='true']")
|
||||||
year_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='year' and @contenteditable='true']")
|
year_elem = dob_container.find_element(By.XPATH, ".//span[@data-type='year' and @contenteditable='true']")
|
||||||
|
|
||||||
# Helper to click, select-all and type (pure send_keys approach)
|
|
||||||
def replace_with_sendkeys(el, value):
|
|
||||||
# focus (same as click)
|
|
||||||
el.click()
|
|
||||||
# select all (Ctrl+A) and delete (some apps pick up BACKSPACE better — we use BACKSPACE after select)
|
|
||||||
el.send_keys(Keys.CONTROL, "a")
|
|
||||||
el.send_keys(Keys.BACKSPACE)
|
|
||||||
# type the value
|
|
||||||
el.send_keys(value)
|
|
||||||
# optionally blur or tab out if app expects it
|
|
||||||
# el.send_keys(Keys.TAB)
|
|
||||||
|
|
||||||
replace_with_sendkeys(month_elem, month)
|
replace_with_sendkeys(month_elem, month)
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
replace_with_sendkeys(day_elem, day)
|
replace_with_sendkeys(day_elem, day)
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
replace_with_sendkeys(year_elem, year)
|
replace_with_sendkeys(year_elem, year)
|
||||||
|
print(f"[DDMA step1] Filled DOB: {month}/{day}/{year}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA step1] Warning: Could not fill DOB: {e}")
|
||||||
|
|
||||||
|
# 3. 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")]')
|
||||||
|
))
|
||||||
|
first_name_input.clear()
|
||||||
|
first_name_input.send_keys(self.firstName)
|
||||||
|
print(f"[DDMA step1] Entered First Name: {self.firstName}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA step1] Warning: Could not fill First Name: {e}")
|
||||||
|
|
||||||
# Click Continue button
|
# 4. Fill Last Name if provided
|
||||||
continue_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//button[@data-testid="member-search_search-button"]')))
|
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")]')
|
||||||
|
))
|
||||||
|
last_name_input.clear()
|
||||||
|
last_name_input.send_keys(self.lastName)
|
||||||
|
print(f"[DDMA step1] Entered Last Name: {self.lastName}")
|
||||||
|
time.sleep(0.2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA step1] Warning: Could not fill Last Name: {e}")
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
# Click Search button
|
||||||
|
continue_btn = wait.until(EC.element_to_be_clickable(
|
||||||
|
(By.XPATH, '//button[@data-testid="member-search_search-button"]')
|
||||||
|
))
|
||||||
continue_btn.click()
|
continue_btn.click()
|
||||||
|
print("[DDMA step1] Clicked Search button")
|
||||||
|
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
# Check for error message
|
# Check for error message
|
||||||
try:
|
try:
|
||||||
@@ -343,23 +392,24 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
(By.XPATH, '//div[@data-testid="member-search-result-no-results"]')
|
(By.XPATH, '//div[@data-testid="member-search-result-no-results"]')
|
||||||
))
|
))
|
||||||
if error_msg:
|
if error_msg:
|
||||||
print("Error: Invalid Member ID or Date of Birth.")
|
print("[DDMA step1] Error: No results found")
|
||||||
return "ERROR: INVALID MEMBERID OR DOB"
|
return "ERROR: INVALID SEARCH CRITERIA"
|
||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
print("[DDMA step1] Search completed successfully")
|
||||||
return "Success"
|
return "Success"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error while step1 i.e Cheking the MemberId and DOB in: {e}")
|
print(f"[DDMA step1] Exception: {e}")
|
||||||
return "ERROR:STEP1"
|
return f"ERROR:STEP1 - {e}"
|
||||||
|
|
||||||
|
|
||||||
def step2(self):
|
def step2(self):
|
||||||
wait = WebDriverWait(self.driver, 90)
|
wait = WebDriverWait(self.driver, 90)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Wait for results table to load (use explicit wait instead of fixed sleep)
|
# Wait for results table to load
|
||||||
try:
|
try:
|
||||||
WebDriverWait(self.driver, 10).until(
|
WebDriverWait(self.driver, 10).until(
|
||||||
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
|
EC.presence_of_element_located((By.XPATH, "//tbody//tr"))
|
||||||
@@ -367,10 +417,50 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
except TimeoutException:
|
except TimeoutException:
|
||||||
print("[DDMA step2] Warning: Results table not found within timeout")
|
print("[DDMA step2] Warning: Results table not found within timeout")
|
||||||
|
|
||||||
# 1) Find and extract eligibility status from search results (use short timeout - not critical)
|
# 1) Extract eligibility status and Member ID from search results
|
||||||
eligibilityText = "unknown"
|
eligibilityText = "unknown"
|
||||||
|
foundMemberId = ""
|
||||||
|
patientName = ""
|
||||||
|
|
||||||
|
# Extract data from first row
|
||||||
|
import re
|
||||||
|
try:
|
||||||
|
first_row = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]")
|
||||||
|
row_text = first_row.text.strip()
|
||||||
|
print(f"[DDMA step2] First row text: {row_text[:150]}...")
|
||||||
|
|
||||||
|
if row_text:
|
||||||
|
lines = row_text.split('\n')
|
||||||
|
|
||||||
|
# Extract patient name (first line, before "DOB:")
|
||||||
|
if lines:
|
||||||
|
potential_name = lines[0].strip()
|
||||||
|
# Remove DOB if included in the name
|
||||||
|
potential_name = re.sub(r'\s*DOB[:\s]*\d{1,2}/\d{1,2}/\d{2,4}\s*', '', potential_name, flags=re.IGNORECASE).strip()
|
||||||
|
if potential_name and not potential_name.startswith('DOB') and not potential_name.isdigit():
|
||||||
|
patientName = potential_name
|
||||||
|
print(f"[DDMA step2] Extracted patient name from row: '{patientName}'")
|
||||||
|
|
||||||
|
# Extract Member ID (usually a numeric/alphanumeric ID on its own line)
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line and re.match(r'^[A-Z0-9]{5,}$', line) and not line.startswith('DOB'):
|
||||||
|
foundMemberId = line
|
||||||
|
print(f"[DDMA step2] Extracted Member ID from row: {foundMemberId}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Fallback: use input memberId if not found
|
||||||
|
if not foundMemberId and self.memberId:
|
||||||
|
foundMemberId = self.memberId
|
||||||
|
print(f"[DDMA step2] Using input Member ID: {foundMemberId}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA step2] Error extracting data from row: {e}")
|
||||||
|
if self.memberId:
|
||||||
|
foundMemberId = self.memberId
|
||||||
|
|
||||||
|
# Extract eligibility status
|
||||||
try:
|
try:
|
||||||
# Use short timeout (3s) since this is just for status extraction
|
|
||||||
short_wait = WebDriverWait(self.driver, 3)
|
short_wait = WebDriverWait(self.driver, 3)
|
||||||
status_link = short_wait.until(EC.presence_of_element_located((
|
status_link = short_wait.until(EC.presence_of_element_located((
|
||||||
By.XPATH,
|
By.XPATH,
|
||||||
@@ -394,7 +484,7 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
# 2) Click on patient name to navigate to detailed patient page
|
# 2) Click on patient name to navigate to detailed patient page
|
||||||
print("[DDMA step2] Clicking on patient name to open detailed page...")
|
print("[DDMA step2] Clicking on patient name to open detailed page...")
|
||||||
patient_name_clicked = False
|
patient_name_clicked = False
|
||||||
patientName = ""
|
# Note: Don't reset patientName here - preserve the name extracted from row above
|
||||||
|
|
||||||
# First, let's print what we see on the page for debugging
|
# First, let's print what we see on the page for debugging
|
||||||
current_url_before = self.driver.current_url
|
current_url_before = self.driver.current_url
|
||||||
@@ -424,9 +514,13 @@ 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))
|
||||||
)
|
)
|
||||||
patientName = patient_link.text.strip()
|
link_text = patient_link.text.strip()
|
||||||
href = patient_link.get_attribute("href")
|
href = patient_link.get_attribute("href")
|
||||||
print(f"[DDMA step2] Found patient link: text='{patientName}', href={href}")
|
print(f"[DDMA step2] Found patient link: text='{link_text}', href={href}")
|
||||||
|
|
||||||
|
# Only update patientName if link has text (preserve previously extracted name)
|
||||||
|
if link_text and not patientName:
|
||||||
|
patientName = link_text
|
||||||
|
|
||||||
if href and "member-details" in href:
|
if href and "member-details" in href:
|
||||||
detail_url = href
|
detail_url = href
|
||||||
@@ -525,7 +619,8 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
continue
|
continue
|
||||||
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 already found
|
||||||
|
if not patientName:
|
||||||
try:
|
try:
|
||||||
name_elem = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]//td[1]")
|
name_elem = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]//td[1]")
|
||||||
patientName = name_elem.text.strip()
|
patientName = name_elem.text.strip()
|
||||||
@@ -566,7 +661,9 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
|
|
||||||
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
|
result = self.driver.execute_cdp_cmd("Page.printToPDF", pdf_options)
|
||||||
pdf_data = base64.b64decode(result.get('data', ''))
|
pdf_data = base64.b64decode(result.get('data', ''))
|
||||||
pdf_path = os.path.join(self.download_dir, f"eligibility_{self.memberId}.pdf")
|
# Use foundMemberId for filename if available, otherwise fall back to input memberId
|
||||||
|
pdf_id = foundMemberId or self.memberId or "unknown"
|
||||||
|
pdf_path = os.path.join(self.download_dir, f"eligibility_{pdf_id}.pdf")
|
||||||
with open(pdf_path, "wb") as f:
|
with open(pdf_path, "wb") as f:
|
||||||
f.write(pdf_data)
|
f.write(pdf_data)
|
||||||
|
|
||||||
@@ -580,12 +677,23 @@ class AutomationDeltaDentalMAEligibilityCheck:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[step2] Error closing browser: {e}")
|
print(f"[step2] Error closing browser: {e}")
|
||||||
|
|
||||||
|
# Clean patient name - remove DOB if it was included (already cleaned above but double check)
|
||||||
|
if patientName:
|
||||||
|
# Remove "DOB: MM/DD/YYYY" or similar patterns from the name
|
||||||
|
cleaned_name = re.sub(r'\s*DOB[:\s]*\d{1,2}/\d{1,2}/\d{2,4}\s*', '', patientName, flags=re.IGNORECASE).strip()
|
||||||
|
if cleaned_name:
|
||||||
|
patientName = cleaned_name
|
||||||
|
print(f"[DDMA step2] Cleaned patient name: {patientName}")
|
||||||
|
|
||||||
|
print(f"[DDMA step2] Final data - PatientName: '{patientName}', MemberID: '{foundMemberId}'")
|
||||||
|
|
||||||
output = {
|
output = {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"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 # Include extracted Member ID
|
||||||
}
|
}
|
||||||
return output
|
return output
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -471,48 +471,35 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
eligibilityText = "unknown"
|
eligibilityText = "unknown"
|
||||||
foundMemberId = ""
|
foundMemberId = ""
|
||||||
|
|
||||||
# Try to extract Member ID from the search results
|
# Try to extract Member ID from the first row of search results
|
||||||
|
# Row format: "NAME\nDOB: MM/DD/YYYY\nMEMBER_ID\n..."
|
||||||
import re
|
import re
|
||||||
try:
|
try:
|
||||||
# Look for Member ID in various places
|
first_row = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]")
|
||||||
member_id_selectors = [
|
row_text = first_row.text.strip()
|
||||||
"(//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
|
if row_text:
|
||||||
|
lines = row_text.split('\n')
|
||||||
# Try regex patterns to find Member ID
|
# Member ID is typically the 3rd line (index 2) - a pure number
|
||||||
patterns = [
|
for line in lines:
|
||||||
r'Member ID[:\s]+([A-Z0-9]+)',
|
line = line.strip()
|
||||||
r'ID[:\s]+([A-Z0-9]{5,})',
|
# Member ID is usually a number, could be alphanumeric
|
||||||
r'MemberID[:\s]+([A-Z0-9]+)',
|
# It should be after DOB line and be mostly digits
|
||||||
]
|
if line and re.match(r'^[A-Z0-9]{5,}$', line) and not line.startswith('DOB'):
|
||||||
|
foundMemberId = line
|
||||||
for pattern in patterns:
|
print(f"[DentaQuest step2] Extracted Member ID from row: {foundMemberId}")
|
||||||
match = re.search(pattern, page_text, re.IGNORECASE)
|
|
||||||
if match:
|
|
||||||
foundMemberId = match.group(1).strip()
|
|
||||||
print(f"[DentaQuest step2] Extracted Member ID: {foundMemberId}")
|
|
||||||
break
|
break
|
||||||
|
|
||||||
if not foundMemberId:
|
# Fallback: if we have self.memberId from input, use that
|
||||||
for selector in member_id_selectors:
|
if not foundMemberId and self.memberId:
|
||||||
try:
|
foundMemberId = self.memberId
|
||||||
elem = self.driver.find_element(By.XPATH, selector)
|
print(f"[DentaQuest step2] Using input Member ID: {foundMemberId}")
|
||||||
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:
|
except Exception as e:
|
||||||
print(f"[DentaQuest step2] Error extracting Member ID: {e}")
|
print(f"[DentaQuest step2] Error extracting Member ID: {e}")
|
||||||
|
# Fallback to input memberId
|
||||||
|
if self.memberId:
|
||||||
|
foundMemberId = self.memberId
|
||||||
|
|
||||||
# Extract eligibility status
|
# Extract eligibility status
|
||||||
status_selectors = [
|
status_selectors = [
|
||||||
@@ -558,21 +545,45 @@ class AutomationDentaQuestEligibilityCheck:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[DentaQuest step2] Error listing links: {e}")
|
print(f"[DentaQuest step2] Error listing links: {e}")
|
||||||
|
|
||||||
# Find the patient detail link
|
# Find the patient detail link and extract patient name from row
|
||||||
patient_link_selectors = [
|
patient_link_selectors = [
|
||||||
"(//table//tbody//tr)[1]//td[1]//a", # First column link
|
"(//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-details')]", # member-details link
|
||||||
"(//tbody//tr)[1]//a[contains(@href, 'member')]", # Any member link
|
"(//tbody//tr)[1]//a[contains(@href, 'member')]", # Any member link
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# First, try to extract patient name from the row text (not the link)
|
||||||
|
try:
|
||||||
|
first_row = self.driver.find_element(By.XPATH, "(//tbody//tr)[1]")
|
||||||
|
row_text = first_row.text.strip()
|
||||||
|
print(f"[DentaQuest step2] First row text: {row_text[:100]}...")
|
||||||
|
|
||||||
|
# The name is typically the first line, before "DOB:"
|
||||||
|
if row_text:
|
||||||
|
lines = row_text.split('\n')
|
||||||
|
if lines:
|
||||||
|
# First line is usually the patient name
|
||||||
|
potential_name = lines[0].strip()
|
||||||
|
# Make sure it's not a date or ID
|
||||||
|
if potential_name and not potential_name.startswith('DOB') and not potential_name.isdigit():
|
||||||
|
patientName = potential_name
|
||||||
|
print(f"[DentaQuest step2] Extracted patient name from row: '{patientName}'")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DentaQuest step2] Error extracting name from row: {e}")
|
||||||
|
|
||||||
|
# Now find the detail link
|
||||||
for selector in patient_link_selectors:
|
for selector in patient_link_selectors:
|
||||||
try:
|
try:
|
||||||
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))
|
||||||
)
|
)
|
||||||
patientName = patient_link.text.strip()
|
link_text = patient_link.text.strip()
|
||||||
href = patient_link.get_attribute("href")
|
href = patient_link.get_attribute("href")
|
||||||
print(f"[DentaQuest step2] Found patient link: text='{patientName}', href={href}")
|
print(f"[DentaQuest step2] Found patient link: text='{link_text}', href={href}")
|
||||||
|
|
||||||
|
# If link has text and we don't have patientName yet, use it
|
||||||
|
if link_text and not patientName:
|
||||||
|
patientName = link_text
|
||||||
|
|
||||||
if href and ("member-details" in href or "member" in href):
|
if href and ("member-details" in href or "member" in href):
|
||||||
detail_url = href
|
detail_url = href
|
||||||
|
|||||||
Reference in New Issue
Block a user