feat: MH eligibility & history, CMSP eligibility & history & remaining

- Add MH Eligibility & History button: runs full MH eligibility flow then
  clicks member ID → service history, prints both PDFs via CDP, opens
  dual side-by-side PDF modal (eligibility auto-downloads, history does not)
- Add CMSP Eligibility & History & Remaining button: same flow plus
  navigates back to member details, clicks View Accumulator, prints
  accumulator PDF via CDP; opens 3-panel side-by-side PDF modal
- Generalize DualPdfPreviewModal to accept panels[] array (works for 2 or 3 PDFs)
- Auto-download eligibility PDF via direct API URL to avoid Chrome Safe
  Browsing pause on blob: URL downloads
- New backend processors, job types, and routes for both flows
- New Python Selenium workers with stable CSS selectors (ng-bind, href*)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gitead
2026-05-13 23:29:55 -04:00
parent 131733564e
commit 06526cd1bc
11 changed files with 1868 additions and 2 deletions

View File

@@ -16,6 +16,8 @@ import { runDeltaInsEligibilityProcessor } from "./processors/deltaInsEligibilit
import { runUnitedSCOEligibilityProcessor } from "./processors/unitedSCOEligibilityProcessor";
import { runDentaQuestEligibilityProcessor } from "./processors/dentaQuestEligibilityProcessor";
import { runCCAEligibilityProcessor } from "./processors/ccaEligibilityProcessor";
import { runEligibilityHistoryProcessor } from "./processors/eligibilityHistoryProcessor";
import { runCmspEligibilityHistoryRemainingProcessor } from "./processors/cmspEligibilityHistoryRemainingProcessor";
import type { SeleniumJobData, OcrJobData } from "./queues";
// ── Queue instances ──────────────────────────────────────────────────────────
@@ -144,6 +146,26 @@ export function enqueueSeleniumJob(data: SeleniumJobData): string {
job.id
);
}
if (jobType === "cmsp-eligibility-history-remaining-check") {
return runCmspEligibilityHistoryRemainingProcessor({
enrichedPayload: data.enrichedPayload,
userId: data.userId,
insuranceId: data.insuranceId!,
formFirstName: data.formFirstName,
formLastName: data.formLastName,
formDob: data.formDob,
});
}
if (jobType === "mh-eligibility-history-check") {
return runEligibilityHistoryProcessor({
enrichedPayload: data.enrichedPayload,
userId: data.userId,
insuranceId: data.insuranceId!,
formFirstName: data.formFirstName,
formLastName: data.formLastName,
formDob: data.formDob,
});
}
throw new Error(`Unknown selenium jobType: ${jobType}`);
});

View File

