- 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>
79 lines
2.5 KiB
TypeScript
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;
|