Files
DentalManagementE/apps/Backend/src/routes/insuranceStatus.ts

772 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<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);
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 doesnt 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";
await storage.updatePatient(updatedPatient.id, { status: newStatus });
resultItem.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// 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;