initial commit
This commit is contained in:
844
apps/Backend/src/routes/insuranceStatus.ts
Executable file
844
apps/Backend/src/routes/insuranceStatus.ts
Executable file
@@ -0,0 +1,844 @@
|
||||
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);
|
||||
// Refetch to get updated data
|
||||
patient = await storage.getPatientByInsuranceId(insuranceId);
|
||||
}
|
||||
return patient;
|
||||
} 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);
|
||||
// Return the created patient
|
||||
return await storage.getPatientByInsuranceId(insuranceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* /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<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" });
|
||||
}
|
||||
|
||||
let seleniumResult: any = undefined;
|
||||
let createdPdfFileId: number | null = null;
|
||||
let outputResult: any = {};
|
||||
const extracted: any = {};
|
||||
|
||||
try {
|
||||
// const insuranceEligibilityData = JSON.parse(req.body.data);
|
||||
// Handle both string and object data
|
||||
const insuranceEligibilityData = typeof req.body.data === 'string'
|
||||
? JSON.parse(req.body.data)
|
||||
: 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) Extract data from selenium result (page extraction) and PDF
|
||||
let extracted: any = {};
|
||||
|
||||
// First, try to get data from selenium's page extraction
|
||||
if (seleniumResult.firstName || seleniumResult.lastName) {
|
||||
extracted.firstName = seleniumResult.firstName || null;
|
||||
extracted.lastName = seleniumResult.lastName || null;
|
||||
console.log('[eligibility-check] Using name from selenium extraction:', {
|
||||
firstName: extracted.firstName,
|
||||
lastName: extracted.lastName
|
||||
});
|
||||
}
|
||||
// Also check for combined name field (fallback)
|
||||
else if (seleniumResult.name) {
|
||||
const parts = splitName(seleniumResult.name);
|
||||
extracted.firstName = parts.firstName;
|
||||
extracted.lastName = parts.lastName;
|
||||
console.log('[eligibility-check] Using combined name from selenium extraction:', parts);
|
||||
}
|
||||
|
||||
// If no name from selenium, try PDF extraction
|
||||
if (!extracted.firstName && !extracted.lastName &&
|
||||
seleniumResult?.pdf_path &&
|
||||
seleniumResult.pdf_path.endsWith(".pdf")
|
||||
) {
|
||||
try {
|
||||
const pdfPath = seleniumResult.pdf_path;
|
||||
console.log('[eligibility-check] Extracting data from PDF:', pdfPath);
|
||||
const pdfBuffer = await fs.readFile(pdfPath);
|
||||
|
||||
const extraction = await forwardToPatientDataExtractorService({
|
||||
buffer: pdfBuffer,
|
||||
originalname: path.basename(pdfPath),
|
||||
mimetype: "application/pdf",
|
||||
} as any);
|
||||
|
||||
console.log('[eligibility-check] PDF Extraction result:', extraction);
|
||||
|
||||
if (extraction.name) {
|
||||
const parts = splitName(extraction.name);
|
||||
extracted.firstName = parts.firstName;
|
||||
extracted.lastName = parts.lastName;
|
||||
console.log('[eligibility-check] Split name from PDF:', parts);
|
||||
} else {
|
||||
console.warn('[eligibility-check] No name extracted from PDF');
|
||||
}
|
||||
} catch (extractErr: any) {
|
||||
console.error('[eligibility-check] Patient data extraction failed:', extractErr);
|
||||
// Continue without extracted names - we'll use form names or create patient with empty names
|
||||
}
|
||||
}
|
||||
|
||||
// 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" });
|
||||
}
|
||||
|
||||
// Always prioritize extracted data from MassHealth over form input
|
||||
// Form input is only used as fallback when extraction fails
|
||||
const preferFirst = extracted.firstName || null;
|
||||
const preferLast = extracted.lastName || null;
|
||||
|
||||
console.log('[eligibility-check] Name priority:', {
|
||||
extracted: { firstName: extracted.firstName, lastName: extracted.lastName },
|
||||
fromForm: { firstName: insuranceEligibilityData.firstName, lastName: insuranceEligibilityData.lastName },
|
||||
using: { firstName: preferFirst, lastName: preferLast }
|
||||
});
|
||||
|
||||
let patient;
|
||||
try {
|
||||
patient = await createOrUpdatePatientByInsuranceId({
|
||||
insuranceId,
|
||||
firstName: preferFirst,
|
||||
lastName: preferLast,
|
||||
dob: insuranceEligibilityData.dateOfBirth,
|
||||
userId: req.user.id,
|
||||
});
|
||||
console.log('[eligibility-check] Patient after create/update:', patient);
|
||||
} catch (patientOpErr: any) {
|
||||
return res.status(500).json({
|
||||
error: "Failed to create/update patient",
|
||||
detail: patientOpErr?.message ?? String(patientOpErr),
|
||||
});
|
||||
}
|
||||
|
||||
// ✅ Step 4: Update patient status based on selenium result
|
||||
if (patient && patient.id !== undefined) {
|
||||
// Use eligibility from selenium extraction if available, otherwise default to UNKNOWN
|
||||
let newStatus = "UNKNOWN";
|
||||
|
||||
if (seleniumResult.eligibility === "Y") {
|
||||
newStatus = "ACTIVE";
|
||||
} else if (seleniumResult.eligibility === "N") {
|
||||
newStatus = "INACTIVE";
|
||||
}
|
||||
|
||||
// Prepare updates object
|
||||
const updates: any = { status: newStatus };
|
||||
|
||||
// Update insurance provider if extracted
|
||||
if (seleniumResult.insurance) {
|
||||
updates.insuranceProvider = seleniumResult.insurance;
|
||||
console.log('[eligibility-check] Updating insurance provider:', seleniumResult.insurance);
|
||||
}
|
||||
|
||||
await storage.updatePatient(patient.id, updates);
|
||||
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}${seleniumResult.insurance ? ', insurance updated' : ''}`;
|
||||
console.log('[eligibility-check] Status updated:', {
|
||||
patientId: patient.id,
|
||||
newStatus,
|
||||
eligibility: seleniumResult.eligibility,
|
||||
insurance: seleniumResult.insurance
|
||||
});
|
||||
|
||||
// ✅ 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<any> => {
|
||||
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<Buffer> {
|
||||
return new Promise<Buffer>((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<any> => {
|
||||
// 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<string>();
|
||||
|
||||
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<any> = [];
|
||||
|
||||
// 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;
|
||||
Reference in New Issue
Block a user