import { Router } from "express"; import { Request, Response } from "express"; import { storage } from "../storage"; import { forwardToSeleniumInsuranceEligibilityAgent } from "../services/seleniumInsuranceEligibilityClient"; import fs from "fs/promises"; import path from "path"; import PDFDocument from "pdfkit"; import { forwardToSeleniumInsuranceClaimStatusAgent } from "../services/seleniumInsuranceClaimStatusClient"; import fsSync from "fs"; import { emptyFolderContainingFile } from "../utils/emptyTempFolder"; import forwardToPatientDataExtractorService from "../services/patientDataExtractorService"; import { InsertPatient, insertPatientSchema, } from "../../../../packages/db/types/patient-types"; import { formatDobForAgent } from "../utils/dateUtils"; const router = Router(); /** 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. * If exists -> update first/last name when different. * If not -> create using provided fields. * Returns the patient object (the version read from DB after potential create/update). */ 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"); let patient = await storage.getPatientByInsuranceId(insuranceId); // Normalize incoming names const incomingFirst = firstName!.trim(); const incomingLast = lastName!.trim(); if (patient && patient.id) { // update only if different 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 { // inside createOrUpdatePatientByInsuranceId, when creating: const createPayload: any = { firstName: incomingFirst, lastName: incomingLast, dateOfBirth: dob, // raw from caller (string | Date | null) gender: "", phone: "", userId, insuranceId, }; let patientData: InsertPatient; try { patientData = insertPatientSchema.parse(createPayload); } catch (err) { // handle malformed dob or other validation errors conservatively console.warn( "Failed to validate patient payload in insurance flow:", err ); // either rethrow or drop invalid fields — here we drop dob and proceed const safePayload = { ...createPayload }; delete (safePayload as any).dateOfBirth; patientData = insertPatientSchema.parse(safePayload); } await storage.createPatient(patientData); } } /** * /eligibility-check * - run selenium * - if pdf created -> call extractor -> get name * - create or update patient (by memberId) * - attach PDF to patient (create pdf group/file) * - return { patient, pdfFileId, extractedName ... } */ router.post( "/eligibility-check", 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 || !req.user.id) { return res.status(401).json({ error: "Unauthorized: user info missing" }); } let seleniumResult: any = undefined; let createdPdfFileId: number | null = null; let outputResult: any = {}; const extracted: any = {}; try { const insuranceEligibilityData = JSON.parse(req.body.data); const credentials = await storage.getInsuranceCredentialByUserAndSiteKey( req.user.id, insuranceEligibilityData.insuranceSiteKey ); if (!credentials) { return res.status(404).json({ error: "No insurance credentials found for this provider, Kindly Update this at Settings Page.", }); } const enrichedData = { ...insuranceEligibilityData, massdhpUsername: credentials.username, massdhpPassword: credentials.password, }; // 1) Run selenium agent try { seleniumResult = await forwardToSeleniumInsuranceEligibilityAgent(enrichedData); } catch (seleniumErr: any) { return res.status(502).json({ error: "Selenium service failed", detail: seleniumErr?.message ?? String(seleniumErr), }); } // 2) If selenium produced a pdf path, extract name if ( seleniumResult?.pdf_path && 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 (extractErr: any) { return res.status(502).json({ error: "Patient data extraction failed", detail: extractErr?.message ?? String(extractErr), }); } } // Step-3) Create or update patient name using extracted info (prefer extractor -> request) const insuranceId = String( insuranceEligibilityData.memberId ?? "" ).trim(); if (!insuranceId) { return res.status(400).json({ error: "Missing memberId" }); } // prefer extractor names, else use request-sent names, else null const preferFirst = extracted.firstName; const preferLast = extracted.lastName; try { await createOrUpdatePatientByInsuranceId({ insuranceId, firstName: preferFirst, lastName: preferLast, dob: insuranceEligibilityData.dateOfBirth, userId: req.user.id, }); } catch (patientOpErr: any) { return res.status(500).json({ error: "Failed to create/update patient", detail: patientOpErr?.message ?? String(patientOpErr), }); } // ✅ Step 4: Check result and update patient status 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}`; // ✅ Step 5: Handle PDF Upload if ( seleniumResult.pdf_path && 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 ); // Step 5b: Create group if it doesn’t exist 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 ); // created could be { id, filename } or just id, adapt to your storage API. 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"; } res.json({ patientUpdateStatus: outputResult.patientUpdateStatus, pdfUploadStatus: outputResult.pdfUploadStatus, pdfFileId: createdPdfFileId, }); } catch (err: any) { console.error(err); return res.status(500).json({ error: err.message || "Failed to forward to selenium agent", }); } finally { try { if (seleniumResult && seleniumResult.pdf_path) { await emptyFolderContainingFile(seleniumResult.pdf_path); } else { console.log(`[eligibility-check] no pdf_path available to cleanup`); } } catch (cleanupErr) { console.error( `[eligibility-check cleanup failed for ${seleniumResult?.pdf_path}`, cleanupErr ); } } } ); router.post( "/claim-status-check", async (req: Request, res: Response): Promise => { if (!req.body.data) { return res .status(400) .json({ error: "Missing Insurance Status data for selenium" }); } if (!req.user || !req.user.id) { return res.status(401).json({ error: "Unauthorized: user info missing" }); } let result: any = undefined; async function imageToPdfBuffer(imagePath: string): Promise { return new Promise((resolve, reject) => { try { const doc = new PDFDocument({ autoFirstPage: false }); const chunks: Uint8Array[] = []; // collect data chunks 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; // points const A4_HEIGHT = 841.89; // points 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); } }); } try { const insuranceClaimStatusData = JSON.parse(req.body.data); const credentials = await storage.getInsuranceCredentialByUserAndSiteKey( req.user.id, insuranceClaimStatusData.insuranceSiteKey ); if (!credentials) { return res.status(404).json({ error: "No insurance credentials found for this provider, Kindly Update this at Settings Page.", }); } const enrichedData = { ...insuranceClaimStatusData, massdhpUsername: credentials.username, massdhpPassword: credentials.password, }; result = await forwardToSeleniumInsuranceClaimStatusAgent(enrichedData); let createdPdfFileId: number | null = null; // ✅ Step 1: Check result const patient = await storage.getPatientByInsuranceId( insuranceClaimStatusData.memberId ); if (patient && patient.id !== undefined) { let pdfBuffer: Buffer | null = null; let generatedPdfPath: string | null = null; if ( result.ss_path && (result.ss_path.endsWith(".png") || result.ss_path.endsWith(".jpg") || result.ss_path.endsWith(".jpeg")) ) { try { // Ensure file exists if (!fsSync.existsSync(result.ss_path)) { throw new Error(`Screenshot file not found: ${result.ss_path}`); } // Convert image to PDF buffer pdfBuffer = await imageToPdfBuffer(result.ss_path); // Optionally write generated PDF to temp path (so name is available for createPdfFile) const pdfFileName = `claimStatus_${insuranceClaimStatusData.memberId}_${Date.now()}.pdf`; generatedPdfPath = path.join( path.dirname(result.ss_path), pdfFileName ); await fs.writeFile(generatedPdfPath, pdfBuffer); } catch (err) { console.error("Failed to convert screenshot to PDF:", err); result.pdfUploadStatus = `Failed to convert screenshot to PDF: ${String(err)}`; } } else { result.pdfUploadStatus = "No valid PDF or screenshot path provided by Selenium; nothing to upload."; } if (pdfBuffer && generatedPdfPath) { const groupTitle = "Claim Status"; const groupTitleKey = "CLAIM_STATUS"; let group = await storage.findPdfGroupByPatientTitleKey( patient.id, groupTitleKey ); // Create group if missing if (!group) { group = await storage.createPdfGroup( patient.id, groupTitle, groupTitleKey ); } if (!group?.id) { throw new Error("PDF group creation failed: missing group ID"); } // Use the basename for storage const basename = path.basename(generatedPdfPath); const created = await storage.createPdfFile( group.id, basename, pdfBuffer ); if (created && typeof created === "object" && "id" in created) { createdPdfFileId = Number(created.id); } result.pdfUploadStatus = `PDF saved to group: ${group.title}`; } } else { result.patientUpdateStatus = "Patient not found or missing ID; no update performed"; } res.json({ pdfUploadStatus: result.pdfUploadStatus, pdfFileId: createdPdfFileId, }); return; } catch (err: any) { console.error(err); return res.status(500).json({ error: err.message || "Failed to forward to selenium agent", }); } finally { try { if (result && result.ss_path) { await emptyFolderContainingFile(result.ss_path); } else { console.log(`claim-status-check] no pdf_path available to cleanup`); } } catch (cleanupErr) { console.error( `[claim-status-check cleanup failed for ${result?.ss_path}`, cleanupErr ); } } } ); router.post( "/appointments/check-all-eligibilities", async (req: Request, res: Response): Promise => { // Query param: date=YYYY-MM-DD (required) const date = String(req.query.date ?? "").trim(); if (!date) { return res .status(400) .json({ error: "Missing date query param (YYYY-MM-DD)" }); } if (!req.user || !req.user.id) { return res.status(401).json({ error: "Unauthorized: user info missing" }); } // Track any paths that couldn't be cleaned immediately so we can try again at the end const remainingCleanupPaths = new Set(); try { // 1) fetch appointments for the day (reuse your storage API) const dayAppointments = await storage.getAppointmentsByDateForUser( date, req.user.id ); if (!Array.isArray(dayAppointments)) { return res .status(500) .json({ error: "Failed to load appointments for date" }); } const results: Array = []; // process sequentially so selenium agent / python semaphore isn't overwhelmed for (const apt of dayAppointments) { // For each appointment we keep a per-appointment seleniumResult so we can cleanup its files let seleniumResult: any = undefined; const resultItem: any = { appointmentId: apt.id, patientId: apt.patientId ?? null, processed: false, error: null, pdfFileId: null, patientUpdateStatus: null, warning: null, }; try { // fetch patient record (use getPatient or getPatientById depending on your storage) const patient = apt.patientId ? await storage.getPatient(apt.patientId) : null; const memberId = (patient?.insuranceId ?? "").toString().trim(); // create a readable patient label for error messages const patientLabel = patient ? `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim() || `patient#${patient.id}` : `patient#${apt.patientId ?? "unknown"}`; const aptLabel = `appointment#${apt.id}${apt.date ? ` (${apt.date}${apt.startTime ? ` ${apt.startTime}` : ""})` : ""}`; if (!memberId) { resultItem.error = `Missing insuranceId for ${patientLabel} — skipping ${aptLabel}`; results.push(resultItem); continue; } // prepare eligibility data; prefer patient DOB + name if present const dob = patient?.dateOfBirth; if (!dob) { resultItem.error = `Missing dob for ${patientLabel} — skipping ${aptLabel}`; results.push(resultItem); continue; } // Convert Date object → YYYY-MM-DD string - req for selenium agent. const dobStr = formatDobForAgent(dob); if (!dobStr) { resultItem.error = `Invalid or missing DOB for ${patientLabel} — skipping ${aptLabel}`; results.push(resultItem); continue; } const payload = { memberId, dateOfBirth: dobStr, insuranceSiteKey: "MH", }; // Get credentials for this user+site const credentials = await storage.getInsuranceCredentialByUserAndSiteKey( req.user.id, payload.insuranceSiteKey ); if (!credentials) { resultItem.error = `No insurance credentials found for siteKey — skipping ${aptLabel} for ${patientLabel}`; results.push(resultItem); continue; } // enrich payload const enriched = { ...payload, massdhpUsername: credentials.username, massdhpPassword: credentials.password, }; // forward to selenium agent (sequential) try { seleniumResult = await forwardToSeleniumInsuranceEligibilityAgent(enriched); } catch (seleniumErr: any) { resultItem.error = `Selenium agent failed for ${patientLabel} (${aptLabel}): ${seleniumErr?.message ?? String(seleniumErr)}`; results.push(resultItem); continue; } // Attempt extraction (if pdf_path present) const extracted: any = {}; if ( seleniumResult?.pdf_path && 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 (extractErr: any) { resultItem.warning = `Extraction failed: ${extractErr?.message ?? String(extractErr)}`; } } // create or update patient by insuranceId — prefer extracted name const preferFirst = extracted.firstName ?? null; const preferLast = extracted.lastName ?? null; try { await createOrUpdatePatientByInsuranceId({ insuranceId: memberId, firstName: preferFirst, lastName: preferLast, dob: payload.dateOfBirth, userId: req.user.id, }); } catch (patientOpErr: any) { resultItem.error = `Failed to create/update patient ${patientLabel} for ${aptLabel}: ${patientOpErr?.message ?? String(patientOpErr)}`; results.push(resultItem); continue; } // fetch patient again const updatedPatient = await storage.getPatientByInsuranceId(memberId); if (!updatedPatient || !updatedPatient.id) { resultItem.error = `Patient not found after create/update for ${patientLabel} (${aptLabel})`; results.push(resultItem); continue; } // Update patient status based on seleniumResult.eligibility const newStatus = seleniumResult?.eligibility === "Y" ? "ACTIVE" : "INACTIVE"; // 1. updating patient await storage.updatePatient(updatedPatient.id, { status: newStatus }); resultItem.patientUpdateStatus = `Patient status updated to ${newStatus}`; // 2. updating appointment status - for aptmnt page try { await storage.updateAppointment(Number(apt.id), { eligibilityStatus: newStatus, }); resultItem.appointmentUpdateStatus = `Appointment eligibility set to ${newStatus}`; } catch (apptUpdateErr: any) { resultItem.warning = (resultItem.warning ? resultItem.warning + " | " : "") + `Failed to update appointment eligibility: ${apptUpdateErr?.message ?? String(apptUpdateErr)}`; } // If PDF exists, upload to PdfGroup (ELIGIBILITY_STATUS) if ( seleniumResult?.pdf_path && seleniumResult.pdf_path.endsWith(".pdf") ) { try { const pdfBuf = await fs.readFile(seleniumResult.pdf_path); const groupTitle = "Eligibility Status"; const groupTitleKey = "ELIGIBILITY_STATUS"; let group = await storage.findPdfGroupByPatientTitleKey( updatedPatient.id, groupTitleKey ); if (!group) { group = await storage.createPdfGroup( updatedPatient.id, groupTitle, groupTitleKey ); } if (!group?.id) throw new Error("Failed to create/find pdf group"); const created = await storage.createPdfFile( group.id, path.basename(seleniumResult.pdf_path), pdfBuf ); if (created && typeof created === "object" && "id" in created) { resultItem.pdfFileId = Number(created.id); } else if (typeof created === "number") { resultItem.pdfFileId = created; } else if (created && (created as any).id) { resultItem.pdfFileId = (created as any).id; } resultItem.processed = true; } catch (pdfErr: any) { resultItem.warning = `PDF upload failed for ${patientLabel} (${aptLabel}): ${pdfErr?.message ?? String(pdfErr)}`; } } else { // no pdf; still mark processed true (status updated) resultItem.processed = true; resultItem.pdfFileId = null; } results.push(resultItem); } catch (err: any) { resultItem.error = `Unexpected error for appointment#${apt.id}: ${err?.message ?? String(err)}`; results.push(resultItem); console.error( "[batch eligibility] unexpected error for appointment", apt.id, err ); } finally { // Per-appointment cleanup: always try to remove selenium temp files for this appointment try { if ( seleniumResult && (seleniumResult.pdf_path || seleniumResult.ss_path) ) { // prefer pdf_path, fallback to ss_path const candidatePath = seleniumResult.pdf_path ?? seleniumResult.ss_path; try { await emptyFolderContainingFile(candidatePath); } catch (cleanupErr: any) { console.warn( `[batch cleanup] failed to clean ${candidatePath} for appointment ${apt.id}`, cleanupErr ); // remember path for final cleanup attempt remainingCleanupPaths.add(candidatePath); } } } catch (cleanupOuterErr: any) { console.warn( "[batch cleanup] unexpected error during per-appointment cleanup", cleanupOuterErr ); // don't throw — we want to continue processing next appointments } } // end try/catch/finally per appointment } // end for appointments // return summary return res.json({ date, count: results.length, results }); } catch (err: any) { console.error("[check-all-eligibilities] error", err); return res .status(500) .json({ error: err?.message ?? "Internal server error" }); } finally { // Final cleanup attempt for any remaining paths we couldn't delete earlier try { if (remainingCleanupPaths.size > 0) { for (const p of remainingCleanupPaths) { try { await emptyFolderContainingFile(p); } catch (finalCleanupErr: any) { console.error(`[final cleanup] failed for ${p}`, finalCleanupErr); } } } } catch (outerFinalErr: any) { console.error( "[check-all-eligibilities final cleanup] unexpected error", outerFinalErr ); } } } ); export default router;