feat(ddma-eligibility) - v1
This commit is contained in:
@@ -2,6 +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
|
||||||
|
SELENIUM_AGENT_BASE_URL=http://localhost:5002
|
||||||
JWT_SECRET = 'dentalsecret'
|
JWT_SECRET = 'dentalsecret'
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_USER=postgres
|
DB_USER=postgres
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zod-validation-error": "^3.4.0"
|
"zod-validation-error": "^3.4.0"
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import app from "./app";
|
import app from "./app";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
|
import http from "http";
|
||||||
|
import { initSocket } from "./socket";
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
@@ -11,7 +13,13 @@ const NODE_ENV = (
|
|||||||
const HOST = process.env.HOST || "0.0.0.0";
|
const HOST = process.env.HOST || "0.0.0.0";
|
||||||
const PORT = Number(process.env.PORT) || 5000;
|
const PORT = Number(process.env.PORT) || 5000;
|
||||||
|
|
||||||
const server = app.listen(PORT, HOST, () => {
|
// HTTP server from express app
|
||||||
|
const server = http.createServer(app);
|
||||||
|
|
||||||
|
// Initialize socket.io on this server
|
||||||
|
initSocket(server);
|
||||||
|
|
||||||
|
server.listen(PORT, HOST, () => {
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Server running in ${NODE_ENV} mode at http://${HOST}:${PORT}`
|
`✅ Server running in ${NODE_ENV} mode at http://${HOST}:${PORT}`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import patientDataExtractionRoutes from "./patientDataExtraction";
|
|||||||
import insuranceCredsRoutes from "./insuranceCreds";
|
import insuranceCredsRoutes from "./insuranceCreds";
|
||||||
import documentsRoutes from "./documents";
|
import documentsRoutes from "./documents";
|
||||||
import insuranceStatusRoutes from "./insuranceStatus";
|
import insuranceStatusRoutes from "./insuranceStatus";
|
||||||
|
import insuranceStatusDdmaRoutes from "./insuranceStatusDDMA";
|
||||||
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";
|
||||||
@@ -27,6 +28,7 @@ router.use("/claims", claimsRoutes);
|
|||||||
router.use("/insuranceCreds", insuranceCredsRoutes);
|
router.use("/insuranceCreds", insuranceCredsRoutes);
|
||||||
router.use("/documents", documentsRoutes);
|
router.use("/documents", documentsRoutes);
|
||||||
router.use("/insurance-status", insuranceStatusRoutes);
|
router.use("/insurance-status", insuranceStatusRoutes);
|
||||||
|
router.use("/insurance-status-ddma", insuranceStatusDdmaRoutes);
|
||||||
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);
|
||||||
|
|||||||
443
apps/Backend/src/routes/insuranceStatusDDMA.ts
Normal file
443
apps/Backend/src/routes/insuranceStatusDDMA.ts
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
import { Router, Request, Response } from "express";
|
||||||
|
import { storage } from "../storage";
|
||||||
|
import {
|
||||||
|
forwardToSeleniumDdmaEligibilityAgent,
|
||||||
|
forwardOtpToSeleniumDdmaAgent,
|
||||||
|
getSeleniumDdmaSessionStatus,
|
||||||
|
} from "../services/seleniumDdmaInsuranceEligibilityClient";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { emptyFolderContainingFile } from "../utils/emptyTempFolder";
|
||||||
|
import forwardToPatientDataExtractorService from "../services/patientDataExtractorService";
|
||||||
|
import {
|
||||||
|
InsertPatient,
|
||||||
|
insertPatientSchema,
|
||||||
|
} from "../../../../packages/db/types/patient-types";
|
||||||
|
import { io } from "../socket";
|
||||||
|
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
/** Job context stored in memory by sessionId */
|
||||||
|
interface DdmaJobContext {
|
||||||
|
userId: number;
|
||||||
|
insuranceEligibilityData: any; // parsed, enriched (includes username/password)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ddmaJobs: Record<string, DdmaJobContext> = {};
|
||||||
|
|
||||||
|
/** 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure patient exists for given insuranceId.
|
||||||
|
*/
|
||||||
|
async function createOrUpdatePatientByInsuranceId(options: {
|
||||||
|
insuranceId: string;
|
||||||
|
firstName?: string | null;
|
||||||
|
lastName?: string | null;
|
||||||
|
dob?: string | Date | null;
|
||||||
|
userId: number;
|
||||||
|
}) {
|
||||||
|
const { insuranceId, firstName, lastName, dob, userId } = 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 {
|
||||||
|
const createPayload: any = {
|
||||||
|
firstName: incomingFirst,
|
||||||
|
lastName: incomingLast,
|
||||||
|
dateOfBirth: dob,
|
||||||
|
gender: "",
|
||||||
|
phone: "",
|
||||||
|
userId,
|
||||||
|
insuranceId,
|
||||||
|
};
|
||||||
|
let patientData: InsertPatient;
|
||||||
|
try {
|
||||||
|
patientData = insertPatientSchema.parse(createPayload);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
"Failed to validate patient payload in ddma insurance flow:",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
const safePayload = { ...createPayload };
|
||||||
|
delete (safePayload as any).dateOfBirth;
|
||||||
|
patientData = insertPatientSchema.parse(safePayload);
|
||||||
|
}
|
||||||
|
await storage.createPatient(patientData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When Selenium finishes for a given sessionId, run your patient + PDF pipeline,
|
||||||
|
* and return the final API response shape.
|
||||||
|
*/
|
||||||
|
async function handleDdmaCompletedJob(
|
||||||
|
sessionId: string,
|
||||||
|
job: DdmaJobContext,
|
||||||
|
seleniumResult: any
|
||||||
|
) {
|
||||||
|
let createdPdfFileId: number | null = null;
|
||||||
|
const outputResult: any = {};
|
||||||
|
const extracted: any = {};
|
||||||
|
|
||||||
|
const insuranceEligibilityData = job.insuranceEligibilityData;
|
||||||
|
|
||||||
|
// 1) Extract name from PDF if available
|
||||||
|
if (
|
||||||
|
seleniumResult?.pdf_path &&
|
||||||
|
typeof seleniumResult.pdf_path === "string" &&
|
||||||
|
seleniumResult.pdf_path.endsWith(".pdf")
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const pdfPath = seleniumResult.pdf_path;
|
||||||
|
const pdfBuffer = await fs.readFile(pdfPath);
|
||||||
|
|
||||||
|
const extraction = await forwardToPatientDataExtractorService({
|
||||||
|
buffer: pdfBuffer,
|
||||||
|
originalname: path.basename(pdfPath),
|
||||||
|
mimetype: "application/pdf",
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
if (extraction.name) {
|
||||||
|
const parts = splitName(extraction.name);
|
||||||
|
extracted.firstName = parts.firstName;
|
||||||
|
extracted.lastName = parts.lastName;
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
outputResult.extractionError =
|
||||||
|
err?.message ?? "Patient data extraction failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Create or update patient
|
||||||
|
const insuranceId = String(insuranceEligibilityData.memberId ?? "").trim();
|
||||||
|
if (!insuranceId) {
|
||||||
|
throw new Error("Missing memberId for ddma job");
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferFirst = extracted.firstName;
|
||||||
|
const preferLast = extracted.lastName;
|
||||||
|
|
||||||
|
await createOrUpdatePatientByInsuranceId({
|
||||||
|
insuranceId,
|
||||||
|
firstName: preferFirst,
|
||||||
|
lastName: preferLast,
|
||||||
|
dob: insuranceEligibilityData.dateOfBirth,
|
||||||
|
userId: job.userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Update patient status + PDF upload
|
||||||
|
const patient = await storage.getPatientByInsuranceId(
|
||||||
|
insuranceEligibilityData.memberId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (patient && patient.id !== undefined) {
|
||||||
|
const newStatus =
|
||||||
|
seleniumResult.eligibility === "Y" ? "ACTIVE" : "INACTIVE";
|
||||||
|
await storage.updatePatient(patient.id, { status: newStatus });
|
||||||
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
||||||
|
|
||||||
|
if (
|
||||||
|
seleniumResult.pdf_path &&
|
||||||
|
typeof seleniumResult.pdf_path === "string" &&
|
||||||
|
seleniumResult.pdf_path.endsWith(".pdf")
|
||||||
|
) {
|
||||||
|
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
||||||
|
|
||||||
|
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(seleniumResult.pdf_path),
|
||||||
|
pdfBuffer
|
||||||
|
);
|
||||||
|
if (created && typeof created === "object" && "id" in created) {
|
||||||
|
createdPdfFileId = Number(created.id);
|
||||||
|
}
|
||||||
|
outputResult.pdfUploadStatus = `PDF saved to group: ${group.title}`;
|
||||||
|
} else {
|
||||||
|
outputResult.pdfUploadStatus =
|
||||||
|
"No valid PDF path provided by Selenium, Couldn't upload pdf to server.";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outputResult.patientUpdateStatus =
|
||||||
|
"Patient not found or missing ID; no update performed";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Cleanup PDF temp folder
|
||||||
|
try {
|
||||||
|
if (seleniumResult && seleniumResult.pdf_path) {
|
||||||
|
await emptyFolderContainingFile(seleniumResult.pdf_path);
|
||||||
|
}
|
||||||
|
} catch (cleanupErr) {
|
||||||
|
console.error(
|
||||||
|
`[ddma-eligibility cleanup failed for ${seleniumResult?.pdf_path}]`,
|
||||||
|
cleanupErr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
patientUpdateStatus: outputResult.patientUpdateStatus,
|
||||||
|
pdfUploadStatus: outputResult.pdfUploadStatus,
|
||||||
|
pdfFileId: createdPdfFileId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls Python agent for session status and emits socket events:
|
||||||
|
* - 'selenium:otp_required' when waiting_for_otp
|
||||||
|
* - 'selenium:session_update' when completed/error
|
||||||
|
*/
|
||||||
|
async function pollAgentSessionAndProcess(
|
||||||
|
sessionId: string,
|
||||||
|
socketId?: string
|
||||||
|
) {
|
||||||
|
const maxAttempts = 300; // ~5 minutes @ 1s
|
||||||
|
const delayMs = 1000;
|
||||||
|
|
||||||
|
const job = ddmaJobs[sessionId];
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
const st = await getSeleniumDdmaSessionStatus(sessionId);
|
||||||
|
const status = st?.status;
|
||||||
|
|
||||||
|
if (status === "waiting_for_otp") {
|
||||||
|
if (socketId && io && io.sockets.sockets.get(socketId)) {
|
||||||
|
io.to(socketId).emit("selenium:otp_required", {
|
||||||
|
session_id: sessionId,
|
||||||
|
message: "OTP required. Please enter the OTP.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// once waiting_for_otp, we stop polling here; OTP flow continues separately
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "completed") {
|
||||||
|
// run DB + PDF pipeline
|
||||||
|
let finalResult: any = null;
|
||||||
|
if (job && st.result) {
|
||||||
|
try {
|
||||||
|
finalResult = await handleDdmaCompletedJob(
|
||||||
|
sessionId,
|
||||||
|
job,
|
||||||
|
st.result
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
finalResult = {
|
||||||
|
error: "Failed to process ddma completed job",
|
||||||
|
detail: err?.message ?? String(err),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (socketId && io && io.sockets.sockets.get(socketId)) {
|
||||||
|
io.to(socketId).emit("selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "completed",
|
||||||
|
rawSelenium: st.result,
|
||||||
|
final: finalResult,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
delete ddmaJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "error" || status === "not_found") {
|
||||||
|
if (socketId && io && io.sockets.sockets.get(socketId)) {
|
||||||
|
io.to(socketId).emit("selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status,
|
||||||
|
message: st?.message || "Selenium session error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
delete ddmaJobs[sessionId];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// swallow transient errors and keep polling
|
||||||
|
console.warn("pollAgentSessionAndProcess error", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, delayMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: timeout
|
||||||
|
if (socketId && io && io.sockets.sockets.get(socketId)) {
|
||||||
|
io.to(socketId).emit("selenium:session_update", {
|
||||||
|
session_id: sessionId,
|
||||||
|
status: "error",
|
||||||
|
message: "Polling timeout while waiting for selenium session",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /ddma-eligibility
|
||||||
|
* Starts DDMA eligibility Selenium job.
|
||||||
|
* Expects:
|
||||||
|
* - req.body.data: stringified JSON like your existing /eligibility-check
|
||||||
|
* - req.body.socketId: socket.io client id
|
||||||
|
*/
|
||||||
|
router.post(
|
||||||
|
"/ddma-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,
|
||||||
|
rawData.insuranceSiteKey
|
||||||
|
);
|
||||||
|
if (!credentials) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error:
|
||||||
|
"No insurance credentials found for this provider, Kindly Update this at Settings Page.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const enrichedData = {
|
||||||
|
...rawData,
|
||||||
|
massddmaUsername: credentials.username,
|
||||||
|
massddmaPassword: credentials.password,
|
||||||
|
};
|
||||||
|
|
||||||
|
const socketId: string | undefined = req.body.socketId;
|
||||||
|
|
||||||
|
const agentResp = await forwardToSeleniumDdmaEligibilityAgent(
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Save job context
|
||||||
|
ddmaJobs[sessionId] = {
|
||||||
|
userId: req.user.id,
|
||||||
|
insuranceEligibilityData: enrichedData,
|
||||||
|
};
|
||||||
|
|
||||||
|
// start polling in background to notify client via socket and process job
|
||||||
|
pollAgentSessionAndProcess(sessionId, socketId).catch((e) =>
|
||||||
|
console.warn("pollAgentSessionAndProcess failed", e)
|
||||||
|
);
|
||||||
|
|
||||||
|
// reply immediately with started status
|
||||||
|
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 ddma selenium agent",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /selenium/submit-otp
|
||||||
|
* Body: { session_id, otp, socketId? }
|
||||||
|
* Forwards OTP to Python agent and optionally notifies client socket.
|
||||||
|
*/
|
||||||
|
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 forwardOtpToSeleniumDdmaAgent(sessionId, otp);
|
||||||
|
|
||||||
|
// notify socket that OTP was accepted (if socketId present)
|
||||||
|
try {
|
||||||
|
const { io } = require("../socket");
|
||||||
|
if (socketId && io && io.sockets.sockets.get(socketId)) {
|
||||||
|
io.to(socketId).emit("selenium:otp_submitted", {
|
||||||
|
session_id: sessionId,
|
||||||
|
result: r,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (emitErr) {
|
||||||
|
console.warn("Failed to emit selenium:otp_submitted", emitErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import dotenv from "dotenv";
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export interface SeleniumPayload {
|
||||||
|
data: any;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SELENIUM_AGENT_BASE =
|
||||||
|
process.env.SELENIUM_AGENT_BASE_URL;
|
||||||
|
|
||||||
|
export async function forwardToSeleniumDdmaEligibilityAgent(
|
||||||
|
insuranceEligibilityData: any,
|
||||||
|
): Promise<any> {
|
||||||
|
const payload: SeleniumPayload = {
|
||||||
|
data: insuranceEligibilityData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = `${SELENIUM_AGENT_BASE}/ddma-eligibility`;
|
||||||
|
console.log(url)
|
||||||
|
const result = await axios.post(
|
||||||
|
`${SELENIUM_AGENT_BASE}/ddma-eligibility`,
|
||||||
|
payload,
|
||||||
|
{ timeout: 5 * 60 * 1000 }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result || !result.data) {
|
||||||
|
throw new Error("Empty response from selenium agent");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.data.status === "error") {
|
||||||
|
const errorMsg =
|
||||||
|
typeof result.data.message === "string"
|
||||||
|
? result.data.message
|
||||||
|
: result.data.message?.msg || "Selenium agent error";
|
||||||
|
throw new Error(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data; // { status: "started", session_id }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function forwardOtpToSeleniumDdmaAgent(
|
||||||
|
sessionId: string,
|
||||||
|
otp: string
|
||||||
|
): Promise<any> {
|
||||||
|
const result = await axios.post(`${SELENIUM_AGENT_BASE}/submit-otp`, {
|
||||||
|
session_id: sessionId,
|
||||||
|
otp,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result || !result.data) throw new Error("Empty OTP response");
|
||||||
|
if (result.data.status === "error") {
|
||||||
|
const message =
|
||||||
|
typeof result.data.message === "string"
|
||||||
|
? result.data.message
|
||||||
|
: JSON.stringify(result.data);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSeleniumDdmaSessionStatus(
|
||||||
|
sessionId: string
|
||||||
|
): Promise<any> {
|
||||||
|
const result = await axios.get(
|
||||||
|
`${SELENIUM_AGENT_BASE}/session/${sessionId}/status`
|
||||||
|
);
|
||||||
|
if (!result || !result.data) throw new Error("Empty session status");
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
53
apps/Backend/src/socket.ts
Normal file
53
apps/Backend/src/socket.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Server as HttpServer } from "http";
|
||||||
|
import { Server, Socket } from "socket.io";
|
||||||
|
|
||||||
|
let io: Server | null = null;
|
||||||
|
|
||||||
|
export function initSocket(server: HttpServer) {
|
||||||
|
const NODE_ENV = (
|
||||||
|
process.env.NODE_ENV ||
|
||||||
|
process.env.ENV ||
|
||||||
|
"development"
|
||||||
|
).toLowerCase();
|
||||||
|
|
||||||
|
const rawFrontendUrls =
|
||||||
|
process.env.FRONTEND_URLS || process.env.FRONTEND_URL || "";
|
||||||
|
const FRONTEND_URLS = rawFrontendUrls
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
// In dev: allow all origins
|
||||||
|
// In prod: restrict to FRONTEND_URLS if provided
|
||||||
|
const corsOrigin =
|
||||||
|
NODE_ENV !== "production"
|
||||||
|
? true
|
||||||
|
: FRONTEND_URLS.length > 0
|
||||||
|
? FRONTEND_URLS
|
||||||
|
: false; // no origins allowed if none configured in prod
|
||||||
|
|
||||||
|
io = new Server(server, {
|
||||||
|
cors: {
|
||||||
|
origin: corsOrigin,
|
||||||
|
methods: ["GET", "POST"],
|
||||||
|
credentials: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
io.on("connection", (socket: Socket) => {
|
||||||
|
console.log("🔌 Socket connected:", socket.id);
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("🔌 Socket disconnected:", socket.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optional: log low-level engine errors for debugging
|
||||||
|
io.engine.on("connection_error", (err) => {
|
||||||
|
console.error("Socket engine connection_error:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return io;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { io };
|
||||||
@@ -76,6 +76,7 @@
|
|||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
// Use Vite env (set VITE_BACKEND_URL in your frontend .env)
|
||||||
|
const SOCKET_URL =
|
||||||
|
import.meta.env.VITE_API_BASE_URL_BACKEND ||
|
||||||
|
(typeof window !== "undefined" ? window.location.origin : "");
|
||||||
|
|
||||||
|
// ---------- OTP Modal component ----------
|
||||||
|
interface DdmaOtpModalProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onSubmit: (otp: string) => Promise<void> | void;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DdmaOtpModal({ open, onClose, onSubmit, isSubmitting }: DdmaOtpModalProps) {
|
||||||
|
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 by the Delta Dental MA portal
|
||||||
|
to complete this eligibility check.
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="ddma-otp">OTP</Label>
|
||||||
|
<Input
|
||||||
|
id="ddma-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 DDMA Eligibility button component ----------
|
||||||
|
interface DdmaEligibilityButtonProps {
|
||||||
|
memberId: string;
|
||||||
|
dateOfBirth: Date | null;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
isFormIncomplete: boolean;
|
||||||
|
/** Called when backend has finished and PDF is ready */
|
||||||
|
onPdfReady: (pdfId: number, fallbackFilename: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DdmaEligibilityButton({
|
||||||
|
memberId,
|
||||||
|
dateOfBirth,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
isFormIncomplete,
|
||||||
|
onPdfReady,
|
||||||
|
}: DdmaEligibilityButtonProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Clean up socket on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.removeAllListeners();
|
||||||
|
socketRef.current.disconnect();
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
|
connectingRef.current = null;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Lazy socket setup: called only when we actually need it (first click)
|
||||||
|
const ensureSocketConnected = async () => {
|
||||||
|
// If already connected, nothing to do
|
||||||
|
if (socketRef.current && socketRef.current.connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a connection is in progress, reuse that promise
|
||||||
|
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", () => {
|
||||||
|
console.log("DDMA socket connected:", socket.id);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (err) => {
|
||||||
|
console.error("DDMA socket connect_error:", err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("disconnect", () => {
|
||||||
|
console.log("DDMA socket disconnected");
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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 DDMA eligibility. Please enter the OTP.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// OTP submitted (optional UX)
|
||||||
|
socket.on("selenium:otp_submitted", (payload: any) => {
|
||||||
|
if (!payload?.session_id || payload.session_id !== sessionId) return;
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "OTP submitted. Finishing DDMA eligibility check...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Session update
|
||||||
|
socket.on("selenium:session_update", (payload: any) => {
|
||||||
|
const { session_id, status, final } = payload || {};
|
||||||
|
if (!session_id || session_id !== sessionId) return;
|
||||||
|
|
||||||
|
if (status === "completed") {
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "success",
|
||||||
|
message:
|
||||||
|
"DDMA eligibility updated and PDF attached to patient documents.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "DDMA 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_ddma_${memberId}.pdf`;
|
||||||
|
onPdfReady(Number(pdfId), filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSessionId(null);
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
} else if (status === "error") {
|
||||||
|
const msg =
|
||||||
|
payload?.message ||
|
||||||
|
final?.error ||
|
||||||
|
"DDMA eligibility session failed.";
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: msg,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "DDMA selenium error",
|
||||||
|
description: msg,
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setSessionId(null);
|
||||||
|
setOtpModalOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.invalidateQueries({ queryKey: QK_PATIENTS_BASE });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
connectingRef.current = promise;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await promise;
|
||||||
|
} finally {
|
||||||
|
// Once resolved or rejected, allow future attempts if needed
|
||||||
|
connectingRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDdmaEligibility = async () => {
|
||||||
|
if (!memberId || !dateOfBirth) {
|
||||||
|
toast({
|
||||||
|
title: "Missing fields",
|
||||||
|
description: "Member ID and Date of Birth are required.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDob = dateOfBirth ? formatLocalDate(dateOfBirth) : "";
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
memberId,
|
||||||
|
dateOfBirth: formattedDob,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
insuranceSiteKey: "DDMA", // make sure this matches backend credential key
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsStarting(true);
|
||||||
|
|
||||||
|
// 1) Ensure socket is connected (lazy)
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "Opening realtime channel for DDMA eligibility...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
await ensureSocketConnected();
|
||||||
|
|
||||||
|
const socket = socketRef.current;
|
||||||
|
if (!socket || !socket.connected) {
|
||||||
|
throw new Error("Socket connection failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
const socketId = socket.id;
|
||||||
|
|
||||||
|
// 2) Start the selenium job via backend
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message: "Starting DDMA eligibility check via selenium...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/insurance-status-ddma/ddma-eligibility",
|
||||||
|
{
|
||||||
|
data: JSON.stringify(payload),
|
||||||
|
socketId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (!response.ok || result.error) {
|
||||||
|
throw new Error(result.error || "DDMA selenium start failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === "started" && result.session_id) {
|
||||||
|
setSessionId(result.session_id as string);
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "pending",
|
||||||
|
message:
|
||||||
|
"DDMA eligibility job started. Waiting for OTP or final result...",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// fallback if backend returns immediate result
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "success",
|
||||||
|
message: "DDMA eligibility completed.",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error("startDdmaEligibility error:", err);
|
||||||
|
dispatch(
|
||||||
|
setTaskStatus({
|
||||||
|
status: "error",
|
||||||
|
message: err?.message || "Failed to start DDMA eligibility",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
toast({
|
||||||
|
title: "DDMA selenium error",
|
||||||
|
description: err?.message || "Failed to start DDMA 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 DDMA session or socket is not ready.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsSubmittingOtp(true);
|
||||||
|
const resp = await apiRequest(
|
||||||
|
"POST",
|
||||||
|
"/api/insurance-status-ddma/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");
|
||||||
|
}
|
||||||
|
|
||||||
|
// from here we rely on websocket events (otp_submitted + session_update)
|
||||||
|
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="default"
|
||||||
|
disabled={isFormIncomplete || isStarting}
|
||||||
|
onClick={startDdmaEligibility}
|
||||||
|
>
|
||||||
|
{isStarting ? (
|
||||||
|
<>
|
||||||
|
<LoaderCircleIcon className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
Processing...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-4 w-4 mr-2" />
|
||||||
|
Delta MA Eligibility
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<DdmaOtpModal
|
||||||
|
open={otpModalOpen}
|
||||||
|
onClose={() => setOtpModalOpen(false)}
|
||||||
|
onSubmit={handleSubmitOtp}
|
||||||
|
isSubmitting={isSubmittingOtp}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import { DateInput } from "@/components/ui/dateInput";
|
|||||||
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
import { QK_PATIENTS_BASE } from "@/components/patients/patient-table";
|
||||||
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
import { PdfPreviewModal } from "@/components/insurance-status/pdf-preview-modal";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
|
import { DdmaEligibilityButton } from "@/components/insurance-status/ddma-buton-modal";
|
||||||
|
|
||||||
export default function InsuranceStatusPage() {
|
export default function InsuranceStatusPage() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@@ -574,19 +575,25 @@ export default function InsuranceStatusPage() {
|
|||||||
{/* TEMP PROVIDER BUTTONS */}
|
{/* TEMP PROVIDER BUTTONS */}
|
||||||
<div className="space-y-4 mt-6">
|
<div className="space-y-4 mt-6">
|
||||||
<h3 className="text-sm font-medium text-muted-foreground">
|
<h3 className="text-sm font-medium text-muted-foreground">
|
||||||
Other provider checks (not working)
|
Other provider checks
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{/* Row 1 */}
|
{/* Row 1 */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<Button
|
<DdmaEligibilityButton
|
||||||
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}
|
||||||
Delta MA
|
onPdfReady={(pdfId, fallbackFilename) => {
|
||||||
</Button>
|
setPreviewPdfId(pdfId);
|
||||||
|
setPreviewFallbackFilename(
|
||||||
|
fallbackFilename ?? `eligibility_ddma_${memberId}.pdf`
|
||||||
|
);
|
||||||
|
setPreviewOpen(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
import uvicorn
|
import uvicorn
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -7,6 +7,8 @@ from selenium_eligibilityCheckWorker import AutomationMassHealthEligibilityCheck
|
|||||||
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
from selenium_claimStatusCheckWorker import AutomationMassHealthClaimStatusCheck
|
||||||
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
from selenium_preAuthWorker import AutomationMassHealthPreAuth
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
import helpers_ddma_eligibility as hddma
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
@@ -137,6 +139,79 @@ async def start_workflow(request: Request):
|
|||||||
async with lock:
|
async with lock:
|
||||||
active_jobs -= 1
|
active_jobs -= 1
|
||||||
|
|
||||||
|
# Endpoint:5 - DDMA eligibility (background, OTP)
|
||||||
|
|
||||||
|
async def _ddma_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 DDMA flow via helpers.start_ddma_run.
|
||||||
|
"""
|
||||||
|
global active_jobs, waiting_jobs
|
||||||
|
async with semaphore:
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs -= 1
|
||||||
|
active_jobs += 1
|
||||||
|
try:
|
||||||
|
await hddma.start_ddma_run(sid, data, url)
|
||||||
|
finally:
|
||||||
|
async with lock:
|
||||||
|
active_jobs -= 1
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/ddma-eligibility")
|
||||||
|
async def ddma_eligibility(request: Request):
|
||||||
|
"""
|
||||||
|
Starts a DDMA 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", {})
|
||||||
|
|
||||||
|
# create session
|
||||||
|
sid = hddma.make_session_entry()
|
||||||
|
hddma.sessions[sid]["type"] = "ddma_eligibility"
|
||||||
|
hddma.sessions[sid]["last_activity"] = time.time()
|
||||||
|
|
||||||
|
async with lock:
|
||||||
|
waiting_jobs += 1
|
||||||
|
|
||||||
|
# run in background (queued under semaphore)
|
||||||
|
asyncio.create_task(_ddma_worker_wrapper(sid, data, url="https://providers.deltadentalma.com/onboarding/start/"))
|
||||||
|
|
||||||
|
return {"status": "started", "session_id": sid}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/submit-otp")
|
||||||
|
async def submit_otp(request: Request):
|
||||||
|
"""
|
||||||
|
Body: { "session_id": "<sid>", "otp": "123456" }
|
||||||
|
Node / frontend call this when user provides OTP.
|
||||||
|
"""
|
||||||
|
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 = hddma.submit_otp(sid, otp)
|
||||||
|
if res.get("status") == "error":
|
||||||
|
raise HTTPException(status_code=400, detail=res.get("message"))
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/session/{sid}/status")
|
||||||
|
async def session_status(sid: str):
|
||||||
|
s = hddma.get_session_status(sid)
|
||||||
|
if s.get("status") == "not_found":
|
||||||
|
raise HTTPException(status_code=404, detail="session not found")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
# ✅ Status Endpoint
|
# ✅ Status Endpoint
|
||||||
@app.get("/status")
|
@app.get("/status")
|
||||||
async def get_status():
|
async def get_status():
|
||||||
|
|||||||
212
apps/SeleniumService/helpers_ddma_eligibility.py
Normal file
212
apps/SeleniumService/helpers_ddma_eligibility.py
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
from typing import Dict, Any
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
|
|
||||||
|
from selenium_DDMA_eligibilityCheckWorker import AutomationDeltaDentalMAEligibilityCheck
|
||||||
|
|
||||||
|
# In-memory session store
|
||||||
|
sessions: Dict[str, Dict[str, Any]] = {}
|
||||||
|
|
||||||
|
SESSION_OTP_TIMEOUT = int(os.getenv("SESSION_OTP_TIMEOUT", "120")) # seconds
|
||||||
|
|
||||||
|
|
||||||
|
def make_session_entry() -> str:
|
||||||
|
"""Create a new session entry and return its ID."""
|
||||||
|
import uuid
|
||||||
|
sid = str(uuid.uuid4())
|
||||||
|
sessions[sid] = {
|
||||||
|
"status": "created", # created -> running -> waiting_for_otp -> otp_submitted -> completed / error
|
||||||
|
"created_at": time.time(),
|
||||||
|
"last_activity": time.time(),
|
||||||
|
"bot": None, # worker instance
|
||||||
|
"driver": None, # selenium webdriver
|
||||||
|
"otp_event": asyncio.Event(),
|
||||||
|
"otp_value": None,
|
||||||
|
"result": None,
|
||||||
|
"message": None,
|
||||||
|
"type": None,
|
||||||
|
}
|
||||||
|
return sid
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_session(sid: str):
|
||||||
|
"""Close driver (if any) and remove session entry."""
|
||||||
|
s = sessions.get(sid)
|
||||||
|
if not s:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
driver = s.get("driver")
|
||||||
|
if driver:
|
||||||
|
try:
|
||||||
|
driver.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
sessions.pop(sid, None)
|
||||||
|
print(f"[helpers] cleaned session {sid}")
|
||||||
|
|
||||||
|
|
||||||
|
async def _remove_session_later(sid: str, delay: int = 20):
|
||||||
|
await asyncio.sleep(delay)
|
||||||
|
await cleanup_session(sid)
|
||||||
|
|
||||||
|
|
||||||
|
async def start_ddma_run(sid: str, data: dict, url: str):
|
||||||
|
"""
|
||||||
|
Run the DDMA workflow for a session (WITHOUT managing semaphore/counters).
|
||||||
|
Called by agent.py inside a wrapper that handles queue/counters.
|
||||||
|
"""
|
||||||
|
s = sessions.get(sid)
|
||||||
|
if not s:
|
||||||
|
return {"status": "error", "message": "session not found"}
|
||||||
|
|
||||||
|
s["status"] = "running"
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
bot = AutomationDeltaDentalMAEligibilityCheck({"data": data})
|
||||||
|
bot.config_driver()
|
||||||
|
|
||||||
|
s["bot"] = bot
|
||||||
|
s["driver"] = bot.driver
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
|
# Navigate to login URL
|
||||||
|
try:
|
||||||
|
if not url:
|
||||||
|
raise ValueError("URL not provided for DDMA run")
|
||||||
|
bot.driver.maximize_window()
|
||||||
|
bot.driver.get(url)
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"Navigation failed: {e}"
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
# Login
|
||||||
|
login_result = bot.login()
|
||||||
|
|
||||||
|
# OTP required path
|
||||||
|
if isinstance(login_result, str) and login_result == "OTP_REQUIRED":
|
||||||
|
s["status"] = "waiting_for_otp"
|
||||||
|
s["message"] = "OTP required for login"
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(s["otp_event"].wait(), timeout=SESSION_OTP_TIMEOUT)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = "OTP timeout"
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": "OTP not provided in time"}
|
||||||
|
|
||||||
|
otp_value = s.get("otp_value")
|
||||||
|
if not otp_value:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = "OTP missing after event"
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": "OTP missing after event"}
|
||||||
|
|
||||||
|
# Submit OTP in the same Selenium window
|
||||||
|
try:
|
||||||
|
driver = s["driver"]
|
||||||
|
wait = WebDriverWait(driver, 30)
|
||||||
|
|
||||||
|
otp_input = wait.until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//input[contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code')]")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
otp_input.clear()
|
||||||
|
otp_input.send_keys(otp_value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
submit_btn = wait.until(
|
||||||
|
EC.element_to_be_clickable(
|
||||||
|
(By.XPATH, "//button[@type='button' and @aria-label='Verify']")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
submit_btn.click()
|
||||||
|
except Exception:
|
||||||
|
otp_input.send_keys("\n")
|
||||||
|
|
||||||
|
s["status"] = "otp_submitted"
|
||||||
|
s["last_activity"] = time.time()
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
except Exception as e:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"Failed to submit OTP into page: {e}"
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
elif isinstance(login_result, str) and login_result.startswith("ERROR"):
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = login_result
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": login_result}
|
||||||
|
|
||||||
|
# Step 1
|
||||||
|
step1_result = bot.step1()
|
||||||
|
if isinstance(step1_result, str) and step1_result.startswith("ERROR"):
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = step1_result
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": step1_result}
|
||||||
|
|
||||||
|
# Step 2 (PDF)
|
||||||
|
step2_result = bot.step2()
|
||||||
|
if isinstance(step2_result, dict) and step2_result.get("status") == "success":
|
||||||
|
s["status"] = "completed"
|
||||||
|
s["result"] = step2_result
|
||||||
|
s["message"] = "completed"
|
||||||
|
asyncio.create_task(_remove_session_later(sid, 30))
|
||||||
|
return step2_result
|
||||||
|
else:
|
||||||
|
s["status"] = "error"
|
||||||
|
if isinstance(step2_result, dict):
|
||||||
|
s["message"] = step2_result.get("message", "unknown error")
|
||||||
|
else:
|
||||||
|
s["message"] = str(step2_result)
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
s["status"] = "error"
|
||||||
|
s["message"] = f"worker exception: {e}"
|
||||||
|
await cleanup_session(sid)
|
||||||
|
return {"status": "error", "message": s["message"]}
|
||||||
|
|
||||||
|
|
||||||
|
def submit_otp(sid: str, otp: str) -> Dict[str, Any]:
|
||||||
|
"""Set OTP for a session and wake waiting runner."""
|
||||||
|
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") == "completed" else None,
|
||||||
|
}
|
||||||
272
apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py
Normal file
272
apps/SeleniumService/selenium_DDMA_eligibilityCheckWorker.py
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
from selenium import webdriver
|
||||||
|
from selenium.common import TimeoutException
|
||||||
|
from selenium.webdriver.chrome.service import Service
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
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 shutil
|
||||||
|
import stat
|
||||||
|
|
||||||
|
class AutomationDeltaDentalMAEligibilityCheck:
|
||||||
|
def __init__(self, data):
|
||||||
|
self.headless = False
|
||||||
|
self.driver = None
|
||||||
|
|
||||||
|
self.data = data.get("data", {}) if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
# Flatten values for convenience
|
||||||
|
self.memberId = self.data.get("memberId", "")
|
||||||
|
self.dateOfBirth = self.data.get("dateOfBirth", "")
|
||||||
|
self.massddma_username = self.data.get("massddmaUsername", "")
|
||||||
|
self.massddma_password = self.data.get("massddmaPassword", "")
|
||||||
|
|
||||||
|
self.download_dir = os.path.abspath("seleniumDownloads")
|
||||||
|
os.makedirs(self.download_dir, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def config_driver(self):
|
||||||
|
options = webdriver.ChromeOptions()
|
||||||
|
if self.headless:
|
||||||
|
options.add_argument("--headless")
|
||||||
|
|
||||||
|
# Add PDF download preferences
|
||||||
|
prefs = {
|
||||||
|
"download.default_directory": self.download_dir,
|
||||||
|
"plugins.always_open_pdf_externally": True,
|
||||||
|
"download.prompt_for_download": False,
|
||||||
|
"download.directory_upgrade": True
|
||||||
|
}
|
||||||
|
options.add_experimental_option("prefs", prefs)
|
||||||
|
|
||||||
|
s = Service(ChromeDriverManager().install())
|
||||||
|
driver = webdriver.Chrome(service=s, options=options)
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def login(self):
|
||||||
|
"""
|
||||||
|
Attempts login and detects OTP.
|
||||||
|
Returns:
|
||||||
|
- "Success" -> logged in
|
||||||
|
- "OTP_REQUIRED" -> page requires OTP (we do NOT block here)
|
||||||
|
- "ERROR:..." -> error occurred
|
||||||
|
"""
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
try:
|
||||||
|
email_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='username' and @type='text']")))
|
||||||
|
email_field.clear()
|
||||||
|
email_field.send_keys(self.massddma_username)
|
||||||
|
|
||||||
|
password_field = wait.until(EC.presence_of_element_located((By.XPATH, "//input[@name='password' and @type='password']")))
|
||||||
|
password_field.clear()
|
||||||
|
password_field.send_keys(self.massddma_password)
|
||||||
|
|
||||||
|
# Click Remember me checkbox
|
||||||
|
remember_me_checkbox = wait.until(EC.element_to_be_clickable(
|
||||||
|
(By.XPATH, "//label[.//span[contains(text(),'Remember me')]]")
|
||||||
|
))
|
||||||
|
remember_me_checkbox.click()
|
||||||
|
|
||||||
|
login_button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[@type='button' and @aria-label='Sign in']")))
|
||||||
|
login_button.click()
|
||||||
|
|
||||||
|
|
||||||
|
# 1) Detect OTP presence (adjust the XPath/attributes to the actual site)
|
||||||
|
try:
|
||||||
|
otp_candidate = WebDriverWait(self.driver, 30).until(
|
||||||
|
EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//input[contains(@aria-lable,'Verification code') or contains(@placeholder,'Enter your verification code')]")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if otp_candidate:
|
||||||
|
print("[DDMA worker] OTP input detected -> OTP_REQUIRED")
|
||||||
|
return "OTP_REQUIRED"
|
||||||
|
except TimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 2) Detect successful login by presence of a known post-login element
|
||||||
|
try:
|
||||||
|
logged_in_el = WebDriverWait(self.driver, 5).until(
|
||||||
|
EC.presence_of_element_located((By.XPATH, "//a[text()='Member Eligibility' or contains(., 'Member Eligibility')]"))
|
||||||
|
)
|
||||||
|
if logged_in_el:
|
||||||
|
return "Success"
|
||||||
|
except TimeoutException:
|
||||||
|
# last chance: see if URL changed
|
||||||
|
if "dashboard" in self.driver.current_url or "providers" in self.driver.current_url:
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
return "ERROR:LOGIN FAILED - unable to detect success or OTP"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DDMA worker] login exception: {e}")
|
||||||
|
return f"ERROR:LOGIN FAILED: {e}"
|
||||||
|
|
||||||
|
def step1(self):
|
||||||
|
wait = WebDriverWait(self.driver, 30)
|
||||||
|
|
||||||
|
try:
|
||||||
|
eligibility_link = wait.until(
|
||||||
|
EC.element_to_be_clickable((By.XPATH, "//a[text()='Member Eligibility']"))
|
||||||
|
)
|
||||||
|
eligibility_link.click()
|
||||||
|
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Fill Member ID
|
||||||
|
member_id_input = wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text1"]')))
|
||||||
|
member_id_input.clear()
|
||||||
|
member_id_input.send_keys(self.memberId)
|
||||||
|
|
||||||
|
# Fill DOB parts
|
||||||
|
try:
|
||||||
|
dob_parts = self.dateOfBirth.split("-")
|
||||||
|
year = dob_parts[0] # "1964"
|
||||||
|
month = dob_parts[1].zfill(2) # "04"
|
||||||
|
day = dob_parts[2].zfill(2) # "17"
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing DOB: {e}")
|
||||||
|
return "ERROR: PARSING DOB"
|
||||||
|
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text2"]'))).send_keys(month)
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text3"]'))).send_keys(day)
|
||||||
|
wait.until(EC.presence_of_element_located((By.XPATH, '//*[@id="Text4"]'))).send_keys(year)
|
||||||
|
|
||||||
|
# Click Continue button
|
||||||
|
continue_btn = wait.until(EC.element_to_be_clickable((By.XPATH, '//input[@type="submit" and @value="Add Member"]')))
|
||||||
|
continue_btn.click()
|
||||||
|
|
||||||
|
# Check for error message
|
||||||
|
try:
|
||||||
|
error_msg = WebDriverWait(self.driver, 5).until(EC.presence_of_element_located(
|
||||||
|
(By.XPATH, "//td[@class='text_err_msg' and contains(text(), 'Invalid Medicaid ID or Date of Birth')]")
|
||||||
|
))
|
||||||
|
if error_msg:
|
||||||
|
print("Error: Invalid Member ID or Date of Birth.")
|
||||||
|
return "ERROR: INVALID MEMBERID OR DOB"
|
||||||
|
except TimeoutException:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return "Success"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error while step1 i.e Cheking the MemberId and DOB in: {e}")
|
||||||
|
return "ERROR:STEP1"
|
||||||
|
|
||||||
|
|
||||||
|
def step2(self):
|
||||||
|
def wait_for_pdf_download(timeout=60):
|
||||||
|
for _ in range(timeout):
|
||||||
|
files = [f for f in os.listdir(self.download_dir) if f.endswith(".pdf")]
|
||||||
|
if files:
|
||||||
|
return os.path.join(self.download_dir, files[0])
|
||||||
|
time.sleep(1)
|
||||||
|
raise TimeoutError("PDF did not download in time")
|
||||||
|
|
||||||
|
def _unique_target_path():
|
||||||
|
"""
|
||||||
|
Create a unique filename using memberId.
|
||||||
|
"""
|
||||||
|
safe_member = "".join(c for c in str(self.memberId) if c.isalnum() or c in "-_.")
|
||||||
|
filename = f"eligibility_{safe_member}.pdf"
|
||||||
|
return os.path.join(self.download_dir, filename)
|
||||||
|
|
||||||
|
wait = WebDriverWait(self.driver, 90)
|
||||||
|
tmp_created_path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
eligibilityElement = wait.until(EC.presence_of_element_located((By.XPATH,
|
||||||
|
f"//table[@id='Table3']//tr[td[contains(text(), '{self.memberId}')]]/td[3]")))
|
||||||
|
eligibilityText = eligibilityElement.text
|
||||||
|
|
||||||
|
txReportElement = wait.until(EC.element_to_be_clickable((By.XPATH,
|
||||||
|
f"//table[@id='Table3']//tr[td[contains(text(), '{self.memberId}')]]//input[@value='Tx Report']"
|
||||||
|
)))
|
||||||
|
|
||||||
|
txReportElement.click()
|
||||||
|
|
||||||
|
# wait for the PDF to fully appear
|
||||||
|
downloaded_path = wait_for_pdf_download()
|
||||||
|
# generate unique target path (include memberId)
|
||||||
|
target_path = _unique_target_path()
|
||||||
|
# It's possible Chrome writes a file with a fixed name: copy/rename it to our target name.
|
||||||
|
shutil.copyfile(downloaded_path, target_path)
|
||||||
|
# ensure the copied file is writable / stable
|
||||||
|
os.chmod(target_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
|
||||||
|
|
||||||
|
|
||||||
|
print("PDF downloaded at:", target_path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "success",
|
||||||
|
"eligibility": eligibilityText,
|
||||||
|
"pdf_path": target_path
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: {str(e)}")
|
||||||
|
|
||||||
|
# Empty the download folder (remove files / symlinks only)
|
||||||
|
try:
|
||||||
|
dl = os.path.abspath(self.download_dir)
|
||||||
|
if os.path.isdir(dl):
|
||||||
|
for name in os.listdir(dl):
|
||||||
|
item = os.path.join(dl, name)
|
||||||
|
try:
|
||||||
|
if os.path.isfile(item) or os.path.islink(item):
|
||||||
|
os.remove(item)
|
||||||
|
print(f"[cleanup] removed: {item}")
|
||||||
|
except Exception as rm_err:
|
||||||
|
print(f"[cleanup] failed to remove {item}: {rm_err}")
|
||||||
|
print(f"[cleanup] emptied download dir: {dl}")
|
||||||
|
else:
|
||||||
|
print(f"[cleanup] download dir does not exist: {dl}")
|
||||||
|
except Exception as cleanup_exc:
|
||||||
|
print(f"[cleanup] unexpected error while cleaning downloads dir: {cleanup_exc}")
|
||||||
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": str(e),
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if self.driver:
|
||||||
|
self.driver.quit()
|
||||||
|
|
||||||
|
def main_workflow(self, url):
|
||||||
|
try:
|
||||||
|
self.config_driver()
|
||||||
|
self.driver.maximize_window()
|
||||||
|
self.driver.get(url)
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
login_result = self.login()
|
||||||
|
if login_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": login_result}
|
||||||
|
if login_result == "OTP_REQUIRED":
|
||||||
|
return {"status": "otp_required", "message": "OTP required after login"}
|
||||||
|
|
||||||
|
step1_result = self.step1()
|
||||||
|
if step1_result.startswith("ERROR"):
|
||||||
|
return {"status": "error", "message": step1_result}
|
||||||
|
|
||||||
|
step2_result = self.step2()
|
||||||
|
if step2_result.get("status") == "error":
|
||||||
|
return {"status": "error", "message": step2_result.get("message")}
|
||||||
|
|
||||||
|
return step2_result
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"message": e
|
||||||
|
}
|
||||||
|
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
if self.driver:
|
||||||
|
self.driver.quit()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
300
package-lock.json
generated
300
package-lock.json
generated
@@ -51,6 +51,7 @@
|
|||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"pdfkit": "^0.17.2",
|
"pdfkit": "^0.17.2",
|
||||||
|
"socket.io": "^4.8.1",
|
||||||
"ws": "^8.18.0",
|
"ws": "^8.18.0",
|
||||||
"zod": "^3.24.2",
|
"zod": "^3.24.2",
|
||||||
"zod-validation-error": "^3.4.0"
|
"zod-validation-error": "^3.4.0"
|
||||||
@@ -142,6 +143,7 @@
|
|||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
"recharts": "^2.15.2",
|
"recharts": "^2.15.2",
|
||||||
|
"socket.io-client": "^4.8.1",
|
||||||
"tailwind-merge": "^3.2.0",
|
"tailwind-merge": "^3.2.0",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -3773,6 +3775,12 @@
|
|||||||
"integrity": "sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ==",
|
"integrity": "sha512-auUj4k+f4pyrIVf4GW5UKquSZFHJWri06QgARy9C0t9ZTjJLIuNIrr1yl9bWcJWJ1Gz1vOvYN1D+QPaIlNMVkQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/spec": {
|
"node_modules/@standard-schema/spec": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
@@ -4252,7 +4260,6 @@
|
|||||||
"version": "2.8.19",
|
"version": "2.8.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
|
||||||
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
"integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
@@ -4438,7 +4445,6 @@
|
|||||||
"version": "20.16.11",
|
"version": "20.16.11",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.11.tgz",
|
||||||
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
"integrity": "sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
@@ -5295,6 +5301,15 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64id": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^4.5.0 || >= 5.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.8.25",
|
"version": "2.8.25",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
|
||||||
@@ -6492,6 +6507,133 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io": {
|
||||||
|
"version": "6.6.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz",
|
||||||
|
"integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/cors": "^2.8.12",
|
||||||
|
"@types/node": ">=10.0.0",
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "2.0.0",
|
||||||
|
"cookie": "~0.7.2",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
|
||||||
|
"integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.2.1",
|
||||||
|
"ws": "~8.17.1",
|
||||||
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-client/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||||
|
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/enhanced-resolve": {
|
"node_modules/enhanced-resolve": {
|
||||||
"version": "5.18.3",
|
"version": "5.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||||
@@ -11005,6 +11147,151 @@
|
|||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.4",
|
||||||
|
"base64id": "~2.0.0",
|
||||||
|
"cors": "~2.8.5",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io": "~6.6.0",
|
||||||
|
"socket.io-adapter": "~2.5.2",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter": {
|
||||||
|
"version": "2.5.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz",
|
||||||
|
"integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "~4.3.4",
|
||||||
|
"ws": "~8.17.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-adapter/node_modules/ws": {
|
||||||
|
"version": "8.17.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.6.1",
|
||||||
|
"socket.io-parser": "~4.2.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-client/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||||
|
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io/node_modules/debug": {
|
||||||
|
"version": "4.3.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map": {
|
"node_modules/source-map": {
|
||||||
"version": "0.6.1",
|
"version": "0.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
@@ -12033,7 +12320,6 @@
|
|||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicode-properties": {
|
"node_modules/unicode-properties": {
|
||||||
@@ -12567,6 +12853,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user