@@ -0,0 +1,161 @@
/**
* 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 | null;
pdfFilename?: string | null;
historyPdfFileId?: number | null;
historyPdfFilename?: string | null;
accumulatorPdfFileId?: number | null;
accumulatorPdfFilename?: 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;
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// Helper: save a single PDF to the patient's ELIGIBILITY_STATUS group
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
try {
const buffer = await fs.readFile(pdfPath);
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
if (created && typeof created === "object" && "id" in created) {
return Number(created.id);
}
return null;
} 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 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;
}

View File

@@ -0,0 +1,159 @@
/**
* Processor for "mh-eligibility-history-check" jobs.
* Runs MH eligibility + service history selenium flow and saves both PDFs
* 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 EligibilityHistoryProcessorInput {
enrichedPayload: any;
userId: number;
insuranceId: string;
formFirstName?: string;
formLastName?: string;
formDob?: string;
}
export interface EligibilityHistoryProcessorResult {
patientUpdateStatus?: string;
pdfUploadStatus?: string;
pdfFileId?: number | null;
historyPdfFileId?: number | null;
pdfFilename?: string | null;
historyPdfFilename?: string | null;
}
export async function runEligibilityHistoryProcessor(
input: EligibilityHistoryProcessorInput
): Promise<EligibilityHistoryProcessorResult> {
const { enrichedPayload, userId, insuranceId, formFirstName, formLastName, formDob } = input;
const seleniumResult = await callPythonSync("/mh-eligibility-history-check", {
data: enrichedPayload,
});
const outputResult: EligibilityHistoryProcessorResult = {};
// 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("[eligibilityHistoryProcessor] 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;
await storage.updatePatient(patient.id, updates);
outputResult.patientUpdateStatus = `Patient status updated to ${newStatus}`;
// Helper: save a PDF buffer to the patient's ELIGIBILITY_STATUS group
const saveToGroup = async (pdfPath: string): Promise<number | null> => {
try {
const buffer = await fs.readFile(pdfPath);
const groupTitleKey = "ELIGIBILITY_STATUS";
const groupTitle = "Eligibility Status";
let group = await storage.findPdfGroupByPatientTitleKey(patient!.id, groupTitleKey);
if (!group) group = await storage.createPdfGroup(patient!.id, groupTitle, groupTitleKey);
if (!group?.id) throw new Error("PDF group creation failed");
const created = await storage.createPdfFile(group.id, path.basename(pdfPath), buffer);
if (created && typeof created === "object" && "id" in created) {
return Number(created.id);
}
return null;
} catch (e: any) {
console.error("[eligibilityHistoryProcessor] saveToGroup failed:", e.message);
return null;
}
};
// Save eligibility PDF
let eligibilityPdfFileId: number | null = null;
if (seleniumResult.pdf_path?.endsWith(".pdf")) {
eligibilityPdfFileId = await saveToGroup(seleniumResult.pdf_path);
outputResult.pdfUploadStatus = eligibilityPdfFileId
? "Eligibility PDF saved"
: "Eligibility PDF upload failed";
outputResult.pdfFilename = path.basename(seleniumResult.pdf_path);
} else {
outputResult.pdfUploadStatus = "No eligibility PDF returned by Selenium";
}
outputResult.pdfFileId = eligibilityPdfFileId;
// Save history PDF
let historyPdfFileId: number | null = null;
if (seleniumResult.history_pdf_path?.endsWith(".pdf")) {
historyPdfFileId = await saveToGroup(seleniumResult.history_pdf_path);
outputResult.historyPdfFilename = path.basename(seleniumResult.history_pdf_path);
}
outputResult.historyPdfFileId = historyPdfFileId;
} 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("[eligibilityHistoryProcessor] cleanup failed:", e);
}
return outputResult;
}

View File

@@ -10,7 +10,9 @@ export type SeleniumJobType =
| "ddma-eligibility-check"
| "deltains-eligibility-check"
| "unitedsco-eligibility-check"
| "cca-eligibility-check";
| "cca-eligibility-check"
| "mh-eligibility-history-check"
| "cmsp-eligibility-history-remaining-check";
export interface SeleniumJobData {
jobType: SeleniumJobType;

View File

@@ -15,6 +15,7 @@ import {
} from "../../../../packages/db/types/patient-types";
import { formatDobForAgent } from "../utils/dateUtils";
import { seleniumQueue } from "../queue/queues";
import { enqueueSeleniumJob } from "../queue/jobRunner";
const router = Router();
@@ -166,6 +167,98 @@ router.post(
}
);
router.post(
"/eligibility-history-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" });
}
const data =
typeof req.body.data === "string" ? JSON.parse(req.body.data) : req.body.data;
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, "MH");
if (!credentials) {
return res.status(404).json({
error: "No MassHealth credentials found. Please update them in Settings.",
});
}
const insuranceId = String(data.memberId ?? "").trim();
if (!insuranceId) {
return res.status(400).json({ error: "Missing memberId" });
}
const enrichedData = {
...data,
massdhpUsername: credentials.username,
massdhpPassword: credentials.password,
};
const jobId = enqueueSeleniumJob({
jobType: "mh-eligibility-history-check",
userId: req.user.id,
socketId: req.body.socketId,
enrichedPayload: enrichedData,
insuranceId,
formFirstName: data.firstName,
formLastName: data.lastName,
formDob: data.dateOfBirth,
});
return res.json({ jobId, status: "queued" });
}
);
router.post(
"/cmsp-eligibility-history-remaining-check",
async (req: Request, res: Response): Promise<any> => {
if (!req.body.data) {
return res.status(400).json({ error: "Missing data for selenium" });
}
if (!req.user || !req.user.id) {
return res.status(401).json({ error: "Unauthorized: user info missing" });
}
const data =
typeof req.body.data === "string" ? JSON.parse(req.body.data) : req.body.data;
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(req.user.id, "MH");
if (!credentials) {
return res.status(404).json({
error: "No MassHealth credentials found. Please update them in Settings.",
});
}
const insuranceId = String(data.memberId ?? "").trim();
if (!insuranceId) {
return res.status(400).json({ error: "Missing memberId" });
}
const enrichedData = {
...data,
massdhpUsername: credentials.username,
massdhpPassword: credentials.password,
};
const jobId = enqueueSeleniumJob({
jobType: "cmsp-eligibility-history-remaining-check",
userId: req.user.id,
socketId: req.body.socketId,
enrichedPayload: enrichedData,
insuranceId,
formFirstName: data.firstName,
formLastName: data.lastName,
formDob: data.dateOfBirth,
});
return res.json({ jobId, status: "queued" });
}
);
router.post(
"/claim-status-check",
async (req: Request, res: Response): Promise<any> => {