feat(eligibility-check) - implement Delta Dental Ins eligibility workflow with OTP handling; added routes, services, and frontend components for patient data processing and eligibility status retrieval; enhanced browser session management and logging
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -40,3 +40,5 @@ dist/
|
|||||||
*chrome_profile_ddma*
|
*chrome_profile_ddma*
|
||||||
*chrome_profile_dentaquest*
|
*chrome_profile_dentaquest*
|
||||||
*chrome_profile_unitedsco*
|
*chrome_profile_unitedsco*
|
||||||
|
*chrome_profile_deltains*
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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
|
FRONTEND_URLS=http://192.168.0.238: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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -11,6 +11,7 @@ import insuranceStatusRoutes from "./insuranceStatus";
|
|||||||
import insuranceStatusDdmaRoutes from "./insuranceStatusDDMA";
|
import insuranceStatusDdmaRoutes from "./insuranceStatusDDMA";
|
||||||
import insuranceStatusDentaQuestRoutes from "./insuranceStatusDentaQuest";
|
import insuranceStatusDentaQuestRoutes from "./insuranceStatusDentaQuest";
|
||||||
import insuranceStatusUnitedSCORoutes from "./insuranceStatusUnitedSCO";
|
import insuranceStatusUnitedSCORoutes from "./insuranceStatusUnitedSCO";
|
||||||
|
import insuranceStatusDeltaInsRoutes from "./insuranceStatusDeltaIns";
|
||||||
import paymentsRoutes from "./payments";
|
import paymentsRoutes from "./payments";
|
||||||
import databaseManagementRoutes from "./database-management";
|
import databaseManagementRoutes from "./database-management";
|
||||||
import notificationsRoutes from "./notifications";
|
import notificationsRoutes from "./notifications";
|
||||||
@@ -33,6 +34,7 @@ router.use("/insurance-status", insuranceStatusRoutes);
|
|||||||
router.use("/insurance-status-ddma", insuranceStatusDdmaRoutes);
|
router.use("/insurance-status-ddma", insuranceStatusDdmaRoutes);
|
||||||
router.use("/insurance-status-dentaquest", insuranceStatusDentaQuestRoutes);
|
router.use("/insurance-status-dentaquest", insuranceStatusDentaQuestRoutes);
|
||||||
router.use("/insurance-status-unitedsco", insuranceStatusUnitedSCORoutes);
|
router.use("/insurance-status-unitedsco", insuranceStatusUnitedSCORoutes);
|
||||||
|
router.use("/insurance-status-deltains", insuranceStatusDeltaInsRoutes);
|
||||||
router.use("/payments", paymentsRoutes);
|
router.use("/payments", paymentsRoutes);
|
||||||
router.use("/database-management", databaseManagementRoutes);
|
router.use("/database-management", databaseManagementRoutes);
|
||||||
router.use("/notifications", notificationsRoutes);
|
router.use("/notifications", notificationsRoutes);
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ router.put("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
} else if (existing.siteKey === "UNITEDSCO") {
|
} else if (existing.siteKey === "UNITEDSCO") {
|
||||||
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
|
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
|
||||||
console.log("[insuranceCreds] Cleared United SCO browser session after credential update");
|
console.log("[insuranceCreds] Cleared United SCO browser session after credential update");
|
||||||
|
} else if (existing.siteKey === "DELTAINS") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-deltains-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared Delta Dental Ins browser session after credential update");
|
||||||
}
|
}
|
||||||
} catch (seleniumErr) {
|
} catch (seleniumErr) {
|
||||||
// Don't fail the update if Selenium session clear fails
|
// Don't fail the update if Selenium session clear fails
|
||||||
@@ -153,6 +156,9 @@ router.delete("/:id", async (req: Request, res: Response): Promise<any> => {
|
|||||||
} else if (existing.siteKey === "UNITEDSCO") {
|
} else if (existing.siteKey === "UNITEDSCO") {
|
||||||
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
|
await fetch(`${seleniumAgentUrl}/clear-unitedsco-session`, { method: "POST" });
|
||||||
console.log("[insuranceCreds] Cleared United SCO browser session after credential deletion");
|
console.log("[insuranceCreds] Cleared United SCO browser session after credential deletion");
|
||||||
|
} else if (existing.siteKey === "DELTAINS") {
|
||||||
|
await fetch(`${seleniumAgentUrl}/clear-deltains-session`, { method: "POST" });
|
||||||
|
console.log("[insuranceCreds] Cleared Delta Dental Ins browser session after credential deletion");
|
||||||
}
|
}
|
||||||
} catch (seleniumErr) {
|
} catch (seleniumErr) {
|
||||||
// Don't fail the delete if Selenium session clear fails
|
// Don't fail the delete if Selenium session clear fails
|
||||||
|
|||||||
749
apps/Backend/src/routes/insuranceStatusDeltaIns.ts
Normal file
749
apps/Backend/src/routes/insuranceStatusDeltaIns.ts
Normal file
@@ -0,0 +1,749 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { storage } from "../storage";
|
||||||
|
import {
|
||||||
|
forwardToSeleniumDeltaInsEligibilityAgent,
|
||||||
|
forwardOtpToSeleniumDeltaInsAgent,
|
||||||
|
getSeleniumDeltaInsSessionStatus,
|
||||||
|
} from "../services/seleniumDeltainsInsuranceEligibilityClient";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import fsSync from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import PDFDocument from "pdfkit";
|
||||||
|
import { emptyFolderContainingFile } from "../utils/emptyTempFolder";
|
||||||
|
import {
|
||||||
|
InsertPatient,
|
||||||
|
insertPatientSchema,
|
||||||
|
} from "../../../../packages/db/types/patient-types";
|
||||||
|
import { io } from "../socket";
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/** Job context stored in memory by sessionId */
|
||||||
|
interface DeltaInsJobContext {
|
||||||
|
userId: number;
|
||||||
|
insuranceEligibilityData: any;
|
||||||
|
socketId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltainsJobs: Record<string, DeltaInsJobContext> = {};
|
||||||
|
|
||||||
|
/** Utility: naive name splitter */
|
||||||
|
function splitName(fullName?: string | null) {
|
||||||
|
if (!fullName) return { firstName: "", lastName: "" };
|
||||||
|
const parts = fullName.trim().split(/\s+/).filter(Boolean);
|
||||||
|
const firstName = parts.shift() ?? "";
|
||||||
|
const lastName = parts.join(" ") ?? "";
|
||||||
|
return { firstName, lastName };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function imageToPdfBuffer(imagePath: string): Promise<Buffer> {
|
||||||
|
return new Promise<Buffer>((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const doc = new PDFDocument({ autoFirstPage: false });
|
||||||
|
const chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
|
doc.on("data", (chunk: any) => chunks.push(chunk));
|
||||||
|
doc.on("end", () => resolve(Buffer.concat(chunks)));
|
||||||
|
doc.on("error", (err: any) => reject(err));
|
||||||
|
|
||||||
|
const A4_WIDTH = 595.28;
|
||||||
|
const A4_HEIGHT = 841.89;
|
||||||
|
|
||||||
|
doc.addPage({ size: [A4_WIDTH, A4_HEIGHT] });
|
||||||
|
|
||||||
|
doc.image(imagePath, 0, 0, {
|
||||||
|
fit: [A4_WIDTH, A4_HEIGHT],
|
||||||
|
align: "center",
|
||||||
|
valign: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.end();
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure patient exists for given insuranceId.
|
||||||
|
*/
|
||||||
|
async function createOrUpdatePatientByInsuranceId(options: {
|
||||||
|
insuranceId: string;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
dob?: string | Date | null;
|
||||||
|
userId: number;
|
||||||
|
eligibilityStatus?: string;
|
||||||
|
}) {
|
||||||
|
const { insuranceId, firstName, lastName, dob, userId, eligibilityStatus } = options;
|
||||||
|
if (!insuranceId) throw new Error("Missing insuranceId");
|
||||||
|
|
||||||
|
const incomingFirst = (firstName || "").trim();
|
||||||
|
const incomingLast = (lastName || "").trim();
|
||||||
|
|
||||||
|
let patient = await storage.getPatientByInsuranceId(insuranceId);
|
||||||
|
|
||||||
|
if (patient && patient.id) {
|
||||||
|
const updates: any = {};
|
||||||
|
if (
|
||||||
|
incomingFirst &&
|
||||||
|
String(patient.firstName ?? "").trim() !== incomingFirst
|
||||||
|
) {
|
||||||
|
updates.firstName = incomingFirst;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
incomingLast &&
|
||||||
|
String(patient.lastName ?? "").trim() !== incomingLast
|
||||||
|
) {
|
||||||
|
updates.lastName = incomingLast;
|
||||||
|
}
|
||||||
|
if (Object.keys(updates).length > 0) {
|
||||||
|
await storage.updatePatient(patient.id, updates);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
console.log(`[deltains-eligibility] Creating new patient: ${incomingFirst} ${incomingLast} with status: ${eligibilityStatus || "UNKNOWN"}`);
|
||||||
|
const createPayload: any = {
|
||||||
|
firstName: incomingFirst,
|
||||||
|
lastName: incomingLast,
|
||||||
|
dateOfBirth: dob,
|
||||||
|
gender: "Unknown",
|
||||||
|
phone: "",
|
||||||
|
userId,
|
||||||
|
insuranceId,
|
||||||
|
insuranceProvider: "Delta Dental Ins",
|
||||||
|
status: eligibilityStatus || "UNKNOWN",
|
||||||
|
};
|
||||||
|
let patientData: InsertPatient;
|
||||||
|
try {
|
||||||
|
patientData = insertPatientSchema.parse(createPayload);
|
||||||
|
} catch (err) {
|
||||||
|
const safePayload = { ...createPayload };
|
||||||
|
delete (safePayload as any).dateOfBirth;
|
||||||
|
patientData = insertPatientSchema.parse(safePayload);
|
||||||
|
}
|
||||||
|
const newPatient = await storage.createPatient(patientData);
|
||||||
|
console.log(`[deltains-eligibility] Created new patient: ${newPatient.id} with status: ${eligibilityStatus || "UNKNOWN"}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When Selenium finishes for a given sessionId, run patient + PDF pipeline.
|
||||||
|
*/
|
||||||
|
async function handleDeltaInsCompletedJob(
|
||||||
|
sessionId: string,
|
||||||
|
job: DeltaInsJobContext,
|
||||||
|
seleniumResult: any
|
||||||
|
) {
|
||||||
|
let createdPdfFileId: number | null = null;
|
||||||
|
let generatedPdfPath: string | null = null;
|
||||||
|
const outputResult: any = {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const insuranceEligibilityData = job.insuranceEligibilityData;
|
||||||
|
|
||||||
|
let insuranceId = String(seleniumResult?.memberId ?? "").trim();
|
||||||
|
if (!insuranceId) {
|
||||||
|
insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!insuranceId) {
|
||||||
|
console.log("[deltains-eligibility] No Member ID found - will use name for patient lookup");
|
||||||
|
} else {
|
||||||
|
console.log(`[deltains-eligibility] Using Member ID: ${insuranceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const patientNameFromResult =
|
||||||
|
typeof seleniumResult?.patientName === "string"
|
||||||
|
? seleniumResult.patientName.trim()
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let firstName = insuranceEligibilityData.firstName || "";
|
||||||
|
let lastName = insuranceEligibilityData.lastName || "";
|
||||||
|
|
||||||
|
if (patientNameFromResult) {
|
||||||
|
const parsedName = splitName(patientNameFromResult);
|
||||||
|
firstName = parsedName.firstName || firstName;
|
||||||
|
lastName = parsedName.lastName || lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawEligibility = String(seleniumResult?.eligibility ?? "").toLowerCase();
|
||||||
|
const eligibilityStatus = rawEligibility.includes("active") || rawEligibility.includes("eligible")
|
||||||
|
? "ACTIVE" : "INACTIVE";
|
||||||
|
console.log(`[deltains-eligibility] Eligibility status: ${eligibilityStatus}`);
|
||||||
|
|
||||||
|
if (insuranceId) {
|
||||||
|
await createOrUpdatePatientByInsuranceId({
|
||||||
|
insuranceId,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
dob: insuranceEligibilityData.dateOfBirth,
|
||||||
|
userId: job.userId,
|
||||||
|
eligibilityStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let patient = insuranceId
|
||||||
|
? await storage.getPatientByInsuranceId(insuranceId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!patient?.id && firstName && lastName) {
|
||||||
|
const patients = await storage.getAllPatients(job.userId);
|
||||||
|
patient = patients.find(
|
||||||
|
(p) =>
|
||||||
|
p.firstName?.toLowerCase() === firstName.toLowerCase() &&
|
||||||
|
p.lastName?.toLowerCase() === lastName.toLowerCase()
|
||||||
|
) ?? null;
|
||||||
|
if (patient) {
|
||||||
|
console.log(`[deltains-eligibility] Found patient by name: ${patient.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patient && firstName && lastName) {
|
||||||
|
console.log(`[deltains-eligibility] Creating new patient: ${firstName} ${lastName}`);
|
||||||
|
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,
|
||||||
|
lastName,
|
||||||
|
dateOfBirth: parsedDob || new Date(),
|
||||||
|
insuranceId: insuranceId || undefined,
|
||||||
|
insuranceProvider: "Delta Dental Ins",
|
||||||
|
gender: "Unknown",
|
||||||
|
phone: "",
|
||||||
|
userId: job.userId,
|
||||||
|
status: eligibilityStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
const validation = insertPatientSchema.safeParse(newPatientData);
|
||||||
|
if (validation.success) {
|
||||||
|
patient = await storage.createPatient(validation.data);
|
||||||
|
console.log(`[deltains-eligibility] Created new patient: ${patient.id}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[deltains-eligibility] Patient validation failed: ${validation.error.message}`);
|
||||||
|
}
|
||||||
|
} catch (createErr: any) {
|
||||||
|
console.log(`[deltains-eligibility] Failed to create patient: ${createErr.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!patient?.id) {
|
||||||
|
outputResult.patientUpdateStatus =
|
||||||
|
"Patient not found and could not be created; no update performed";
|
||||||
|
return {
|
||||||
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
|
pdfUploadStatus: "none",
|
||||||
|
pdfFileId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePayload: Record<string, any> = { status: eligibilityStatus };
|
||||||
|
if (firstName && (!patient.firstName || patient.firstName.trim() === "")) {
|
||||||
|
updatePayload.firstName = firstName;
|
||||||
|
}
|
||||||
|
if (lastName && (!patient.lastName || patient.lastName.trim() === "")) {
|
||||||
|
updatePayload.lastName = lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
await storage.updatePatient(patient.id, updatePayload);
|
||||||
|
outputResult.patientUpdateStatus = `Patient ${patient.id} updated: status=${eligibilityStatus}, name=${firstName} ${lastName}`;
|
||||||
|
console.log(`[deltains-eligibility] ${outputResult.patientUpdateStatus}`);
|
||||||
|
|
||||||
|
// Handle PDF
|
||||||
|
let pdfBuffer: Buffer | null = null;
|
||||||
|
|
||||||
|
// Check for base64 PDF from CDP command
|
||||||
|
if (seleniumResult?.pdfBase64 && typeof seleniumResult.pdfBase64 === "string" && seleniumResult.pdfBase64.length > 100) {
|
||||||
|
try {
|
||||||
|
pdfBuffer = Buffer.from(seleniumResult.pdfBase64, "base64");
|
||||||
|
const pdfFileName = `deltains_eligibility_${insuranceId || "unknown"}_${Date.now()}.pdf`;
|
||||||
|
const downloadDir = path.join(process.cwd(), "seleniumDownloads");
|
||||||
|
if (!fsSync.existsSync(downloadDir)) {
|
||||||
|
fsSync.mkdirSync(downloadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
generatedPdfPath = path.join(downloadDir, pdfFileName);
|
||||||
|
await fs.writeFile(generatedPdfPath, pdfBuffer);
|
||||||
|
console.log(`[deltains-eligibility] PDF saved from base64: ${generatedPdfPath}`);
|
||||||
|
} catch (pdfErr: any) {
|
||||||
|
console.error(`[deltains-eligibility] Failed to save base64 PDF: ${pdfErr.message}`);
|
||||||
|
pdfBuffer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check for file path from selenium
|
||||||
|
if (!pdfBuffer && seleniumResult?.ss_path && typeof seleniumResult.ss_path === "string") {
|
||||||
|
try {
|
||||||
|
if (!fsSync.existsSync(seleniumResult.ss_path)) {
|
||||||
|
throw new Error(`File not found: ${seleniumResult.ss_path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seleniumResult.ss_path.endsWith(".pdf")) {
|
||||||
|
pdfBuffer = await fs.readFile(seleniumResult.ss_path);
|
||||||
|
generatedPdfPath = seleniumResult.ss_path;
|
||||||
|
seleniumResult.pdf_path = generatedPdfPath;
|
||||||
|
} else if (
|
||||||
|
seleniumResult.ss_path.endsWith(".png") ||
|
||||||
|
seleniumResult.ss_path.endsWith(".jpg") ||
|
||||||
|
seleniumResult.ss_path.endsWith(".jpeg")
|
||||||
|
) {
|
||||||
|
pdfBuffer = await imageToPdfBuffer(seleniumResult.ss_path);
|
||||||
|
const pdfFileName = `deltains_eligibility_${insuranceId || "unknown"}_${Date.now()}.pdf`;
|
||||||
|
generatedPdfPath = path.join(
|
||||||
|
path.dirname(seleniumResult.ss_path),
|
||||||
|
pdfFileName
|
||||||
|
);
|
||||||
|
await fs.writeFile(generatedPdfPath, pdfBuffer);
|
||||||
|
seleniumResult.pdf_path = generatedPdfPath;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("[deltains-eligibility] Failed to process PDF/screenshot:", err);
|
||||||
|
outputResult.pdfUploadStatus = `Failed to process file: ${String(err)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pdfBuffer && generatedPdfPath) {
|
||||||
|
const groupTitle = "Eligibility Status";
|
||||||
|
const groupTitleKey = "ELIGIBILITY_STATUS";
|
||||||
|
|
||||||
|
let group = await storage.findPdfGroupByPatientTitleKey(
|
||||||
|
patient.id,
|
||||||
|
groupTitleKey
|
||||||
|
);
|
||||||
|
if (!group) {
|
||||||
|
group = await storage.createPdfGroup(
|
||||||
|
patient.id,
|
||||||
|
groupTitle,
|
||||||
|
groupTitleKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!group?.id) {
|
||||||
|
throw new Error("PDF group creation failed: missing group ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await storage.createPdfFile(
|
||||||
|
group.id,
|
||||||
|
path.basename(generatedPdfPath),
|
||||||
|
pdfBuffer
|
||||||
|
);
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
createdPdfFileId = Number(created.id);
|
||||||
|
}
|
||||||
|
outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
||||||
|
} else if (!outputResult.pdfUploadStatus) {
|
||||||
|
outputResult.pdfUploadStatus = "No PDF available from Selenium";
|
||||||
|
}
|
||||||
|
|
||||||
|
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
|
pdfUploadStatus: outputResult.pdfUploadStatus,
|
||||||
|
pdfFileId: createdPdfFileId,
|
||||||
|
pdfFilename,
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
const pdfFilename = generatedPdfPath ? path.basename(generatedPdfPath) : null;
|
||||||
|
return {
|
||||||
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
|
pdfUploadStatus:
|
||||||
|
outputResult.pdfUploadStatus ??
|
||||||
|
`Failed to process DeltaIns job: ${err?.message ?? String(err)}`,
|
||||||
|
pdfFileId: createdPdfFileId,
|
||||||
|
pdfFilename,
|
||||||
|
error: err?.message ?? String(err),
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
if (seleniumResult && seleniumResult.pdf_path) {
|
||||||
|
await emptyFolderContainingFile(seleniumResult.pdf_path);
|
||||||
|
} else if (seleniumResult && seleniumResult.ss_path) {
|
||||||
|
await emptyFolderContainingFile(seleniumResult.ss_path);
|
||||||
|
}
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
console.error(
|
||||||
|
`[deltains-eligibility cleanup failed]`,
|
||||||
|
cleanupErr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentFinalSessionId: string | null = null;
|
||||||
|
let currentFinalResult: any = null;
|
||||||
|
|
||||||
|
function now() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
function log(tag: string, msg: string, ctx?: any) {
|
||||||
|
console.log(`${now()} [${tag}] ${msg}`, ctx ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSafe(socketId: string | undefined, event: string, payload: any) {
|
||||||
|
if (!socketId) {
|
||||||
|
log("socket", "no socketId for emit", { event });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const socket = io?.sockets.sockets.get(socketId);
|
||||||
|
if (!socket) {
|
||||||
|
log("socket", "socket not found (maybe disconnected)", {
|
||||||
|
socketId,
|
||||||
|
event,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit(event, payload);
|
||||||
|
log("socket", "emitted", { socketId, event });
|
||||||
|
} catch (err: any) {
|
||||||
|
log("socket", "emit failed", { socketId, event, err: err?.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollAgentSessionAndProcess(
|
||||||
|
sessionId: string,
|
||||||
|
socketId?: string,
|
||||||
|
pollTimeoutMs = 8 * 60 * 1000
|
||||||
|
) {
|
||||||
|
const maxAttempts = 500;
|
||||||
|
const baseDelayMs = 1000;
|
||||||
|
const maxTransientErrors = 12;
|
||||||
|
const noProgressLimit = 200;
|
||||||
|
|
||||||
|
const job = deltainsJobs[sessionId];
|
||||||
|
let transientErrorCount = 0;
|
||||||
|
let consecutiveNoProgress = 0;
|
||||||
|
let lastStatus: string | null = null;
|
||||||
|
const deadline = Date.now() + pollTimeoutMs;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
|
if (Date.now() > deadline) {
|
||||||
|
emitSafe(socketId, "selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "error",
|
||||||
|
message: `Polling timeout reached (${Math.round(pollTimeoutMs / 1000)}s).`,
|
||||||
|
});
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
"poller-deltains",
|
||||||
|
`attempt=${attempt} session=${sessionId} transientErrCount=${transientErrorCount}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const st = await getSeleniumDeltaInsSessionStatus(sessionId);
|
||||||
|
const status = st?.status ?? null;
|
||||||
|
log("poller-deltains", "got status", {
|
||||||
|
sessionId,
|
||||||
|
status,
|
||||||
|
message: st?.message,
|
||||||
|
resultKeys: st?.result ? Object.keys(st.result) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
transientErrorCount = 0;
|
||||||
|
|
||||||
|
const isTerminalLike =
|
||||||
|
status === "completed" || status === "error" || status === "not_found";
|
||||||
|
if (status === lastStatus && !isTerminalLike) {
|
||||||
|
consecutiveNoProgress++;
|
||||||
|
} else {
|
||||||
|
consecutiveNoProgress = 0;
|
||||||
|
}
|
||||||
|
lastStatus = status;
|
||||||
|
|
||||||
|
if (consecutiveNoProgress >= noProgressLimit) {
|
||||||
|
emitSafe(socketId, "selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "error",
|
||||||
|
message: `No progress from selenium agent (status="${status}") after ${consecutiveNoProgress} polls; aborting.`,
|
||||||
|
});
|
||||||
|
emitSafe(socketId, "selenium:session_error", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "error",
|
||||||
|
message: "No progress from selenium agent",
|
||||||
|
});
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSafe(socketId, "selenium:debug", {
|
||||||
|
session_id: sessionId,
|
||||||
|
attempt,
|
||||||
|
status,
|
||||||
|
serverTime: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === "waiting_for_otp") {
|
||||||
|
emitSafe(socketId, "selenium:otp_required", {
|
||||||
|
session_id: sessionId,
|
||||||
|
message: "OTP required. Please enter the code sent to your email.",
|
||||||
|
});
|
||||||
|
await new Promise((r) => setTimeout(r, baseDelayMs));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "completed") {
|
||||||
|
log("poller-deltains", "agent completed; processing result", {
|
||||||
|
sessionId,
|
||||||
|
resultKeys: st.result ? Object.keys(st.result) : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
currentFinalSessionId = sessionId;
|
||||||
|
currentFinalResult = {
|
||||||
|
rawSelenium: st.result,
|
||||||
|
processedAt: null,
|
||||||
|
final: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
let finalResult: any = null;
|
||||||
|
if (job && st.result) {
|
||||||
|
try {
|
||||||
|
finalResult = await handleDeltaInsCompletedJob(
|
||||||
|
sessionId,
|
||||||
|
job,
|
||||||
|
st.result
|
||||||
|
);
|
||||||
|
currentFinalResult.final = finalResult;
|
||||||
|
currentFinalResult.processedAt = Date.now();
|
||||||
|
} catch (err: any) {
|
||||||
|
currentFinalResult.final = {
|
||||||
|
error: "processing_failed",
|
||||||
|
detail: err?.message ?? String(err),
|
||||||
|
};
|
||||||
|
currentFinalResult.processedAt = Date.now();
|
||||||
|
log("poller-deltains", "handleDeltaInsCompletedJob failed", {
|
||||||
|
sessionId,
|
||||||
|
err: err?.message ?? err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentFinalResult.final = {
|
||||||
|
error: "no_job_or_no_result",
|
||||||
|
};
|
||||||
|
currentFinalResult.processedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSafe(socketId, "selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "completed",
|
||||||
|
rawSelenium: st.result,
|
||||||
|
final: currentFinalResult.final,
|
||||||
|
});
|
||||||
|
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "error" || status === "not_found") {
|
||||||
|
const emitPayload = {
|
||||||
|
session_id: sessionId,
|
||||||
|
status,
|
||||||
|
message: st?.message || "Selenium session error",
|
||||||
|
};
|
||||||
|
emitSafe(socketId, "selenium:session_update", emitPayload);
|
||||||
|
emitSafe(socketId, "selenium:session_error", emitPayload);
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
const axiosStatus =
|
||||||
|
err?.response?.status ?? (err?.status ? Number(err.status) : undefined);
|
||||||
|
const errCode = err?.code ?? err?.errno;
|
||||||
|
const errMsg = err?.message ?? String(err);
|
||||||
|
const errData = err?.response?.data ?? null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
axiosStatus === 404 ||
|
||||||
|
(typeof errMsg === "string" && errMsg.includes("not_found"))
|
||||||
|
) {
|
||||||
|
console.warn(
|
||||||
|
`${new Date().toISOString()} [poller-deltains] terminal 404/not_found for ${sessionId}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const emitPayload = {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "not_found",
|
||||||
|
message:
|
||||||
|
errData?.detail || "Selenium session not found (agent cleaned up).",
|
||||||
|
};
|
||||||
|
emitSafe(socketId, "selenium:session_update", emitPayload);
|
||||||
|
emitSafe(socketId, "selenium:session_error", emitPayload);
|
||||||
|
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transientErrorCount++;
|
||||||
|
if (transientErrorCount > maxTransientErrors) {
|
||||||
|
const emitPayload = {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "error",
|
||||||
|
message:
|
||||||
|
"Repeated network errors while polling selenium agent; giving up.",
|
||||||
|
};
|
||||||
|
emitSafe(socketId, "selenium:session_update", emitPayload);
|
||||||
|
emitSafe(socketId, "selenium:session_error", emitPayload);
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backoffMs = Math.min(
|
||||||
|
30_000,
|
||||||
|
baseDelayMs * Math.pow(2, transientErrorCount - 1)
|
||||||
|
);
|
||||||
|
console.warn(
|
||||||
|
`${new Date().toISOString()} [poller-deltains] transient error (#${transientErrorCount}) for ${sessionId}: code=${errCode} status=${axiosStatus} msg=${errMsg}`
|
||||||
|
);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, backoffMs));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, baseDelayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
emitSafe(socketId, "selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "error",
|
||||||
|
message: "Polling timeout while waiting for selenium session",
|
||||||
|
});
|
||||||
|
delete deltainsJobs[sessionId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /deltains-eligibility
|
||||||
|
* Starts DeltaIns eligibility Selenium job.
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/deltains-eligibility",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
if (!req.body.data) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "Missing Insurance Eligibility data for selenium" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.user || !req.user.id) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized: user info missing" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rawData =
|
||||||
|
typeof req.body.data === "string"
|
||||||
|
? JSON.parse(req.body.data)
|
||||||
|
: req.body.data;
|
||||||
|
|
||||||
|
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(
|
||||||
|
req.user.id,
|
||||||
|
"DELTAINS"
|
||||||
|
);
|
||||||
|
if (!credentials) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error:
|
||||||
|
"No insurance credentials found for this provider, Kindly Update this at Settings Page.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedData = {
|
||||||
|
...rawData,
|
||||||
|
deltains_username: credentials.username,
|
||||||
|
deltains_password: credentials.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const socketId: string | undefined = req.body.socketId;
|
||||||
|
|
||||||
|
const agentResp =
|
||||||
|
await forwardToSeleniumDeltaInsEligibilityAgent(enrichedData);
|
||||||
|
|
||||||
|
if (
|
||||||
|
!agentResp ||
|
||||||
|
agentResp.status !== "started" ||
|
||||||
|
!agentResp.session_id
|
||||||
|
) {
|
||||||
|
return res.status(502).json({
|
||||||
|
error: "Selenium agent did not return a started session",
|
||||||
|
detail: agentResp,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = agentResp.session_id as string;
|
||||||
|
|
||||||
|
deltainsJobs[sessionId] = {
|
||||||
|
userId: req.user.id,
|
||||||
|
insuranceEligibilityData: enrichedData,
|
||||||
|
socketId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pollAgentSessionAndProcess(sessionId, socketId).catch((e) =>
|
||||||
|
console.warn("pollAgentSessionAndProcess (deltains) failed", e)
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({ status: "started", session_id: sessionId });
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: err.message || "Failed to start DeltaIns selenium agent",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /selenium/submit-otp
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/selenium/submit-otp",
|
||||||
|
async (req: Request, res: Response): Promise<any> => {
|
||||||
|
const { session_id: sessionId, otp, socketId } = req.body;
|
||||||
|
if (!sessionId || !otp) {
|
||||||
|
return res.status(400).json({ error: "session_id and otp are required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const r = await forwardOtpToSeleniumDeltaInsAgent(sessionId, otp);
|
||||||
|
|
||||||
|
emitSafe(socketId, "selenium:otp_submitted", {
|
||||||
|
session_id: sessionId,
|
||||||
|
result: r,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json(r);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(
|
||||||
|
"Failed to forward OTP:",
|
||||||
|
err?.response?.data || err?.message || err
|
||||||
|
);
|
||||||
|
return res.status(500).json({
|
||||||
|
error: "Failed to forward otp to selenium agent",
|
||||||
|
detail: err?.message || err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// GET /selenium/session/:sid/final
|
||||||
|
router.get(
|
||||||
|
"/selenium/session/:sid/final",
|
||||||
|
async (req: Request, res: Response) => {
|
||||||
|
const sid = req.params.sid;
|
||||||
|
if (!sid) return res.status(400).json({ error: "session id required" });
|
||||||
|
|
||||||
|
if (currentFinalSessionId !== sid || !currentFinalResult) {
|
||||||
|
return res.status(404).json({ error: "final result not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(currentFinalResult);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import http from "http";
|
||||||
|
import https from "https";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export interface SeleniumPayload {
|
||||||
|
data: any;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SELENIUM_AGENT_BASE = process.env.SELENIUM_AGENT_BASE_URL;
|
||||||
|
|
||||||
|
const httpAgent = new http.Agent({ keepAlive: true, keepAliveMsecs: 60_000 });
|
||||||
|
const httpsAgent = new https.Agent({ keepAlive: true, keepAliveMsecs: 60_000 });
|
||||||
|
|
||||||
|
const client = axios.create({
|
||||||
|
baseURL: SELENIUM_AGENT_BASE,
|
||||||
|
timeout: 5 * 60 * 1000,
|
||||||
|
httpAgent,
|
||||||
|
httpsAgent,
|
||||||
|
validateStatus: (s) => s >= 200 && s < 600,
|
||||||
|
});
|
||||||
|
|
||||||
|
async function requestWithRetries(
|
||||||
|
config: any,
|
||||||
|
retries = 4,
|
||||||
|
baseBackoffMs = 300
|
||||||
|
) {
|
||||||
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||||
|
try {
|
||||||
|
const r = await client.request(config);
|
||||||
|
if (![502, 503, 504].includes(r.status)) return r;
|
||||||
|
console.warn(
|
||||||
|
`[selenium-deltains-client] retryable HTTP status ${r.status} (attempt ${attempt})`
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
const code = err?.code;
|
||||||
|
const isTransient =
|
||||||
|
code === "ECONNRESET" ||
|
||||||
|
code === "ECONNREFUSED" ||
|
||||||
|
code === "EPIPE" ||
|
||||||
|
code === "ETIMEDOUT";
|
||||||
|
if (!isTransient) throw err;
|
||||||
|
console.warn(
|
||||||
|
`[selenium-deltains-client] transient network error ${code} (attempt ${attempt})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await new Promise((r) => setTimeout(r, baseBackoffMs * attempt));
|
||||||
|
}
|
||||||
|
return client.request(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function now() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
function log(tag: string, msg: string, ctx?: any) {
|
||||||
|
console.log(`${now()} [${tag}] ${msg}`, ctx ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function forwardToSeleniumDeltaInsEligibilityAgent(
|
||||||
|
insuranceEligibilityData: any
|
||||||
|
): Promise<any> {
|
||||||
|
const payload = { data: insuranceEligibilityData };
|
||||||
|
const url = `/deltains-eligibility`;
|
||||||
|
log("selenium-deltains-client", "POST deltains-eligibility", {
|
||||||
|
url: SELENIUM_AGENT_BASE + url,
|
||||||
|
keys: Object.keys(payload),
|
||||||
|
});
|
||||||
|
const r = await requestWithRetries({ url, method: "POST", data: payload }, 4);
|
||||||
|
log("selenium-deltains-client", "agent response", {
|
||||||
|
status: r.status,
|
||||||
|
dataKeys: r.data ? Object.keys(r.data) : null,
|
||||||
|
});
|
||||||
|
if (r.status >= 500)
|
||||||
|
throw new Error(`Selenium agent server error: ${r.status}`);
|
||||||
|
return r.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function forwardOtpToSeleniumDeltaInsAgent(
|
||||||
|
sessionId: string,
|
||||||
|
otp: string
|
||||||
|
): Promise<any> {
|
||||||
|
const url = `/deltains-submit-otp`;
|
||||||
|
log("selenium-deltains-client", "POST deltains-submit-otp", {
|
||||||
|
url: SELENIUM_AGENT_BASE + url,
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const r = await requestWithRetries(
|
||||||
|
{ url, method: "POST", data: { session_id: sessionId, otp } },
|
||||||
|
4
|
||||||
|
);
|
||||||
|
log("selenium-deltains-client", "submit-otp response", {
|
||||||
|
status: r.status,
|
||||||
|
data: r.data,
|
||||||
|
});
|
||||||
|
if (r.status >= 500)
|
||||||
|
throw new Error(`Selenium agent server error on submit-otp: ${r.status}`);
|
||||||
|
return r.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSeleniumDeltaInsSessionStatus(
|
||||||
|
sessionId: string
|
||||||
|
): Promise<any> {
|
||||||
|
const url = `/deltains-session/${sessionId}/status`;
|
||||||
|
log("selenium-deltains-client", "GET session status", {
|
||||||
|
url: SELENIUM_AGENT_BASE + url,
|
||||||
|
sessionId,
|
||||||
|
});
|
||||||
|
const r = await requestWithRetries({ url, method: "GET" }, 4);
|
||||||
|
log("selenium-deltains-client", "session status response", {
|
||||||
|
status: r.status,
|
||||||
|
dataKeys: r.data ? Object.keys(r.data) : null,
|
||||||
|
});
|
||||||
|
if (r.status === 404) {
|
||||||
|
const e: any = new Error("not_found");
|
||||||
|
e.response = { status: 404, data: r.data };
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
return r.data;
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=3000
|
PORT=3000
|
||||||
VITE_API_BASE_URL_BACKEND=http://localhost:5000
|
# VITE_API_BASE_URL_BACKEND=http://localhost:5000
|
||||||
|
VITE_API_BASE_URL_BACKEND=
|
||||||
|
|||||||
@@ -0,0 +1,551 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { io as ioClient, Socket } from "socket.io-client";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { CheckCircle, LoaderCircleIcon, X } from "lucide-react";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { apiRequest, queryClient } from "@/lib/queryClient";
|
||||||
|
import { useAppDispatch } from "@/redux/hooks";
|
||||||
|
import { setTaskStatus } from "@/redux/slices/seleniumEligibilityCheckTaskSlice";
|
||||||
|
import { formatLocalDate } from "@/utils/dateUtils";
|
||||||
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||||
|
|
||||||
|
const SOCKET_URL =
|
||||||
|
import.meta.env.VITE_API_BASE_URL_BACKEND ||
|
||||||
|
(typeof window !== "undefined" ? window.location.origin : "");
|
||||||
|
|
||||||
|
// ---------- OTP Modal component ----------
|
||||||
|
interface DeltaInsOtpModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (otp: string) => Promise<void> | void;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DeltaInsOtpModal({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
isSubmitting,
|
||||||
|
}: DeltaInsOtpModalProps) {
|
||||||
|
const [otp, setOtp] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) setOtp("");
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!otp.trim()) return;
|
||||||
|
await onSubmit(otp.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg w-full max-w-md p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-semibold">Enter OTP</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-slate-500 hover:text-slate-800"
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-500 mb-4">
|
||||||
|
We need the one-time password (OTP) sent to your email by Delta Dental
|
||||||
|
Ins to complete this eligibility check.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="deltains-otp">OTP</Label>
|
||||||
|
<Input
|
||||||
|
id="deltains-otp"
|
||||||
|
placeholder="Enter OTP code"
|
||||||
|
value={otp}
|
||||||
|
onChange={(e) => setOtp(e.target.value)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isSubmitting || !otp.trim()}>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<LoaderCircleIcon className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Submitting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Submit OTP"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Main DeltaIns Eligibility button component ----------
|
||||||
|
interface DeltaInsEligibilityButtonProps {
|
||||||
|
memberId: string;
|
||||||
|
dateOfBirth: Date | null;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
isFormIncomplete: boolean;
|
||||||
|
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeltaInsEligibilityButton({
|
||||||
|
memberId,
|
||||||
|
dateOfBirth,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
isFormIncomplete,
|
||||||
|
onPdfReady,
|
||||||
|
}: DeltaInsEligibilityButtonProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const isDeltaInsFormIncomplete =
|
||||||
|
!dateOfBirth || (!memberId && !firstName && !lastName);
|
||||||
|
|
||||||
|
const socketRef = useRef<Socket | null>(null);
|
||||||
|
const connectingRef = useRef<Promise<void> | null>(null);
|
||||||
|
|
||||||
|
const [sessionId, setSessionId] = useState<string | null>(null);
|
||||||
|
const [otpModalOpen, setOtpModalOpen] = useState(false);
|
||||||
|
const [isStarting, setIsStarting] = useState(false);
|
||||||
|
const [isSubmittingOtp, setIsSubmittingOtp] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.removeAllListeners();
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
|
connectingRef.current = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeSocket = () => {
|
||||||
|
try {
|
||||||
|
socketRef.current?.removeAllListeners();
|
||||||
|
socketRef.current?.disconnect();
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
} finally {
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureSocketConnected = async () => {
|
||||||
|
if (socketRef.current && socketRef.current.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectingRef.current) {
|
||||||
|
return connectingRef.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = new Promise<void>((resolve, reject) => {
|
||||||
|
const socket = ioClient(SOCKET_URL, {
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
socketRef.current = socket;
|
||||||
|
|
||||||
|
socket.on("connect", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (err: any) => {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: "Connection failed",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Realtime connection failed",
|
||||||
|
description:
|
||||||
|
"Could not connect to realtime server. Retrying automatically...",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("reconnect_attempt", (attempt: number) => {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: `Realtime reconnect attempt #${attempt}`,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("reconnect_failed", () => {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: "Reconnect failed",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Realtime reconnect failed",
|
||||||
|
description:
|
||||||
|
"Connection to realtime server could not be re-established. Please try again later.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
closeSocket();
|
||||||
|
reject(new Error("Realtime reconnect failed"));
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", (reason: any) => {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: "Connection disconnected",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Connection Disconnected",
|
||||||
|
description:
|
||||||
|
"Connection to the server was lost. If a DeltaIns job was running it may have failed.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setSessionId(null);
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OTP required
|
||||||
|
socket.on("selenium:otp_required", (payload: any) => {
|
||||||
|
if (!payload?.session_id) return;
|
||||||
|
setSessionId(payload.session_id);
|
||||||
|
setOtpModalOpen(true);
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "OTP required for Delta Dental Ins eligibility. Please enter the code sent to your email.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OTP submitted
|
||||||
|
socket.on("selenium:otp_submitted", (payload: any) => {
|
||||||
|
if (!payload?.session_id) return;
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "OTP submitted. Finishing Delta Dental Ins eligibility check...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session update
|
||||||
|
socket.on("selenium:session_update", (payload: any) => {
|
||||||
|
const { session_id, status, final } = payload || {};
|
||||||
|
if (!session_id) return;
|
||||||
|
|
||||||
|
if (status === "completed") {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "success",
|
||||||
|
message:
|
||||||
|
"Delta Dental Ins eligibility updated and PDF attached to patient documents.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Delta Dental Ins eligibility complete",
|
||||||
|
description:
|
||||||
|
"Patient status was updated and the eligibility PDF was saved.",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfId = final?.pdfFileId;
|
||||||
|
if (pdfId) {
|
||||||
|
const filename =
|
||||||
|
final?.pdfFilename ?? `eligibility_deltains_${memberId}.pdf`;
|
||||||
|
onPdfReady(Number(pdfId), filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionId(null);
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
} else if (status === "error") {
|
||||||
|
const msg =
|
||||||
|
payload?.message ||
|
||||||
|
final?.error ||
|
||||||
|
"Delta Dental Ins eligibility session failed.";
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: msg,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Delta Dental Ins selenium error",
|
||||||
|
description: msg,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
closeSocket();
|
||||||
|
} catch (e) {}
|
||||||
|
setSessionId(null);
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
|
});
|
||||||
|
|
||||||
|
// explicit session error event
|
||||||
|
socket.on("selenium:session_error", (payload: any) => {
|
||||||
|
const msg = payload?.message || "Selenium session error";
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: msg,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: "Selenium session error",
|
||||||
|
description: msg,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
closeSocket();
|
||||||
|
} catch (e) {}
|
||||||
|
setSessionId(null);
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialConnectTimeout = setTimeout(() => {
|
||||||
|
if (!socket.connected) {
|
||||||
|
closeSocket();
|
||||||
|
reject(new Error("Realtime initial connection timeout"));
|
||||||
|
}
|
||||||
|
}, 8000);
|
||||||
|
|
||||||
|
socket.once("connect", () => {
|
||||||
|
clearTimeout(initialConnectTimeout);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connectingRef.current = promise;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
} finally {
|
||||||
|
connectingRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDeltaInsEligibility = async () => {
|
||||||
|
if (!dateOfBirth) {
|
||||||
|
toast({
|
||||||
|
title: "Missing fields",
|
||||||
|
description: "Date of Birth is required for Delta Dental Ins eligibility.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!memberId && !firstName && !lastName) {
|
||||||
|
toast({
|
||||||
|
title: "Missing fields",
|
||||||
|
description: "Member ID, First Name, or Last Name is required for Delta Dental Ins eligibility.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
memberId: memberId || "",
|
||||||
|
dateOfBirth: formattedDob,
|
||||||
|
firstName: firstName || "",
|
||||||
|
lastName: lastName || "",
|
||||||
|
insuranceSiteKey: "DELTAINS",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsStarting(true);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "Opening realtime channel for Delta Dental Ins eligibility...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await ensureSocketConnected();
|
||||||
|
|
||||||
|
const socket = socketRef.current;
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
throw new Error("Socket connection failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketId = socket.id;
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "Starting Delta Dental Ins eligibility check via selenium...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/insurance-status-deltains/deltains-eligibility",
|
||||||
|
{
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
socketId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let result: any = null;
|
||||||
|
let backendError: string | null = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await response.clone().json();
|
||||||
|
backendError =
|
||||||
|
result?.error || result?.message || result?.detail || null;
|
||||||
|
} catch {
|
||||||
|
try {
|
||||||
|
const text = await response.clone().text();
|
||||||
|
backendError = text?.trim() || null;
|
||||||
|
} catch {
|
||||||
|
backendError = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
backendError ||
|
||||||
|
`Delta Dental Ins selenium start failed (status ${response.status})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
throw new Error(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === "started" && result.session_id) {
|
||||||
|
setSessionId(result.session_id as string);
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message:
|
||||||
|
"Delta Dental Ins eligibility job started. Waiting for OTP or final result...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "success",
|
||||||
|
message: "Delta Dental Ins eligibility completed.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("startDeltaInsEligibility error:", err);
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: err?.message || "Failed to start Delta Dental Ins eligibility",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "Delta Dental Ins selenium error",
|
||||||
|
description: err?.message || "Failed to start Delta Dental Ins eligibility",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsStarting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitOtp = async (otp: string) => {
|
||||||
|
if (!sessionId || !socketRef.current || !socketRef.current.connected) {
|
||||||
|
toast({
|
||||||
|
title: "Session not ready",
|
||||||
|
description:
|
||||||
|
"Could not submit OTP because the DeltaIns session or socket is not ready.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmittingOtp(true);
|
||||||
|
const resp = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/insurance-status-deltains/selenium/submit-otp",
|
||||||
|
{
|
||||||
|
session_id: sessionId,
|
||||||
|
otp,
|
||||||
|
socketId: socketRef.current.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const data = await resp.json();
|
||||||
|
if (!resp.ok || data.error) {
|
||||||
|
throw new Error(data.error || "Failed to submit OTP");
|
||||||
|
}
|
||||||
|
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("handleSubmitOtp error:", err);
|
||||||
|
toast({
|
||||||
|
title: "Failed to submit OTP",
|
||||||
|
description: err?.message || "Error forwarding OTP to selenium agent",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingOtp(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isDeltaInsFormIncomplete || isStarting}
|
||||||
|
onClick={startDeltaInsEligibility}
|
||||||
|
>
|
||||||
|
{isStarting ? (
|
||||||
|
<>
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
|
Delta Dental Ins
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DeltaInsOtpModal
|
||||||
|
open={otpModalOpen}
|
||||||
|
onClose={() => setOtpModalOpen(false)}
|
||||||
|
onSubmit={handleSubmitOtp}
|
||||||
|
isSubmitting={isSubmittingOtp}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ type CredentialFormProps = {
|
|||||||
const SITE_KEY_OPTIONS = [
|
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: "DELTAINS", label: "Delta Dental Ins" },
|
||||||
{ value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" },
|
{ value: "DENTAQUEST", label: "Tufts SCO / DentaQuest" },
|
||||||
{ value: "UNITEDSCO", label: "United SCO" },
|
{ value: "UNITEDSCO", label: "United SCO" },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Credential = {
|
|||||||
const SITE_KEY_LABELS: Record<string, string> = {
|
const SITE_KEY_LABELS: Record<string, string> = {
|
||||||
MH: "MassHealth",
|
MH: "MassHealth",
|
||||||
DDMA: "Delta Dental MA",
|
DDMA: "Delta Dental MA",
|
||||||
|
DELTAINS: "Delta Dental Ins",
|
||||||
DENTAQUEST: "Tufts SCO / DentaQuest",
|
DENTAQUEST: "Tufts SCO / DentaQuest",
|
||||||
UNITEDSCO: "United SCO",
|
UNITEDSCO: "United SCO",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { useLocation } from "wouter";
|
|||||||
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
||||||
import { DentaQuestEligibilityButton } from "@/components/insurance-status/dentaquest-button-modal";
|
import { DentaQuestEligibilityButton } from "@/components/insurance-status/dentaquest-button-modal";
|
||||||
import { UnitedSCOEligibilityButton } from "@/components/insurance-status/unitedsco-button-modal";
|
import { UnitedSCOEligibilityButton } from "@/components/insurance-status/unitedsco-button-modal";
|
||||||
|
import { DeltaInsEligibilityButton } from "@/components/insurance-status/deltains-button-modal";
|
||||||
|
|
||||||
export default function InsuranceStatusPage() {
|
export default function InsuranceStatusPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -597,14 +598,20 @@ export default function InsuranceStatusPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<DeltaInsEligibilityButton
|
||||||
className="w-full"
|
memberId={memberId}
|
||||||
variant="outline"
|
dateOfBirth={dateOfBirth}
|
||||||
disabled={isFormIncomplete}
|
firstName={firstName}
|
||||||
>
|
lastName={lastName}
|
||||||
<CheckCircle className="h-4 w-4 mr-2" />
|
isFormIncomplete={isFormIncomplete}
|
||||||
Metlife Dental
|
onPdfReady={(pdfId, fallbackFilename) => {
|
||||||
</Button>
|
setPreviewPdfId(pdfId);
|
||||||
|
setPreviewFallbackFilename(
|
||||||
|
fallbackFilename ?? `eligibility_deltains_${memberId}.pdf`
|
||||||
|
);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ export default defineConfig(({ mode }) => {
|
|||||||
server: {
|
server: {
|
||||||
host: env.HOST,
|
host: env.HOST,
|
||||||
port: Number(env.PORT),
|
port: Number(env.PORT),
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000",
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
"/socket.io": {
|
||||||
|
target: env.VITE_API_BASE_URL_BACKEND || "http://localhost:5000",
|
||||||
|
changeOrigin: true,
|
||||||
|
ws: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import time
|
|||||||
import helpers_ddma_eligibility as hddma
|
import helpers_ddma_eligibility as hddma
|
||||||
import helpers_dentaquest_eligibility as hdentaquest
|
import helpers_dentaquest_eligibility as hdentaquest
|
||||||
import helpers_unitedsco_eligibility as hunitedsco
|
import helpers_unitedsco_eligibility as hunitedsco
|
||||||
|
import helpers_deltains_eligibility as hdeltains
|
||||||
|
|
||||||
# Import session clear functions for startup
|
# Import session clear functions for startup
|
||||||
from ddma_browser_manager import clear_ddma_session_on_startup
|
from ddma_browser_manager import clear_ddma_session_on_startup
|
||||||
from dentaquest_browser_manager import clear_dentaquest_session_on_startup
|
from dentaquest_browser_manager import clear_dentaquest_session_on_startup
|
||||||
from unitedsco_browser_manager import clear_unitedsco_session_on_startup
|
from unitedsco_browser_manager import clear_unitedsco_session_on_startup
|
||||||
|
from deltains_browser_manager import clear_deltains_session_on_startup
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -28,6 +30,7 @@ print("=" * 50)
|
|||||||
clear_ddma_session_on_startup()
|
clear_ddma_session_on_startup()
|
||||||
clear_dentaquest_session_on_startup()
|
clear_dentaquest_session_on_startup()
|
||||||
clear_unitedsco_session_on_startup()
|
clear_unitedsco_session_on_startup()
|
||||||
|
clear_deltains_session_on_startup()
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print("SESSION CLEAR COMPLETE - FRESH LOGINS REQUIRED")
|
print("SESSION CLEAR COMPLETE - FRESH LOGINS REQUIRED")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
@@ -351,6 +354,77 @@ async def unitedsco_session_status(sid: str):
|
|||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# Endpoint:8 - DeltaIns eligibility (background, OTP)
|
||||||
|
|
||||||
|
async def _deltains_worker_wrapper(sid: str, data: dict, url: str):
|
||||||
|
"""
|
||||||
|
Background worker that:
|
||||||
|
- acquires semaphore (to keep 1 selenium at a time),
|
||||||
|
- updates active/queued counters,
|
||||||
|
- runs the DeltaIns flow via helpers.start_deltains_run.
|
||||||
|
"""
|
||||||
|
global active_jobs, waiting_jobs
|
||||||
|
async with semaphore:
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs -= 1
|
||||||
|
active_jobs += 1
|
||||||
|
try:
|
||||||
|
await hdeltains.start_deltains_run(sid, data, url)
|
||||||
|
finally:
|
||||||
|
async with lock:
|
||||||
|
active_jobs -= 1
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/deltains-eligibility")
|
||||||
|
async def deltains_eligibility(request: Request):
|
||||||
|
"""
|
||||||
|
Starts a DeltaIns eligibility session in the background.
|
||||||
|
Body: { "data": { ... }, "url"?: string }
|
||||||
|
Returns: { status: "started", session_id: "<uuid>" }
|
||||||
|
"""
|
||||||
|
global waiting_jobs
|
||||||
|
|
||||||
|
body = await request.json()
|
||||||
|
data = body.get("data", {})
|
||||||
|
|
||||||
|
sid = hdeltains.make_session_entry()
|
||||||
|
hdeltains.sessions[sid]["type"] = "deltains_eligibility"
|
||||||
|
hdeltains.sessions[sid]["last_activity"] = time.time()
|
||||||
|
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs += 1
|
||||||
|
|
||||||
|
asyncio.create_task(_deltains_worker_wrapper(sid, data, url="https://www.deltadentalins.com/ciam/login?TARGET=%2Fprovider-tools%2Fv2"))
|
||||||
|
|
||||||
|
return {"status": "started", "session_id": sid}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/deltains-submit-otp")
|
||||||
|
async def deltains_submit_otp(request: Request):
|
||||||
|
"""
|
||||||
|
Body: { "session_id": "<sid>", "otp": "123456" }
|
||||||
|
Node / frontend call this when user provides OTP for DeltaIns.
|
||||||
|
"""
|
||||||
|
body = await request.json()
|
||||||
|
sid = body.get("session_id")
|
||||||
|
otp = body.get("otp")
|
||||||
|
if not sid or not otp:
|
||||||
|
raise HTTPException(status_code=400, detail="session_id and otp required")
|
||||||
|
|
||||||
|
res = hdeltains.submit_otp(sid, otp)
|
||||||
|
if res.get("status") == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=res.get("message"))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/deltains-session/{sid}/status")
|
||||||
|
async def deltains_session_status(sid: str):
|
||||||
|
s = hdeltains.get_session_status(sid)
|
||||||
|
if s.get("status") == "not_found":
|
||||||
|
raise HTTPException(status_code=404, detail="session not found")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
@app.post("/submit-otp")
|
@app.post("/submit-otp")
|
||||||
async def submit_otp(request: Request):
|
async def submit_otp(request: Request):
|
||||||
"""
|
"""
|
||||||
@@ -425,6 +499,18 @@ async def clear_unitedsco_session():
|
|||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/clear-deltains-session")
|
||||||
|
async def clear_deltains_session():
|
||||||
|
"""
|
||||||
|
Clears the Delta Dental Ins browser session. Called when DeltaIns credentials are deleted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
clear_deltains_session_on_startup()
|
||||||
|
return {"status": "success", "message": "DeltaIns session cleared"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
host = os.getenv("HOST")
|
host = os.getenv("HOST")
|
||||||
port = int(os.getenv("PORT"))
|
port = int(os.getenv("PORT"))
|
||||||
|
|||||||
376
apps/SeleniumService/deltains_browser_manager.py
Normal file
376
apps/SeleniumService/deltains_browser_manager.py
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
"""
|
||||||
|
Browser manager for Delta Dental Ins - handles persistent profile, cookie
|
||||||
|
save/restore (for Okta session-only cookies), and keeping browser alive.
|
||||||
|
Tracks credentials to detect changes mid-session.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import hashlib
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
|
||||||
|
if not os.environ.get("DISPLAY"):
|
||||||
|
os.environ["DISPLAY"] = ":0"
|
||||||
|
|
||||||
|
DELTAINS_DOMAIN = ".deltadentalins.com"
|
||||||
|
OKTA_DOMAINS = [".okta.com", ".oktacdn.com"]
|
||||||
|
|
||||||
|
|
||||||
|
class DeltaInsBrowserManager:
|
||||||
|
"""
|
||||||
|
Singleton that manages a persistent Chrome browser instance for Delta Dental Ins.
|
||||||
|
- Uses --user-data-dir for persistent profile
|
||||||
|
- Saves/restores Okta session cookies to survive browser restarts
|
||||||
|
- Tracks credentials to detect changes mid-session
|
||||||
|
"""
|
||||||
|
_instance = None
|
||||||
|
_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
with cls._lock:
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance._driver = None
|
||||||
|
cls._instance.profile_dir = os.path.abspath("chrome_profile_deltains")
|
||||||
|
cls._instance.download_dir = os.path.abspath("seleniumDownloads")
|
||||||
|
cls._instance._credentials_file = os.path.join(cls._instance.profile_dir, ".last_credentials")
|
||||||
|
cls._instance._cookies_file = os.path.join(cls._instance.profile_dir, ".saved_cookies.json")
|
||||||
|
cls._instance._needs_session_clear = False
|
||||||
|
os.makedirs(cls._instance.profile_dir, exist_ok=True)
|
||||||
|
os.makedirs(cls._instance.download_dir, exist_ok=True)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
# ── Cookie save / restore ──────────────────────────────────────────
|
||||||
|
|
||||||
|
def save_cookies(self):
|
||||||
|
"""Save all browser cookies to a JSON file so they survive browser restart."""
|
||||||
|
try:
|
||||||
|
if not self._driver:
|
||||||
|
return
|
||||||
|
cookies = self._driver.get_cookies()
|
||||||
|
if not cookies:
|
||||||
|
return
|
||||||
|
with open(self._cookies_file, "w") as f:
|
||||||
|
json.dump(cookies, f)
|
||||||
|
print(f"[DeltaIns BrowserManager] Saved {len(cookies)} cookies to disk")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Failed to save cookies: {e}")
|
||||||
|
|
||||||
|
def restore_cookies(self):
|
||||||
|
"""Restore saved cookies into the current browser session."""
|
||||||
|
if not os.path.exists(self._cookies_file):
|
||||||
|
print("[DeltaIns BrowserManager] No saved cookies file found")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self._cookies_file, "r") as f:
|
||||||
|
cookies = json.load(f)
|
||||||
|
|
||||||
|
if not cookies:
|
||||||
|
print("[DeltaIns BrowserManager] Saved cookies file is empty")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Navigate to the DeltaIns domain first so we can set cookies for it
|
||||||
|
try:
|
||||||
|
self._driver.get("https://www.deltadentalins.com/favicon.ico")
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception:
|
||||||
|
self._driver.get("https://www.deltadentalins.com")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
restored = 0
|
||||||
|
for cookie in cookies:
|
||||||
|
try:
|
||||||
|
# Remove problematic fields that Selenium doesn't accept
|
||||||
|
for key in ["sameSite", "storeId", "hostOnly", "session"]:
|
||||||
|
cookie.pop(key, None)
|
||||||
|
# sameSite must be one of: Strict, Lax, None
|
||||||
|
cookie["sameSite"] = "None"
|
||||||
|
self._driver.add_cookie(cookie)
|
||||||
|
restored += 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"[DeltaIns BrowserManager] Restored {restored}/{len(cookies)} cookies")
|
||||||
|
return restored > 0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Failed to restore cookies: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def clear_saved_cookies(self):
|
||||||
|
"""Delete the saved cookies file."""
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._cookies_file):
|
||||||
|
os.remove(self._cookies_file)
|
||||||
|
print("[DeltaIns BrowserManager] Cleared saved cookies file")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Failed to clear saved cookies: {e}")
|
||||||
|
|
||||||
|
# ── Session clear ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def clear_session_on_startup(self):
|
||||||
|
"""
|
||||||
|
Clear session cookies from Chrome profile on startup.
|
||||||
|
This forces a fresh login after PC restart.
|
||||||
|
"""
|
||||||
|
print("[DeltaIns BrowserManager] Clearing session on startup...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._credentials_file):
|
||||||
|
os.remove(self._credentials_file)
|
||||||
|
print("[DeltaIns BrowserManager] Cleared credentials tracking file")
|
||||||
|
|
||||||
|
# Also clear saved cookies
|
||||||
|
self.clear_saved_cookies()
|
||||||
|
|
||||||
|
session_files = [
|
||||||
|
"Cookies",
|
||||||
|
"Cookies-journal",
|
||||||
|
"Login Data",
|
||||||
|
"Login Data-journal",
|
||||||
|
"Web Data",
|
||||||
|
"Web Data-journal",
|
||||||
|
]
|
||||||
|
|
||||||
|
for filename in session_files:
|
||||||
|
filepath = os.path.join(self.profile_dir, "Default", filename)
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
try:
|
||||||
|
os.remove(filepath)
|
||||||
|
print(f"[DeltaIns BrowserManager] Removed {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Could not remove {filename}: {e}")
|
||||||
|
|
||||||
|
for filename in session_files:
|
||||||
|
filepath = os.path.join(self.profile_dir, filename)
|
||||||
|
if os.path.exists(filepath):
|
||||||
|
try:
|
||||||
|
os.remove(filepath)
|
||||||
|
print(f"[DeltaIns BrowserManager] Removed root {filename}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Could not remove root {filename}: {e}")
|
||||||
|
|
||||||
|
session_storage_dir = os.path.join(self.profile_dir, "Default", "Session Storage")
|
||||||
|
if os.path.exists(session_storage_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(session_storage_dir)
|
||||||
|
print("[DeltaIns BrowserManager] Cleared Session Storage")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Could not clear Session Storage: {e}")
|
||||||
|
|
||||||
|
local_storage_dir = os.path.join(self.profile_dir, "Default", "Local Storage")
|
||||||
|
if os.path.exists(local_storage_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(local_storage_dir)
|
||||||
|
print("[DeltaIns BrowserManager] Cleared Local Storage")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Could not clear Local Storage: {e}")
|
||||||
|
|
||||||
|
indexeddb_dir = os.path.join(self.profile_dir, "Default", "IndexedDB")
|
||||||
|
if os.path.exists(indexeddb_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(indexeddb_dir)
|
||||||
|
print("[DeltaIns BrowserManager] Cleared IndexedDB")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Could not clear IndexedDB: {e}")
|
||||||
|
|
||||||
|
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"[DeltaIns BrowserManager] Cleared {os.path.basename(cache_dir)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Could not clear {os.path.basename(cache_dir)}: {e}")
|
||||||
|
|
||||||
|
self._needs_session_clear = True
|
||||||
|
print("[DeltaIns BrowserManager] Session cleared - will require fresh login")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Error clearing session: {e}")
|
||||||
|
|
||||||
|
# ── Credential tracking ────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _hash_credentials(self, username: str) -> str:
|
||||||
|
return hashlib.sha256(username.encode()).hexdigest()[:16]
|
||||||
|
|
||||||
|
def get_last_credentials_hash(self) -> str | None:
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._credentials_file):
|
||||||
|
with open(self._credentials_file, 'r') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_credentials_hash(self, username: str):
|
||||||
|
try:
|
||||||
|
cred_hash = self._hash_credentials(username)
|
||||||
|
with open(self._credentials_file, 'w') as f:
|
||||||
|
f.write(cred_hash)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Failed to save credentials hash: {e}")
|
||||||
|
|
||||||
|
def credentials_changed(self, username: str) -> bool:
|
||||||
|
last_hash = self.get_last_credentials_hash()
|
||||||
|
if last_hash is None:
|
||||||
|
return False
|
||||||
|
current_hash = self._hash_credentials(username)
|
||||||
|
changed = last_hash != current_hash
|
||||||
|
if changed:
|
||||||
|
print("[DeltaIns BrowserManager] Credentials changed - logout required")
|
||||||
|
return changed
|
||||||
|
|
||||||
|
def clear_credentials_hash(self):
|
||||||
|
try:
|
||||||
|
if os.path.exists(self._credentials_file):
|
||||||
|
os.remove(self._credentials_file)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns BrowserManager] Failed to clear credentials hash: {e}")
|
||||||
|
|
||||||
|
# ── Chrome process management ──────────────────────────────────────
|
||||||
|
|
||||||
|
def _kill_existing_chrome_for_profile(self):
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["pgrep", "-f", f"user-data-dir={self.profile_dir}"],
|
||||||
|
capture_output=True, text=True
|
||||||
|
)
|
||||||
|
if result.stdout.strip():
|
||||||
|
pids = result.stdout.strip().split('\n')
|
||||||
|
for pid in pids:
|
||||||
|
try:
|
||||||
|
subprocess.run(["kill", "-9", pid], check=False)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for lock_file in ["SingletonLock", "SingletonSocket", "SingletonCookie"]:
|
||||||
|
lock_path = os.path.join(self.profile_dir, lock_file)
|
||||||
|
try:
|
||||||
|
if os.path.islink(lock_path) or os.path.exists(lock_path):
|
||||||
|
os.remove(lock_path)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ── Driver lifecycle ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
def get_driver(self, headless=False):
|
||||||
|
with self._lock:
|
||||||
|
need_cookie_restore = False
|
||||||
|
|
||||||
|
if self._driver is None:
|
||||||
|
print("[DeltaIns BrowserManager] Driver is None, creating new driver")
|
||||||
|
self._kill_existing_chrome_for_profile()
|
||||||
|
self._create_driver(headless)
|
||||||
|
need_cookie_restore = True
|
||||||
|
elif not self._is_alive():
|
||||||
|
print("[DeltaIns BrowserManager] Driver not alive, recreating")
|
||||||
|
# Save cookies from the dead session if possible (usually can't)
|
||||||
|
self._kill_existing_chrome_for_profile()
|
||||||
|
self._create_driver(headless)
|
||||||
|
need_cookie_restore = True
|
||||||
|
else:
|
||||||
|
print("[DeltaIns BrowserManager] Reusing existing driver")
|
||||||
|
|
||||||
|
if need_cookie_restore and os.path.exists(self._cookies_file):
|
||||||
|
print("[DeltaIns BrowserManager] Restoring saved cookies into new browser...")
|
||||||
|
self.restore_cookies()
|
||||||
|
|
||||||
|
return self._driver
|
||||||
|
|
||||||
|
def _is_alive(self):
|
||||||
|
try:
|
||||||
|
if self._driver is None:
|
||||||
|
return False
|
||||||
|
_ = self._driver.current_url
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _create_driver(self, headless=False):
|
||||||
|
if self._driver:
|
||||||
|
try:
|
||||||
|
self._driver.quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self._driver = None
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
if headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
|
||||||
|
options.add_argument(f"--user-data-dir={self.profile_dir}")
|
||||||
|
options.add_argument("--no-sandbox")
|
||||||
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
"download.default_directory": self.download_dir,
|
||||||
|
"plugins.always_open_pdf_externally": True,
|
||||||
|
"download.prompt_for_download": False,
|
||||||
|
"download.directory_upgrade": True,
|
||||||
|
"credentials_enable_service": False,
|
||||||
|
"profile.password_manager_enabled": False,
|
||||||
|
"profile.password_manager_leak_detection": False,
|
||||||
|
}
|
||||||
|
options.add_experimental_option("prefs", prefs)
|
||||||
|
|
||||||
|
service = Service(ChromeDriverManager().install())
|
||||||
|
self._driver = webdriver.Chrome(service=service, options=options)
|
||||||
|
self._driver.maximize_window()
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._driver.execute_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._needs_session_clear = False
|
||||||
|
|
||||||
|
def quit_driver(self):
|
||||||
|
with self._lock:
|
||||||
|
if self._driver:
|
||||||
|
try:
|
||||||
|
self._driver.quit()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self._driver = None
|
||||||
|
self._kill_existing_chrome_for_profile()
|
||||||
|
|
||||||
|
|
||||||
|
_manager = None
|
||||||
|
|
||||||
|
def get_browser_manager():
|
||||||
|
global _manager
|
||||||
|
if _manager is None:
|
||||||
|
_manager = DeltaInsBrowserManager()
|
||||||
|
return _manager
|
||||||
|
|
||||||
|
|
||||||
|
def clear_deltains_session_on_startup():
|
||||||
|
"""Called by agent.py on startup to clear session."""
|
||||||
|
manager = get_browser_manager()
|
||||||
|
manager.clear_session_on_startup()
|
||||||
300
apps/SeleniumService/helpers_deltains_eligibility.py
Normal file
300
apps/SeleniumService/helpers_deltains_eligibility.py
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, Any
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||||
|
|
||||||
|
from selenium_DeltaIns_eligibilityCheckWorker import AutomationDeltaInsEligibilityCheck
|
||||||
|
from deltains_browser_manager import get_browser_manager
|
||||||
|
|
||||||
|
# In-memory session store
|
||||||
|
sessions: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
SESSION_OTP_TIMEOUT = int(os.getenv("SESSION_OTP_TIMEOUT", "240"))
|
||||||
|
|
||||||
|
|
||||||
|
def make_session_entry() -> str:
|
||||||
|
import uuid
|
||||||
|
sid = str(uuid.uuid4())
|
||||||
|
sessions[sid] = {
|
||||||
|
"status": "created",
|
||||||
|
"created_at": time.time(),
|
||||||
|
"last_activity": time.time(),
|
||||||
|
"bot": None,
|
||||||
|
"driver": None,
|
||||||
|
"otp_event": asyncio.Event(),
|
||||||
|
"otp_value": None,
|
||||||
|
"result": None,
|
||||||
|
"message": None,
|
||||||
|
"type": None,
|
||||||
|
}
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_session(sid: str, message: str | None = None):
|
||||||
|
s = sessions.get(sid)
|
||||||
|
if not s:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
if s.get("status") not in ("completed", "error", "not_found"):
|
||||||
|
s["status"] = "error"
|
||||||
|
if message:
|
||||||
|
s["message"] = message
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ev = s.get("otp_event")
|
||||||
|
if ev and not ev.is_set():
|
||||||
|
ev.set()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
sessions.pop(sid, None)
|
||||||
|
|
||||||
|
|
||||||
|
async def _remove_session_later(sid: str, delay: int = 30):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
await cleanup_session(sid)
|
||||||
|
|
||||||
|
|
||||||
|
def _close_browser(bot):
|
||||||
|
"""Save cookies and close the browser after task completion."""
|
||||||
|
try:
|
||||||
|
bm = get_browser_manager()
|
||||||
|
try:
|
||||||
|
bm.save_cookies()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
bm.quit_driver()
|
||||||
|
print("[DeltaIns] Browser closed")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns] Could not close browser: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def start_deltains_run(sid: str, data: dict, url: str):
|
||||||
|
"""
|
||||||
|
Run the DeltaIns eligibility check workflow:
|
||||||
|
1. Login (with OTP if needed)
|
||||||
|
2. Search patient by Member ID + DOB
|
||||||
|
3. Extract eligibility info + PDF
|
||||||
|
"""
|
||||||
|
s = sessions.get(sid)
|
||||||
|
if not s:
|
||||||
|
return {"status": "error", "message": "session not found"}
|
||||||
|
|
||||||
|
s["status"] = "running"
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
bot = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
bot = AutomationDeltaInsEligibilityCheck({"data": data})
|
||||||
|
bot.config_driver()
|
||||||
|
|
||||||
|
s["bot"] = bot
|
||||||
|
s["driver"] = bot.driver
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
|
# Maximize window and login (bot.login handles navigation itself,
|
||||||
|
# checking provider-tools URL first to preserve existing sessions)
|
||||||
|
try:
|
||||||
|
bot.driver.maximize_window()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
login_result = bot.login(url)
|
||||||
|
except WebDriverException as wde:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"Selenium driver error during login: {wde}"
|
||||||
|
s["result"] = {"status": "error", "message": s["message"]}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
except Exception as e:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"Unexpected error during login: {e}"
|
||||||
|
s["result"] = {"status": "error", "message": s["message"]}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
# Handle login result
|
||||||
|
if isinstance(login_result, str) and login_result == "ALREADY_LOGGED_IN":
|
||||||
|
s["status"] = "running"
|
||||||
|
s["message"] = "Session persisted"
|
||||||
|
print("[DeltaIns] Session persisted - skipping OTP")
|
||||||
|
# Re-save cookies to keep them fresh on disk
|
||||||
|
get_browser_manager().save_cookies()
|
||||||
|
|
||||||
|
elif isinstance(login_result, str) and login_result == "OTP_REQUIRED":
|
||||||
|
s["status"] = "waiting_for_otp"
|
||||||
|
s["message"] = "OTP required - please enter the code sent to your email"
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
|
driver = s["driver"]
|
||||||
|
max_polls = SESSION_OTP_TIMEOUT
|
||||||
|
login_success = False
|
||||||
|
|
||||||
|
print(f"[DeltaIns OTP] Waiting for OTP (polling for {SESSION_OTP_TIMEOUT}s)...")
|
||||||
|
|
||||||
|
for poll in range(max_polls):
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
otp_value = s.get("otp_value")
|
||||||
|
if otp_value:
|
||||||
|
print(f"[DeltaIns OTP] OTP received from app: {otp_value}")
|
||||||
|
try:
|
||||||
|
otp_input = driver.find_element(By.XPATH,
|
||||||
|
"//input[@name='credentials.passcode' and @type='text'] | "
|
||||||
|
"//input[contains(@name,'passcode')]")
|
||||||
|
otp_input.clear()
|
||||||
|
otp_input.send_keys(otp_value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
verify_btn = driver.find_element(By.XPATH,
|
||||||
|
"//input[@type='submit'] | "
|
||||||
|
"//button[@type='submit']")
|
||||||
|
verify_btn.click()
|
||||||
|
print("[DeltaIns OTP] Clicked verify button")
|
||||||
|
except Exception:
|
||||||
|
otp_input.send_keys(Keys.RETURN)
|
||||||
|
print("[DeltaIns OTP] Pressed Enter as fallback")
|
||||||
|
|
||||||
|
s["otp_value"] = None
|
||||||
|
await asyncio.sleep(8)
|
||||||
|
except Exception as type_err:
|
||||||
|
print(f"[DeltaIns OTP] Failed to type OTP: {type_err}")
|
||||||
|
|
||||||
|
current_url = driver.current_url.lower()
|
||||||
|
if poll % 10 == 0:
|
||||||
|
print(f"[DeltaIns OTP Poll {poll+1}/{max_polls}] URL: {current_url[:80]}")
|
||||||
|
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url and "ciam" not in current_url:
|
||||||
|
print("[DeltaIns OTP] Login successful!")
|
||||||
|
login_success = True
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as poll_err:
|
||||||
|
if poll % 10 == 0:
|
||||||
|
print(f"[DeltaIns OTP Poll {poll+1}] Error: {poll_err}")
|
||||||
|
|
||||||
|
if not login_success:
|
||||||
|
try:
|
||||||
|
current_url = driver.current_url.lower()
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url and "ciam" not in current_url:
|
||||||
|
login_success = True
|
||||||
|
else:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = "OTP timeout - login not completed"
|
||||||
|
s["result"] = {"status": "error", "message": "OTP not completed in time"}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": "OTP not completed in time"}
|
||||||
|
except Exception as final_err:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"OTP verification failed: {final_err}"
|
||||||
|
s["result"] = {"status": "error", "message": s["message"]}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
if login_success:
|
||||||
|
s["status"] = "running"
|
||||||
|
s["message"] = "Login successful after OTP"
|
||||||
|
print("[DeltaIns OTP] Proceeding to step1...")
|
||||||
|
# Save cookies to disk so session survives browser restart
|
||||||
|
get_browser_manager().save_cookies()
|
||||||
|
|
||||||
|
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = login_result
|
||||||
|
s["result"] = {"status": "error", "message": login_result}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": login_result}
|
||||||
|
|
||||||
|
elif isinstance(login_result, str) and login_result == "SUCCESS":
|
||||||
|
print("[DeltaIns] Login succeeded without OTP")
|
||||||
|
s["status"] = "running"
|
||||||
|
s["message"] = "Login succeeded"
|
||||||
|
# Save cookies to disk so session survives browser restart
|
||||||
|
get_browser_manager().save_cookies()
|
||||||
|
|
||||||
|
# Step 1 - search patient
|
||||||
|
step1_result = bot.step1()
|
||||||
|
print(f"[DeltaIns] step1 result: {step1_result}")
|
||||||
|
|
||||||
|
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = step1_result
|
||||||
|
s["result"] = {"status": "error", "message": step1_result}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": step1_result}
|
||||||
|
|
||||||
|
# Step 2 - extract eligibility info + PDF
|
||||||
|
step2_result = bot.step2()
|
||||||
|
print(f"[DeltaIns] step2 result: {step2_result.get('status') if isinstance(step2_result, dict) else step2_result}")
|
||||||
|
|
||||||
|
if isinstance(step2_result, dict):
|
||||||
|
s["status"] = "completed"
|
||||||
|
s["result"] = step2_result
|
||||||
|
s["message"] = "completed"
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 60))
|
||||||
|
return step2_result
|
||||||
|
else:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"step2 returned unexpected result: {step2_result}"
|
||||||
|
s["result"] = {"status": "error", "message": s["message"]}
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if s:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"worker exception: {e}"
|
||||||
|
s["result"] = {"status": "error", "message": s["message"]}
|
||||||
|
if bot:
|
||||||
|
_close_browser(bot)
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return {"status": "error", "message": f"worker exception: {e}"}
|
||||||
|
|
||||||
|
|
||||||
|
def submit_otp(sid: str, otp: str) -> Dict[str, Any]:
|
||||||
|
s = sessions.get(sid)
|
||||||
|
if not s:
|
||||||
|
return {"status": "error", "message": "session not found"}
|
||||||
|
if s.get("status") != "waiting_for_otp":
|
||||||
|
return {"status": "error", "message": f"session not waiting for otp (state={s.get('status')})"}
|
||||||
|
s["otp_value"] = otp
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
try:
|
||||||
|
s["otp_event"].set()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {"status": "ok", "message": "otp accepted"}
|
||||||
|
|
||||||
|
|
||||||
|
def get_session_status(sid: str) -> Dict[str, Any]:
|
||||||
|
s = sessions.get(sid)
|
||||||
|
if not s:
|
||||||
|
return {"status": "not_found"}
|
||||||
|
return {
|
||||||
|
"session_id": sid,
|
||||||
|
"status": s.get("status"),
|
||||||
|
"message": s.get("message"),
|
||||||
|
"created_at": s.get("created_at"),
|
||||||
|
"last_activity": s.get("last_activity"),
|
||||||
|
"result": s.get("result") if s.get("status") in ("completed", "error") else None,
|
||||||
|
}
|
||||||
BIN
apps/SeleniumService/seleniumDownloads/EligibilityBenefit.pdf
Normal file
BIN
apps/SeleniumService/seleniumDownloads/EligibilityBenefit.pdf
Normal file
Binary file not shown.
686
apps/SeleniumService/selenium_DeltaIns_eligibilityCheckWorker.py
Normal file
686
apps/SeleniumService/selenium_DeltaIns_eligibilityCheckWorker.py
Normal file
@@ -0,0 +1,686 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common.exceptions import WebDriverException, TimeoutException
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
import time
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
import glob
|
||||||
|
|
||||||
|
from deltains_browser_manager import get_browser_manager
|
||||||
|
|
||||||
|
LOGIN_URL = "https://www.deltadentalins.com/ciam/login?TARGET=%2Fprovider-tools%2Fv2"
|
||||||
|
PROVIDER_TOOLS_URL = "https://www.deltadentalins.com/provider-tools/v2"
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationDeltaInsEligibilityCheck:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.headless = False
|
||||||
|
self.driver = None
|
||||||
|
|
||||||
|
self.data = data.get("data", {}) if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
self.memberId = self.data.get("memberId", "")
|
||||||
|
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||||
|
self.firstName = self.data.get("firstName", "")
|
||||||
|
self.lastName = self.data.get("lastName", "")
|
||||||
|
self.deltains_username = self.data.get("deltains_username", "")
|
||||||
|
self.deltains_password = self.data.get("deltains_password", "")
|
||||||
|
|
||||||
|
self.download_dir = get_browser_manager().download_dir
|
||||||
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
def config_driver(self):
|
||||||
|
self.driver = get_browser_manager().get_driver(self.headless)
|
||||||
|
|
||||||
|
def _dismiss_cookie_banner(self):
|
||||||
|
try:
|
||||||
|
accept_btn = WebDriverWait(self.driver, 5).until(
|
||||||
|
EC.element_to_be_clickable((By.ID, "onetrust-accept-btn-handler"))
|
||||||
|
)
|
||||||
|
accept_btn.click()
|
||||||
|
print("[DeltaIns login] Dismissed cookie consent banner")
|
||||||
|
time.sleep(1)
|
||||||
|
except TimeoutException:
|
||||||
|
print("[DeltaIns login] No cookie consent banner found")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns login] Error dismissing cookie banner: {e}")
|
||||||
|
|
||||||
|
def _force_logout(self):
|
||||||
|
try:
|
||||||
|
print("[DeltaIns login] Forcing logout due to credential change...")
|
||||||
|
browser_manager = get_browser_manager()
|
||||||
|
try:
|
||||||
|
self.driver.delete_all_cookies()
|
||||||
|
print("[DeltaIns login] Cleared all cookies")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns login] Error clearing cookies: {e}")
|
||||||
|
browser_manager.clear_credentials_hash()
|
||||||
|
print("[DeltaIns login] Logout complete")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns login] Error during forced logout: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def login(self, url):
|
||||||
|
"""
|
||||||
|
Multi-step login flow for DeltaIns (Okta-based):
|
||||||
|
1. Enter username (name='identifier') -> click Next
|
||||||
|
2. Enter password (type='password') -> click Submit
|
||||||
|
3. Handle MFA: click 'Send me an email' -> wait for OTP
|
||||||
|
Returns: ALREADY_LOGGED_IN, SUCCESS, OTP_REQUIRED, or ERROR:...
|
||||||
|
"""
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
browser_manager = get_browser_manager()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if self.deltains_username and browser_manager.credentials_changed(self.deltains_username):
|
||||||
|
self._force_logout()
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# First, try navigating to provider-tools directly (not login URL)
|
||||||
|
# This avoids triggering Okta password re-verification when session is valid
|
||||||
|
try:
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
print(f"[DeltaIns login] Current URL: {current_url}")
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
||||||
|
print("[DeltaIns login] Already on provider tools page - logged in")
|
||||||
|
return "ALREADY_LOGGED_IN"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns login] Error checking current state: {e}")
|
||||||
|
|
||||||
|
# Navigate to provider-tools URL first to check if session is still valid
|
||||||
|
print("[DeltaIns login] Trying provider-tools URL to check session...")
|
||||||
|
self.driver.get(PROVIDER_TOOLS_URL)
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
print(f"[DeltaIns login] After provider-tools nav URL: {current_url}")
|
||||||
|
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
||||||
|
print("[DeltaIns login] Session still valid - already logged in")
|
||||||
|
return "ALREADY_LOGGED_IN"
|
||||||
|
|
||||||
|
# Session expired or not logged in - navigate to login URL
|
||||||
|
print("[DeltaIns login] Session not valid, navigating to login page...")
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
print(f"[DeltaIns login] After login nav URL: {current_url}")
|
||||||
|
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
||||||
|
print("[DeltaIns login] Already logged in - on provider tools")
|
||||||
|
return "ALREADY_LOGGED_IN"
|
||||||
|
|
||||||
|
self._dismiss_cookie_banner()
|
||||||
|
|
||||||
|
# Step 1: Username entry (name='identifier')
|
||||||
|
print("[DeltaIns login] Looking for username field...")
|
||||||
|
username_entered = False
|
||||||
|
for sel in [
|
||||||
|
(By.NAME, "identifier"),
|
||||||
|
(By.ID, "okta-signin-username"),
|
||||||
|
(By.XPATH, "//input[@type='text' and @autocomplete='username']"),
|
||||||
|
(By.XPATH, "//input[@type='text']"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
field = WebDriverWait(self.driver, 8).until(EC.presence_of_element_located(sel))
|
||||||
|
if field.is_displayed():
|
||||||
|
field.clear()
|
||||||
|
field.send_keys(self.deltains_username)
|
||||||
|
username_entered = True
|
||||||
|
print(f"[DeltaIns login] Username entered via {sel}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not username_entered:
|
||||||
|
return "ERROR: Could not find username field"
|
||||||
|
|
||||||
|
# Click Next/Submit
|
||||||
|
time.sleep(1)
|
||||||
|
for sel in [
|
||||||
|
(By.XPATH, "//input[@type='submit' and @value='Next']"),
|
||||||
|
(By.XPATH, "//input[@type='submit']"),
|
||||||
|
(By.XPATH, "//button[@type='submit']"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
btn = self.driver.find_element(*sel)
|
||||||
|
if btn.is_displayed():
|
||||||
|
btn.click()
|
||||||
|
print(f"[DeltaIns login] Clicked Next via {sel}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
time.sleep(4)
|
||||||
|
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
||||||
|
return "ALREADY_LOGGED_IN"
|
||||||
|
|
||||||
|
# Step 2: Password entry
|
||||||
|
print("[DeltaIns login] Looking for password field...")
|
||||||
|
pw_entered = False
|
||||||
|
for sel in [
|
||||||
|
(By.XPATH, "//input[@type='password']"),
|
||||||
|
(By.ID, "okta-signin-password"),
|
||||||
|
(By.NAME, "password"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
field = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(sel))
|
||||||
|
if field.is_displayed():
|
||||||
|
field.clear()
|
||||||
|
field.send_keys(self.deltains_password)
|
||||||
|
pw_entered = True
|
||||||
|
print(f"[DeltaIns login] Password entered via {sel}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not pw_entered:
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower():
|
||||||
|
return "ALREADY_LOGGED_IN"
|
||||||
|
return "ERROR: Password field not found"
|
||||||
|
|
||||||
|
# Click Sign In
|
||||||
|
time.sleep(1)
|
||||||
|
for sel in [
|
||||||
|
(By.ID, "okta-signin-submit"),
|
||||||
|
(By.XPATH, "//input[@type='submit']"),
|
||||||
|
(By.XPATH, "//button[@type='submit']"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
btn = self.driver.find_element(*sel)
|
||||||
|
if btn.is_displayed():
|
||||||
|
btn.click()
|
||||||
|
print(f"[DeltaIns login] Clicked Sign In via {sel}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.deltains_username:
|
||||||
|
browser_manager.save_credentials_hash(self.deltains_username)
|
||||||
|
|
||||||
|
time.sleep(6)
|
||||||
|
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
print(f"[DeltaIns login] After password submit URL: {current_url}")
|
||||||
|
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
||||||
|
print("[DeltaIns login] Login successful - on provider tools")
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
# Step 3: MFA handling
|
||||||
|
# There are two possible MFA pages:
|
||||||
|
# A) Method selection: "Verify it's you with a security method" with Email/Phone Select buttons
|
||||||
|
# B) Direct: "Send me an email" button
|
||||||
|
print("[DeltaIns login] Handling MFA...")
|
||||||
|
|
||||||
|
# Check for method selection page first (Email "Select" link)
|
||||||
|
# The Okta MFA page uses <a> tags (not buttons/inputs) with class "select-factor"
|
||||||
|
# inside <div data-se="okta_email"> for Email selection
|
||||||
|
try:
|
||||||
|
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "security method" in body_text.lower() or "select from the following" in body_text.lower():
|
||||||
|
print("[DeltaIns login] MFA method selection page detected")
|
||||||
|
email_select = None
|
||||||
|
for sel in [
|
||||||
|
(By.CSS_SELECTOR, "div[data-se='okta_email'] a.select-factor"),
|
||||||
|
(By.XPATH, "//div[@data-se='okta_email']//a[contains(@class,'select-factor')]"),
|
||||||
|
(By.XPATH, "//a[contains(@aria-label,'Select Email')]"),
|
||||||
|
(By.XPATH, "//div[@data-se='okta_email']//a[@data-se='button']"),
|
||||||
|
(By.CSS_SELECTOR, "a.select-factor.link-button"),
|
||||||
|
]:
|
||||||
|
try:
|
||||||
|
btn = self.driver.find_element(*sel)
|
||||||
|
if btn.is_displayed():
|
||||||
|
email_select = btn
|
||||||
|
print(f"[DeltaIns login] Found Email Select via {sel}")
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if email_select:
|
||||||
|
email_select.click()
|
||||||
|
print("[DeltaIns login] Clicked 'Select' for Email MFA")
|
||||||
|
time.sleep(5)
|
||||||
|
else:
|
||||||
|
print("[DeltaIns login] Could not find Email Select button")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns login] Error checking MFA method selection: {e}")
|
||||||
|
|
||||||
|
# Now look for "Send me an email" button (may appear after method selection or directly)
|
||||||
|
try:
|
||||||
|
send_btn = WebDriverWait(self.driver, 8).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//input[@type='submit' and @value='Send me an email'] | "
|
||||||
|
"//input[@value='Send me an email'] | "
|
||||||
|
"//button[contains(text(),'Send me an email')]"))
|
||||||
|
)
|
||||||
|
send_btn.click()
|
||||||
|
print("[DeltaIns login] Clicked 'Send me an email'")
|
||||||
|
time.sleep(5)
|
||||||
|
except TimeoutException:
|
||||||
|
print("[DeltaIns login] No 'Send me an email' button, checking for OTP input...")
|
||||||
|
|
||||||
|
# Step 4: OTP entry page
|
||||||
|
try:
|
||||||
|
otp_input = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH,
|
||||||
|
"//input[@name='credentials.passcode' and @type='text'] | "
|
||||||
|
"//input[contains(@name,'passcode')]"))
|
||||||
|
)
|
||||||
|
print("[DeltaIns login] OTP input found -> OTP_REQUIRED")
|
||||||
|
return "OTP_REQUIRED"
|
||||||
|
except TimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
if "provider-tools" in current_url and "login" not in current_url.lower() and "ciam" not in current_url.lower():
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
try:
|
||||||
|
error_elem = self.driver.find_element(By.XPATH,
|
||||||
|
"//*[contains(@class,'error') or contains(@class,'alert-error')]")
|
||||||
|
error_text = error_elem.text.strip()[:200]
|
||||||
|
if error_text:
|
||||||
|
return f"ERROR: {error_text}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("[DeltaIns login] Could not determine login state - returning OTP_REQUIRED as fallback")
|
||||||
|
return "OTP_REQUIRED"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns login] Exception: {e}")
|
||||||
|
return f"ERROR:LOGIN FAILED: {e}"
|
||||||
|
|
||||||
|
def _format_dob(self, dob_str):
|
||||||
|
"""Convert DOB from YYYY-MM-DD to MM/DD/YYYY format."""
|
||||||
|
if dob_str and "-" in dob_str:
|
||||||
|
dob_parts = dob_str.split("-")
|
||||||
|
if len(dob_parts) == 3:
|
||||||
|
return f"{dob_parts[1]}/{dob_parts[2]}/{dob_parts[0]}"
|
||||||
|
return dob_str
|
||||||
|
|
||||||
|
def _close_browser(self):
|
||||||
|
"""Save cookies and close the browser after task completion."""
|
||||||
|
browser_manager = get_browser_manager()
|
||||||
|
try:
|
||||||
|
browser_manager.save_cookies()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns] Failed to save cookies before close: {e}")
|
||||||
|
try:
|
||||||
|
browser_manager.quit_driver()
|
||||||
|
print("[DeltaIns] Browser closed")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns] Could not close browser: {e}")
|
||||||
|
|
||||||
|
def step1(self):
|
||||||
|
"""
|
||||||
|
Navigate to Eligibility search, enter patient info, search, and
|
||||||
|
click 'Check eligibility and benefits' on the result card.
|
||||||
|
|
||||||
|
Search flow:
|
||||||
|
1. Click 'Eligibility and benefits' link
|
||||||
|
2. Click 'Search for a new patient' button
|
||||||
|
3. Click 'Search by member ID' tab
|
||||||
|
4. Enter Member ID in #memberId
|
||||||
|
5. Enter DOB in #dob (MM/DD/YYYY)
|
||||||
|
6. Click Search
|
||||||
|
7. Extract patient info from result card
|
||||||
|
8. Click 'Check eligibility and benefits'
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
formatted_dob = self._format_dob(self.dateOfBirth)
|
||||||
|
print(f"[DeltaIns step1] Starting — memberId={self.memberId}, DOB={formatted_dob}")
|
||||||
|
|
||||||
|
# 1. Click "Eligibility and benefits" link
|
||||||
|
print("[DeltaIns step1] Clicking 'Eligibility and benefits'...")
|
||||||
|
try:
|
||||||
|
elig_link = WebDriverWait(self.driver, 15).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//a[contains(text(),'Eligibility and benefits')] | "
|
||||||
|
"//a[contains(text(),'Eligibility')]"))
|
||||||
|
)
|
||||||
|
elig_link.click()
|
||||||
|
time.sleep(5)
|
||||||
|
print("[DeltaIns step1] Clicked Eligibility link")
|
||||||
|
except TimeoutException:
|
||||||
|
print("[DeltaIns step1] No Eligibility link found, checking if already on page...")
|
||||||
|
if "patient-search" not in self.driver.current_url and "eligibility" not in self.driver.current_url:
|
||||||
|
self.driver.get("https://www.deltadentalins.com/provider-tools/v2/patient-search")
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# 2. Click "Search for a new patient" button
|
||||||
|
print("[DeltaIns step1] Clicking 'Search for a new patient'...")
|
||||||
|
try:
|
||||||
|
new_patient_btn = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//button[contains(text(),'Search for a new patient')]"))
|
||||||
|
)
|
||||||
|
new_patient_btn.click()
|
||||||
|
time.sleep(3)
|
||||||
|
print("[DeltaIns step1] Clicked 'Search for a new patient'")
|
||||||
|
except TimeoutException:
|
||||||
|
print("[DeltaIns step1] 'Search for a new patient' button not found - may already be on search page")
|
||||||
|
|
||||||
|
# 3. Click "Search by member ID" tab
|
||||||
|
print("[DeltaIns step1] Clicking 'Search by member ID' tab...")
|
||||||
|
try:
|
||||||
|
member_id_tab = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//button[contains(text(),'Search by member ID')]"))
|
||||||
|
)
|
||||||
|
member_id_tab.click()
|
||||||
|
time.sleep(2)
|
||||||
|
print("[DeltaIns step1] Clicked 'Search by member ID' tab")
|
||||||
|
except TimeoutException:
|
||||||
|
print("[DeltaIns step1] 'Search by member ID' tab not found")
|
||||||
|
return "ERROR: Could not find 'Search by member ID' tab"
|
||||||
|
|
||||||
|
# 4. Enter Member ID
|
||||||
|
print(f"[DeltaIns step1] Entering Member ID: {self.memberId}")
|
||||||
|
try:
|
||||||
|
mid_field = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.presence_of_element_located((By.ID, "memberId"))
|
||||||
|
)
|
||||||
|
mid_field.click()
|
||||||
|
mid_field.send_keys(Keys.CONTROL + "a")
|
||||||
|
mid_field.send_keys(Keys.DELETE)
|
||||||
|
time.sleep(0.3)
|
||||||
|
mid_field.send_keys(self.memberId)
|
||||||
|
time.sleep(0.5)
|
||||||
|
print(f"[DeltaIns step1] Member ID entered: '{mid_field.get_attribute('value')}'")
|
||||||
|
except TimeoutException:
|
||||||
|
return "ERROR: Member ID field not found"
|
||||||
|
|
||||||
|
# 5. Enter DOB
|
||||||
|
print(f"[DeltaIns step1] Entering DOB: {formatted_dob}")
|
||||||
|
try:
|
||||||
|
dob_field = self.driver.find_element(By.ID, "dob")
|
||||||
|
dob_field.click()
|
||||||
|
dob_field.send_keys(Keys.CONTROL + "a")
|
||||||
|
dob_field.send_keys(Keys.DELETE)
|
||||||
|
time.sleep(0.3)
|
||||||
|
dob_field.send_keys(formatted_dob)
|
||||||
|
time.sleep(0.5)
|
||||||
|
print(f"[DeltaIns step1] DOB entered: '{dob_field.get_attribute('value')}'")
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: DOB field not found: {e}"
|
||||||
|
|
||||||
|
# 6. Click Search
|
||||||
|
print("[DeltaIns step1] Clicking Search...")
|
||||||
|
try:
|
||||||
|
search_btn = self.driver.find_element(By.XPATH,
|
||||||
|
"//button[@type='submit'][contains(text(),'Search')] | "
|
||||||
|
"//button[@data-testid='searchButton']")
|
||||||
|
search_btn.click()
|
||||||
|
time.sleep(10)
|
||||||
|
print("[DeltaIns step1] Search clicked")
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: Search button not found: {e}"
|
||||||
|
|
||||||
|
# 7. Check for results - look for patient card
|
||||||
|
print("[DeltaIns step1] Checking for results...")
|
||||||
|
try:
|
||||||
|
patient_card = WebDriverWait(self.driver, 15).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH,
|
||||||
|
"//div[contains(@class,'patient-card-root')] | "
|
||||||
|
"//div[@data-testid='patientCard'] | "
|
||||||
|
"//div[starts-with(@data-testid,'patientCard')]"))
|
||||||
|
)
|
||||||
|
print("[DeltaIns step1] Patient card found!")
|
||||||
|
|
||||||
|
# Extract patient name
|
||||||
|
try:
|
||||||
|
name_el = patient_card.find_element(By.XPATH, ".//h3")
|
||||||
|
patient_name = name_el.text.strip()
|
||||||
|
print(f"[DeltaIns step1] Patient name: {patient_name}")
|
||||||
|
except Exception:
|
||||||
|
patient_name = ""
|
||||||
|
|
||||||
|
# Extract eligibility dates
|
||||||
|
try:
|
||||||
|
elig_el = patient_card.find_element(By.XPATH,
|
||||||
|
".//*[@data-testid='patientCardMemberEligibility']//*[contains(@class,'pt-staticfield-text')]")
|
||||||
|
elig_text = elig_el.text.strip()
|
||||||
|
print(f"[DeltaIns step1] Eligibility: {elig_text}")
|
||||||
|
except Exception:
|
||||||
|
elig_text = ""
|
||||||
|
|
||||||
|
# Store for step2
|
||||||
|
self._patient_name = patient_name
|
||||||
|
self._eligibility_text = elig_text
|
||||||
|
|
||||||
|
except TimeoutException:
|
||||||
|
# Check for error messages
|
||||||
|
try:
|
||||||
|
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "no results" in body_text.lower() or "not found" in body_text.lower() or "no patient" in body_text.lower():
|
||||||
|
return "ERROR: No patient found with the provided Member ID and DOB"
|
||||||
|
# Check for specific error alerts
|
||||||
|
alerts = self.driver.find_elements(By.XPATH, "//*[@role='alert']")
|
||||||
|
for alert in alerts:
|
||||||
|
if alert.is_displayed():
|
||||||
|
return f"ERROR: {alert.text.strip()[:200]}"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return "ERROR: No patient results found within timeout"
|
||||||
|
|
||||||
|
# 8. Click "Check eligibility and benefits"
|
||||||
|
print("[DeltaIns step1] Clicking 'Check eligibility and benefits'...")
|
||||||
|
try:
|
||||||
|
check_btn = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//button[contains(text(),'Check eligibility and benefits')] | "
|
||||||
|
"//button[@data-testid='eligibilityBenefitsButton']"))
|
||||||
|
)
|
||||||
|
check_btn.click()
|
||||||
|
time.sleep(10)
|
||||||
|
print(f"[DeltaIns step1] Navigated to: {self.driver.current_url}")
|
||||||
|
except TimeoutException:
|
||||||
|
return "ERROR: 'Check eligibility and benefits' button not found"
|
||||||
|
|
||||||
|
return "SUCCESS"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns step1] Exception: {e}")
|
||||||
|
return f"ERROR: step1 failed: {e}"
|
||||||
|
|
||||||
|
def step2(self):
|
||||||
|
"""
|
||||||
|
Extract eligibility information and capture PDF from the
|
||||||
|
Eligibility & Benefits detail page.
|
||||||
|
|
||||||
|
URL: .../provider-tools/v2/eligibility-benefits
|
||||||
|
|
||||||
|
Extracts:
|
||||||
|
- Patient name from h3 in patient-card-header
|
||||||
|
- DOB, Member ID, eligibility from data-testid fields
|
||||||
|
- PDF via Page.printToPDF
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
print("[DeltaIns step2] Extracting eligibility data...")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
current_url = self.driver.current_url
|
||||||
|
print(f"[DeltaIns step2] URL: {current_url}")
|
||||||
|
|
||||||
|
if "eligibility-benefits" not in current_url:
|
||||||
|
print("[DeltaIns step2] Not on eligibility page, checking body text...")
|
||||||
|
|
||||||
|
# Extract patient name
|
||||||
|
patientName = ""
|
||||||
|
try:
|
||||||
|
name_el = self.driver.find_element(By.XPATH,
|
||||||
|
"//div[contains(@class,'patient-card-header')]//h3 | "
|
||||||
|
"//div[starts-with(@data-testid,'patientCard')]//h3")
|
||||||
|
patientName = name_el.text.strip()
|
||||||
|
print(f"[DeltaIns step2] Patient name: {patientName}")
|
||||||
|
except Exception:
|
||||||
|
patientName = getattr(self, '_patient_name', '') or f"{self.firstName} {self.lastName}".strip()
|
||||||
|
print(f"[DeltaIns step2] Using stored/fallback name: {patientName}")
|
||||||
|
|
||||||
|
# Extract DOB from card
|
||||||
|
extractedDob = ""
|
||||||
|
try:
|
||||||
|
dob_el = self.driver.find_element(By.XPATH,
|
||||||
|
"//*[@data-testid='patientCardDateOfBirth']//*[contains(@class,'pt-staticfield-text')]")
|
||||||
|
extractedDob = dob_el.text.strip()
|
||||||
|
print(f"[DeltaIns step2] DOB: {extractedDob}")
|
||||||
|
except Exception:
|
||||||
|
extractedDob = self._format_dob(self.dateOfBirth)
|
||||||
|
|
||||||
|
# Extract Member ID from card
|
||||||
|
foundMemberId = ""
|
||||||
|
try:
|
||||||
|
mid_el = self.driver.find_element(By.XPATH,
|
||||||
|
"//*[@data-testid='patientCardMemberId']//*[contains(@class,'pt-staticfield-text')]")
|
||||||
|
foundMemberId = mid_el.text.strip()
|
||||||
|
print(f"[DeltaIns step2] Member ID: {foundMemberId}")
|
||||||
|
except Exception:
|
||||||
|
foundMemberId = self.memberId
|
||||||
|
|
||||||
|
# Extract eligibility status
|
||||||
|
eligibility = "Unknown"
|
||||||
|
try:
|
||||||
|
elig_el = self.driver.find_element(By.XPATH,
|
||||||
|
"//*[@data-testid='patientCardMemberEligibility']//*[contains(@class,'pt-staticfield-text')]")
|
||||||
|
elig_text = elig_el.text.strip()
|
||||||
|
print(f"[DeltaIns step2] Eligibility text: {elig_text}")
|
||||||
|
|
||||||
|
if "present" in elig_text.lower():
|
||||||
|
eligibility = "Eligible"
|
||||||
|
elif elig_text:
|
||||||
|
eligibility = elig_text
|
||||||
|
except Exception:
|
||||||
|
elig_text = getattr(self, '_eligibility_text', '')
|
||||||
|
if elig_text and "present" in elig_text.lower():
|
||||||
|
eligibility = "Eligible"
|
||||||
|
elif elig_text:
|
||||||
|
eligibility = elig_text
|
||||||
|
|
||||||
|
# Check page body for additional eligibility info
|
||||||
|
try:
|
||||||
|
body_text = self.driver.find_element(By.TAG_NAME, "body").text
|
||||||
|
if "not eligible" in body_text.lower():
|
||||||
|
eligibility = "Not Eligible"
|
||||||
|
elif "terminated" in body_text.lower():
|
||||||
|
eligibility = "Terminated"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Capture PDF via "Download summary" -> "Download PDF" button
|
||||||
|
pdfBase64 = ""
|
||||||
|
try:
|
||||||
|
existing_files = set(glob.glob(os.path.join(self.download_dir, "*")))
|
||||||
|
|
||||||
|
dl_link = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//a[@data-testid='downloadBenefitSummaryLink']"))
|
||||||
|
)
|
||||||
|
dl_link.click()
|
||||||
|
print("[DeltaIns step2] Clicked 'Download summary'")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
dl_btn = WebDriverWait(self.driver, 10).until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH,
|
||||||
|
"//button[@data-testid='downloadPdfButton']"))
|
||||||
|
)
|
||||||
|
dl_btn.click()
|
||||||
|
print("[DeltaIns step2] Clicked 'Download PDF'")
|
||||||
|
|
||||||
|
pdf_path = None
|
||||||
|
for i in range(30):
|
||||||
|
time.sleep(2)
|
||||||
|
current_files = set(glob.glob(os.path.join(self.download_dir, "*")))
|
||||||
|
new_files = current_files - existing_files
|
||||||
|
completed = [f for f in new_files
|
||||||
|
if not f.endswith(".crdownload") and not f.endswith(".tmp")]
|
||||||
|
if completed:
|
||||||
|
pdf_path = completed[0]
|
||||||
|
break
|
||||||
|
|
||||||
|
if pdf_path and os.path.exists(pdf_path):
|
||||||
|
with open(pdf_path, "rb") as f:
|
||||||
|
pdfBase64 = base64.b64encode(f.read()).decode()
|
||||||
|
print(f"[DeltaIns step2] PDF downloaded: {os.path.basename(pdf_path)} "
|
||||||
|
f"({os.path.getsize(pdf_path)} bytes), b64 len={len(pdfBase64)}")
|
||||||
|
try:
|
||||||
|
os.remove(pdf_path)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
print("[DeltaIns step2] Download PDF timed out, falling back to CDP")
|
||||||
|
cdp_result = self.driver.execute_cdp_cmd("Page.printToPDF", {
|
||||||
|
"printBackground": True,
|
||||||
|
"preferCSSPageSize": True,
|
||||||
|
"scale": 0.7,
|
||||||
|
"paperWidth": 11,
|
||||||
|
"paperHeight": 17,
|
||||||
|
})
|
||||||
|
pdfBase64 = cdp_result.get("data", "")
|
||||||
|
print(f"[DeltaIns step2] CDP fallback PDF, b64 len={len(pdfBase64)}")
|
||||||
|
|
||||||
|
# Dismiss the download modal
|
||||||
|
try:
|
||||||
|
self.driver.find_element(By.TAG_NAME, "body").send_keys(Keys.ESCAPE)
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns step2] PDF capture failed: {e}")
|
||||||
|
try:
|
||||||
|
cdp_result = self.driver.execute_cdp_cmd("Page.printToPDF", {
|
||||||
|
"printBackground": True,
|
||||||
|
"preferCSSPageSize": True,
|
||||||
|
"scale": 0.7,
|
||||||
|
"paperWidth": 11,
|
||||||
|
"paperHeight": 17,
|
||||||
|
})
|
||||||
|
pdfBase64 = cdp_result.get("data", "")
|
||||||
|
print(f"[DeltaIns step2] CDP fallback PDF, b64 len={len(pdfBase64)}")
|
||||||
|
except Exception as e2:
|
||||||
|
print(f"[DeltaIns step2] CDP fallback also failed: {e2}")
|
||||||
|
|
||||||
|
# Hide browser after completion
|
||||||
|
self._close_browser()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status": "success",
|
||||||
|
"patientName": patientName,
|
||||||
|
"eligibility": eligibility,
|
||||||
|
"pdfBase64": pdfBase64,
|
||||||
|
"extractedDob": extractedDob,
|
||||||
|
"memberId": foundMemberId,
|
||||||
|
}
|
||||||
|
|
||||||
|
print(f"[DeltaIns step2] Result: name={result['patientName']}, "
|
||||||
|
f"eligibility={result['eligibility']}, "
|
||||||
|
f"memberId={result['memberId']}")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DeltaIns step2] Exception: {e}")
|
||||||
|
self._close_browser()
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"patientName": getattr(self, '_patient_name', '') or f"{self.firstName} {self.lastName}".strip(),
|
||||||
|
"eligibility": "Unknown",
|
||||||
|
"pdfBase64": "",
|
||||||
|
"extractedDob": self._format_dob(self.dateOfBirth),
|
||||||
|
"memberId": self.memberId,
|
||||||
|
"error": str(e),
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user