import { Router, Request, Response } from "express"; import { storage } from "../storage"; import { forwardOtpToSeleniumDdmaAgent } from "../services/seleniumDdmaInsuranceEligibilityClient"; import { io } from "../socket"; import { enqueueSeleniumJob } from "../queue/jobRunner"; const router = Router(); function log(tag: string, msg: string, ctx?: any) { console.log(`${new Date().toISOString()} [${tag}] ${msg}`, ctx ?? ""); } function emitSafe(socketId: string | undefined, event: string, payload: any) { if (!socketId || !io) return; try { const socket = io.sockets.sockets.get(socketId); if (socket) socket.emit(event, payload); } catch (err: any) { log("socket", "emit failed", { socketId, event, err: err?.message }); } } /** * POST /ddma-eligibility * * Enqueues a DDMA eligibility check in the shared InProcessQueue * (concurrency=1, mirrors the Python semaphore). * * Body: * data — patient + search fields (memberId, dateOfBirth, …) * socketId — socket.io client id for real-time updates * * Response: { status: "queued", jobId: "…" } * * Real-time events emitted to socketId during job execution: * job:update { jobId, jobType, status: "active"|"completed"|"failed", … } * selenium:ddma_session_started { session_id, jobId } * selenium:otp_required { session_id, jobId, message } */ router.post( "/ddma-eligibility", async (req: Request, res: Response): Promise => { if (!req.body.data) { return res .status(400) .json({ error: "Missing Insurance Eligibility data for selenium" }); } if (!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; // Fetch credentials from DB 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. Please update them at the Settings page.", }); } // Fetch NPI providers to pick the target provider on the DDMA portal const npiProviders = await storage.getNpiProvidersByUser(req.user.id); const primaryProvider = npiProviders[0]; // sorted by sortOrder asc, then id asc const enrichedData = { ...rawData, massddmaUsername: credentials.username, massddmaPassword: credentials.password, providerName: primaryProvider?.providerName ?? "", }; const socketId: string | undefined = req.body.socketId; // Enqueue — this enforces the same concurrency=1 as all other selenium jobs const jobId = enqueueSeleniumJob({ jobType: "ddma-eligibility-check", userId: req.user.id, socketId, enrichedPayload: enrichedData, insuranceId: String(rawData.memberId ?? "").trim(), formFirstName: rawData.firstName, formLastName: rawData.lastName, formDob: rawData.dateOfBirth, }); log("ddma-route", "job enqueued", { jobId, insuranceId: rawData.memberId }); return res.json({ status: "queued", jobId }); } catch (err: any) { console.error("[ddma-route] enqueue failed:", err); return res.status(500).json({ error: err.message || "Failed to enqueue DDMA selenium job", }); } } ); /** * POST /selenium/submit-otp * * Forwards the OTP entered by the user directly to the Python agent. * This is a side-channel — it does NOT go through the queue. * The polling loop inside ddmaEligibilityProcessor picks up the completed * state after OTP is submitted. * * Body: { session_id, otp, socketId? } */ router.post( "/selenium/submit-otp", async (req: Request, res: Response): Promise => { 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); emitSafe(socketId, "selenium:otp_submitted", { session_id: sessionId, result: r, }); return res.json(r); } catch (err: any) { console.error( "[ddma-route] submit-otp failed:", 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;