- Add remainingBenefit and thirdPartyInsurance columns to Patient; extract both from the MassHealth accumulator/member-detail pages during the eligibility check and surface them in the eligibility table - Add an updated-date display and a toggle to suppress the post-check PDF viewer popup (default off) on the insurance eligibility page - Rename CMSP-prefixed PDF filenames and UI copy to MH, since CMSP and MH now share the same worker - Fold CMSP into MassHealth/MH across patient/claim insurance-type dropdowns and the chatbot's eligibility routing, since they're the same insurance - Remove auto-download of saved eligibility PDFs; downloading is now manual Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
185 lines
7.0 KiB
TypeScript
185 lines
7.0 KiB
TypeScript
/**
|
|
* Processor for "cmsp-eligibility-history-remaining-check" jobs.
|
|
* Saves eligibility PDF, service history PDF, and accumulator PDF
|
|
* to the patient's ELIGIBILITY_STATUS document group.
|
|
*/
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import { storage } from "../../storage";
|
|
import { emptyFolderContainingFile } from "../../utils/emptyTempFolder";
|
|
import forwardToPatientDataExtractorService from "../../services/patientDataExtractorService";
|
|
import {
|
|
callPythonSync,
|
|
splitName,
|
|
createOrUpdatePatientByInsuranceId,
|
|
} from "./_shared";
|
|
|
|
export interface CmspEligibilityHistoryRemainingProcessorInput {
|
|
enrichedPayload: any;
|
|
userId: number;
|
|
insuranceId: string;
|
|
formFirstName?: string;
|
|
formLastName?: string;
|
|
formDob?: string;
|
|
}
|
|
|
|
export interface CmspEligibilityHistoryRemainingProcessorResult {
|
|
patientUpdateStatus?: string;
|
|
pdfUploadStatus?: string;
|
|
pdfFileId?: number | string | null;
|
|
pdfFilename?: string | null;
|
|
memberDetailsPdfFileId?: number | string | null;
|
|
memberDetailsPdfFilename?: string | null;
|
|
historyPdfFileId?: number | string | null;
|
|
historyPdfFilename?: string | null;
|
|
accumulatorPdfFileId?: number | string | null;
|
|
accumulatorPdfFilename?: string | null;
|
|
remainingBenefit?: string | null;
|
|
thirdPartyInsurance?: string | null;
|
|
}
|
|
|
|
export async function runCmspEligibilityHistoryRemainingProcessor(
|
|
input: CmspEligibilityHistoryRemainingProcessorInput
|
|
): Promise<CmspEligibilityHistoryRemainingProcessorResult> {
|
|
const { enrichedPayload, userId, insuranceId, formFirstName, formLastName, formDob } = input;
|
|
|
|
const seleniumResult = await callPythonSync("/cmsp-eligibility-history-remaining-check", {
|
|
data: enrichedPayload,
|
|
});
|
|
|
|
const outputResult: CmspEligibilityHistoryRemainingProcessorResult = {};
|
|
|
|
// Extract patient name
|
|
const seleniumFirst = seleniumResult.firstName?.trim() || null;
|
|
const seleniumLast = seleniumResult.lastName?.trim() || null;
|
|
const seleniumName = seleniumResult.name?.trim() || null;
|
|
const seleniumInsurance = seleniumResult.insurance?.trim() || null;
|
|
|
|
const extracted: { firstName?: string | null; lastName?: string | null } = {};
|
|
|
|
if (seleniumFirst || seleniumLast) {
|
|
extracted.firstName = seleniumFirst;
|
|
extracted.lastName = seleniumLast;
|
|
} else if (seleniumName) {
|
|
const parts = splitName(seleniumName);
|
|
extracted.firstName = parts.firstName || null;
|
|
extracted.lastName = parts.lastName || null;
|
|
}
|
|
|
|
if (!extracted.firstName && !extracted.lastName && seleniumResult?.pdf_path?.endsWith(".pdf")) {
|
|
try {
|
|
const pdfBuffer = await fs.readFile(seleniumResult.pdf_path);
|
|
const extraction = await forwardToPatientDataExtractorService({
|
|
buffer: pdfBuffer,
|
|
originalname: path.basename(seleniumResult.pdf_path),
|
|
mimetype: "application/pdf",
|
|
} as any);
|
|
if (extraction.name) {
|
|
const parts = splitName(extraction.name);
|
|
extracted.firstName = parts.firstName || null;
|
|
extracted.lastName = parts.lastName || null;
|
|
}
|
|
} catch (e) {
|
|
console.error("[cmspProcessor] PDF name extraction failed:", e);
|
|
}
|
|
}
|
|
|
|
const preferFirst = extracted.firstName || formFirstName || null;
|
|
const preferLast = extracted.lastName || formLastName || null;
|
|
|
|
let patient;
|
|
try {
|
|
patient = await createOrUpdatePatientByInsuranceId({
|
|
insuranceId,
|
|
firstName: preferFirst,
|
|
lastName: preferLast,
|
|
dob: formDob,
|
|
userId,
|
|
});
|
|
} catch (e: any) {
|
|
throw new Error(`Failed to create/update patient: ${e.message}`);
|
|
}
|
|
|
|
if (patient && patient.id !== undefined) {
|
|
let newStatus = "UNKNOWN";
|
|
if (seleniumResult.eligibility === "Y") newStatus = "ACTIVE";
|
|
else if (seleniumResult.eligibility === "N") newStatus = "INACTIVE";
|
|
|
|
const updates: any = { status: newStatus };
|
|
if (seleniumInsurance) updates.insuranceProvider = seleniumInsurance;
|
|
|
|
const remainingBenefit = seleniumResult.remaining_amount?.trim() || null;
|
|
if (remainingBenefit) updates.remainingBenefit = remainingBenefit;
|
|
|
|
// third_party_insurance is present (even as null) whenever the Third Party
|
|
// Liability table was found — null means the member has no 2nd insurance,
|
|
// so we still write it to clear any stale value from a prior check.
|
|
if (Object.prototype.hasOwnProperty.call(seleniumResult, "third_party_insurance")) {
|
|
updates.thirdPartyInsurance = seleniumResult.third_party_insurance?.trim() || null;
|
|
}
|
|
|
|
await storage.updatePatient(patient.id, updates);
|
|
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
|
|
outputResult.remainingBenefit = remainingBenefit;
|
|
outputResult.thirdPartyInsurance = updates.thirdPartyInsurance ?? null;
|
|
|
|
// Helper: save a single PDF to the patient's Cloud Storage Eligibility folder
|
|
const patientName = `${patient!.firstName ?? ""} ${patient!.lastName ?? ""}`.trim() || `Patient ${patient!.id}`;
|
|
const saveToGroup = async (pdfPath: string): Promise<number | string | null> => {
|
|
try {
|
|
const buffer = await fs.readFile(pdfPath);
|
|
const cloudFile = await storage.savePdfToCloudStorage(
|
|
userId,
|
|
patient!.id,
|
|
patientName,
|
|
"Eligibility",
|
|
path.basename(pdfPath),
|
|
buffer
|
|
);
|
|
return `cloud:${cloudFile.id}`;
|
|
} catch (e: any) {
|
|
console.error("[cmspProcessor] saveToGroup failed:", e.message);
|
|
return null;
|
|
}
|
|
};
|
|
|
|
// Save eligibility PDF
|
|
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
|
|
outputResult.pdfFileId = await saveToGroup(seleniumResult.pdf_path);
|
|
outputResult.pdfFilename = path.basename(seleniumResult.pdf_path);
|
|
outputResult.pdfUploadStatus = outputResult.pdfFileId ? "Eligibility PDF saved" : "Eligibility PDF upload failed";
|
|
} else {
|
|
outputResult.pdfUploadStatus = "No eligibility PDF returned by Selenium";
|
|
}
|
|
|
|
// Save member details PDF
|
|
if (seleniumResult.member_details_pdf_path?.endsWith(".pdf")) {
|
|
outputResult.memberDetailsPdfFileId = await saveToGroup(seleniumResult.member_details_pdf_path);
|
|
outputResult.memberDetailsPdfFilename = path.basename(seleniumResult.member_details_pdf_path);
|
|
}
|
|
|
|
// Save history PDF
|
|
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
|
|
outputResult.historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
|
|
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
|
|
}
|
|
|
|
// Save accumulator PDF
|
|
if (seleniumResult.accumulator_pdf_path?.endsWith(".pdf")) {
|
|
outputResult.accumulatorPdfFileId = await saveToGroup(seleniumResult.accumulator_pdf_path);
|
|
outputResult.accumulatorPdfFilename = path.basename(seleniumResult.accumulator_pdf_path);
|
|
}
|
|
} else {
|
|
outputResult.patientUpdateStatus = "Patient not found or missing ID; no update performed";
|
|
}
|
|
|
|
// Cleanup temp files
|
|
try {
|
|
if (seleniumResult.pdf_path) await emptyFolderContainingFile(seleniumResult.pdf_path);
|
|
} catch (e) {
|
|
console.error("[cmspProcessor] cleanup failed:", e);
|
|
}
|
|
|
|
return outputResult;
|
|
}
|