feat: MassHealth PDF import auto-pays full balance + patient name fix
- PDF import now marks payments as PAID when MassHealth patient's mhPaidAmount >= totalBilled (no patient balance) - Newly created patients from MH vouchers get insuranceProvider = 'MassHealth' - Existing patients with blank insuranceProvider get it filled on import - Fix: update patient name from PDF if existing record has empty name - Various frontend/selenium/route updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,51 @@ import { prisma } from "@repo/db/client";
|
||||
import { PaymentStatusSchema } from "@repo/db/types";
|
||||
import * as paymentService from "../services/paymentService";
|
||||
import { callPythonSync } from "../queue/processors/_shared";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
|
||||
const VOUCHER_DIR = path.join(__dirname, "..", "..", "uploads", "MHVoucher");
|
||||
const IMPORTED_LOG = path.join(VOUCHER_DIR, "imported_vouchers.json");
|
||||
const OCR_BASE_URL = process.env.OCR_SERVICE_BASE_URL || "http://localhost:5003";
|
||||
|
||||
function loadImportedVouchers(): Set<string> {
|
||||
try {
|
||||
if (!fs.existsSync(IMPORTED_LOG)) return new Set();
|
||||
const data = JSON.parse(fs.readFileSync(IMPORTED_LOG, "utf-8"));
|
||||
return new Set(data.imported ?? []);
|
||||
} catch {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
|
||||
function saveImportedVoucher(voucher: string) {
|
||||
const existing = loadImportedVouchers();
|
||||
existing.add(voucher);
|
||||
fs.writeFileSync(IMPORTED_LOG, JSON.stringify({ imported: [...existing].sort() }, null, 2));
|
||||
}
|
||||
|
||||
async function extractAndImportVoucherPdf(filePath: string, userId: number): Promise<{ paymentIds: number[]; rowCount: number }> {
|
||||
const buffer = fs.readFileSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
const form = new FormData();
|
||||
form.append("files", buffer, { filename, contentType: "application/pdf", knownLength: buffer.length });
|
||||
|
||||
const resp = await axios.post<{ rows: any[] }>(
|
||||
`${OCR_BASE_URL}/extract/pdf/json`,
|
||||
form,
|
||||
{ headers: form.getHeaders(), maxBodyLength: Infinity, maxContentLength: Infinity, timeout: 120_000 }
|
||||
);
|
||||
|
||||
const rows = resp.data?.rows ?? [];
|
||||
console.log(`[mh-batch] Extracted ${rows.length} rows from ${filename}`);
|
||||
if (rows.length === 0) return { paymentIds: [], rowCount: 0 };
|
||||
|
||||
const paymentIds = await paymentService.pdfImportService.importRows(rows, userId);
|
||||
return { paymentIds, rowCount: rows.length };
|
||||
}
|
||||
|
||||
const paymentFilterSchema = z.object({
|
||||
from: z.string().datetime(),
|
||||
@@ -192,6 +237,76 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/payments/mh-batch-payment-check
|
||||
router.post(
|
||||
"/mh-batch-payment-check",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ message: "Unauthorized" });
|
||||
|
||||
const { fromDate, toDate } = req.body;
|
||||
|
||||
const credentials = await storage.getInsuranceCredentialByUserAndSiteKey(userId, "MH");
|
||||
if (!credentials) {
|
||||
return res.status(404).json({
|
||||
message: "No MassHealth credentials found. Please add them in Settings.",
|
||||
});
|
||||
}
|
||||
|
||||
const seleniumResult = await callPythonSync("/mh-batch-payment-check", {
|
||||
data: {
|
||||
massdhpUsername: credentials.username,
|
||||
massdhpPassword: credentials.password,
|
||||
fromDate,
|
||||
toDate,
|
||||
},
|
||||
});
|
||||
|
||||
// --- PDF import phase ---
|
||||
const alreadyImported = loadImportedVouchers();
|
||||
const allPdfs = fs.existsSync(VOUCHER_DIR)
|
||||
? fs.readdirSync(VOUCHER_DIR).filter((f) => f.endsWith(".pdf") && !f.startsWith("remittance_search_"))
|
||||
: [];
|
||||
const newPdfs = allPdfs.filter((f) => !alreadyImported.has(f.replace(".pdf", "")));
|
||||
|
||||
console.log(`[mh-batch] ${allPdfs.length} voucher PDFs total, ${newPdfs.length} new to import`);
|
||||
|
||||
const importResults: { voucher: string; paymentIds: number[]; rowCount: number; error?: string }[] = [];
|
||||
|
||||
for (const pdfFile of newPdfs) {
|
||||
const voucher = pdfFile.replace(".pdf", "");
|
||||
const filePath = path.join(VOUCHER_DIR, pdfFile);
|
||||
console.log(`[mh-batch] Extracting and importing: ${voucher}`);
|
||||
try {
|
||||
const { paymentIds, rowCount } = await extractAndImportVoucherPdf(filePath, userId);
|
||||
saveImportedVoucher(voucher);
|
||||
importResults.push({ voucher, paymentIds, rowCount });
|
||||
console.log(`[mh-batch] ✓ ${voucher}: ${rowCount} rows → ${paymentIds.length} payment(s)`);
|
||||
} catch (err: any) {
|
||||
const errMsg = err?.response?.data?.detail ?? err.message ?? "Unknown error";
|
||||
console.error(`[mh-batch] ✗ ${voucher}: ${errMsg}`);
|
||||
importResults.push({ voucher, paymentIds: [], rowCount: 0, error: errMsg });
|
||||
}
|
||||
}
|
||||
|
||||
const succeeded = importResults.filter((r) => !r.error);
|
||||
const failed = importResults.filter((r) => r.error);
|
||||
|
||||
return res.json({
|
||||
...seleniumResult,
|
||||
importResults,
|
||||
importSummary: newPdfs.length === 0
|
||||
? "All PDFs already imported."
|
||||
: `${succeeded.length} of ${newPdfs.length} PDFs imported successfully${failed.length ? `, ${failed.length} failed` : ""}.`,
|
||||
});
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : "MH batch payment check failed";
|
||||
return res.status(500).json({ message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/payments/:claimId
|
||||
router.post("/:claimId", async (req: Request, res: Response): Promise<any> => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user