feat: payment PDF extraction, import, and remittance tracking
- Add Upload Payment Documents section with Extract & Download (Excel) and Extract & Import (database) buttons - PDF extractor (pdfplumber) parses MassHealth RA PDFs: two-pass strategy joins summary-page ICN/patient map with detail-page procedure data (CDT code, paid code, tooth, date, allowed amount) - RA cover-page summary (Payee ID, RA #, Payment Amount, etc.) included as separate Excel sheet; numeric values written as numbers - Backend PDF import route groups rows by Member #, finds/creates patient, creates Payment + ServiceLines with ICN per procedure - Add icn, paidCode, allowedAmount fields to ServiceLine schema - Payments table: status simplified to Paid in Full / Balance; adjustment auto-computed on mhPaidAmount/copayment change; Paid in Full and Revert buttons with confirmation dialogs - Edit Payment modal: shows ICN, Paid Code, Allowed Amount per line - PDF Import badge distinguishes from OCR imports in payments table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
78
apps/Backend/src/routes/paymentPdfExtraction.ts
Normal file
78
apps/Backend/src/routes/paymentPdfExtraction.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Router, Request, Response } from "express";
|
||||
import multer from "multer";
|
||||
import axios from "axios";
|
||||
import FormData from "form-data";
|
||||
import { pdfImportService, PdfRow } from "../services/paymentService";
|
||||
|
||||
const router = Router();
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
const OCR_BASE_URL = process.env.OCR_SERVICE_BASE_URL || "http://localhost:5003";
|
||||
|
||||
router.post(
|
||||
"/extract",
|
||||
upload.array("files"),
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
const files = req.files as Express.Multer.File[] | undefined;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
return res.status(400).json({ error: "No PDF files uploaded. Use field name 'files'." });
|
||||
}
|
||||
|
||||
const bad = files.filter((f) => !f.originalname.toLowerCase().endsWith(".pdf"));
|
||||
if (bad.length) {
|
||||
return res.status(415).json({ error: `Only PDF files are allowed: ${bad.map((b) => b.originalname).join(", ")}` });
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
for (const f of files) {
|
||||
form.append("files", f.buffer, {
|
||||
filename: f.originalname,
|
||||
contentType: "application/pdf",
|
||||
knownLength: f.size,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await axios.post<{ rows: any[] }>(
|
||||
`${OCR_BASE_URL}/extract/pdf/json`,
|
||||
form,
|
||||
{
|
||||
headers: form.getHeaders(),
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Infinity,
|
||||
timeout: 60_000,
|
||||
}
|
||||
);
|
||||
return res.json({ rows: resp.data?.rows ?? [], headers: resp.data?.headers ?? [] });
|
||||
} catch (err: any) {
|
||||
const status = err?.response?.status;
|
||||
const detail = err?.response?.data?.detail || err?.message || "Unknown error";
|
||||
return res.status(500).json({ error: `PDF extraction failed${status ? ` (${status})` : ""}: ${detail}` });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /api/payment-pdf/import
|
||||
// Accepts already-extracted rows and imports them into the database.
|
||||
router.post(
|
||||
"/import",
|
||||
async (req: Request, res: Response): Promise<any> => {
|
||||
const userId = req.user?.id;
|
||||
if (!userId) return res.status(401).json({ error: "Unauthorized" });
|
||||
|
||||
const { rows } = req.body as { rows: PdfRow[] };
|
||||
if (!rows || !Array.isArray(rows) || rows.length === 0) {
|
||||
return res.status(400).json({ error: "No rows provided." });
|
||||
}
|
||||
|
||||
try {
|
||||
const paymentIds = await pdfImportService.importRows(rows, userId);
|
||||
return res.json({ message: "PDF rows imported successfully", paymentIds });
|
||||
} catch (err: any) {
|
||||
return res.status(500).json({ error: err?.message ?? "Import failed" });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user