Files
DentalManagementMH06/apps/Backend/src/routes/paymentPdfExtraction.ts
Gitead dd0df4a435 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>
2026-05-07 12:53:50 -04:00

79 lines
2.5 KiB
TypeScript

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;