Files
DentalManagementMH07/apps/Backend/src/routes/payments-reports.ts
T
GiteadandClaude Sonnet 4.6 7360b1930b feat: add provider column, commission tracking, and report provider filter
- Claims & Payments: save npiProviderId when submitting MH claim; sync between claim and payment on update
- Claims table: add Provider column showing rendering provider name
- Payments table: add Provider column + purple Commissioned badge on status
- Claim edit modal: add Rendering Provider dropdown (defaults to Mary Scannell)
- Payment edit modal: add Rendering Provider dropdown + Commissioned metadata display
- Reports page: add Provider filter dropdown (dynamic from NPI providers settings)
- Reports page: remove Collections by Doctor report type and Select Doctor dropdown
- Commission section: new section in reports page with date range + provider filter, shows eligible paid claims/payments per provider, multi-select checkboxes, Pay Commission modal with print + save, marks payments as commissioned so they are excluded from future cycles
- DB: add CommissionBatch and CommissionBatchItem tables; backfill Payment.npiProviderId from linked claims
- Backend: PATCH /api/payments/:id/provider syncs to linked claim; PUT /api/claims/:id syncs to linked payment; new /api/commissions routes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:51:39 -04:00

231 lines
6.9 KiB
TypeScript
Executable File

import { Router } from "express";
import type { Request, Response } from "express";
import { storage } from "../storage";
const router = Router();
/**
* GET /api/payments-reports/summary
* optional query: from=YYYY-MM-DD&to=YYYY-MM-DD (ISO date strings)
*/
router.get("/summary", async (req: Request, res: Response): Promise<any> => {
try {
const from = req.query.from ? new Date(String(req.query.from)) : undefined;
const to = req.query.to ? new Date(String(req.query.to)) : undefined;
if (req.query.from && isNaN(from?.getTime() ?? NaN))
return res.status(400).json({ message: "Invalid 'from' date" });
if (req.query.to && isNaN(to?.getTime() ?? NaN))
return res.status(400).json({ message: "Invalid 'to' date" });
const npiProviderId = req.query.npiProviderId
? Number(req.query.npiProviderId)
: null;
const summary = await storage.getSummary(from, to, npiProviderId);
} catch (err: any) {
console.error(
"GET /api/payments-reports/summary error:",
err?.message ?? err,
err?.stack
);
res.status(500).json({ message: "Failed to fetch dashboard summary" });
}
});
/**
* GET /api/payments-reports/patient-balances
* query:
* - limit (default 25)
* - cursor (optional base64 cursor token)
* - from / to (optional ISO date strings) - filter payments by createdAt in the range (inclusive)
*/
router.get(
"/patients-with-balances",
async (req: Request, res: Response): Promise<any> => {
try {
const limit = Math.max(
1,
Math.min(200, parseInt(String(req.query.limit || "25"), 10))
);
const cursor =
typeof req.query.cursor === "string" ? String(req.query.cursor) : null;
const from = req.query.from
? new Date(String(req.query.from))
: undefined;
const to = req.query.to ? new Date(String(req.query.to)) : undefined;
if (req.query.from && isNaN(from?.getTime() ?? NaN)) {
return res.status(400).json({ message: "Invalid 'from' date" });
}
if (req.query.to && isNaN(to?.getTime() ?? NaN)) {
return res.status(400).json({ message: "Invalid 'to' date" });
}
const npiProviderId = req.query.npiProviderId
? Number(req.query.npiProviderId)
: null;
const data = await storage.getPatientsWithBalances(
limit,
cursor,
from,
to,
npiProviderId
);
// returns { balances, totalCount, nextCursor, hasMore }
res.json(data);
} catch (err: any) {
console.error(
"GET /api/payments-reports/patient-balances error:",
err?.message ?? err,
err?.stack
);
res.status(500).json({ message: "Failed to fetch patient balances" });
}
}
);
/**
* GET /api/payments-reports/by-doctor/balances
* Query params:
* - staffId (required)
* - limit (optional, default 25)
* - cursor (optional)
* - from/to (optional ISO date strings) - filter payments by createdAt in the range (inclusive)
*
* Response: { balances, totalCount, nextCursor, hasMore }
*/
router.get(
"/by-doctor/balances",
async (req: Request, res: Response): Promise<any> => {
try {
const staffIdRaw = req.query.staffId;
if (!staffIdRaw) {
return res
.status(400)
.json({ message: "Missing required 'staffId' query parameter" });
}
const staffId = Number(staffIdRaw);
if (!Number.isFinite(staffId) || staffId <= 0) {
return res
.status(400)
.json({ message: "Invalid 'staffId' query parameter" });
}
const limit = Math.max(
1,
Math.min(200, parseInt(String(req.query.limit || "25"), 10))
);
const cursor =
typeof req.query.cursor === "string" ? String(req.query.cursor) : null;
const from = req.query.from
? new Date(String(req.query.from))
: undefined;
const to = req.query.to ? new Date(String(req.query.to)) : undefined;
if (req.query.from && isNaN(from?.getTime() ?? NaN)) {
return res.status(400).json({ message: "Invalid 'from' date" });
}
if (req.query.to && isNaN(to?.getTime() ?? NaN)) {
return res.status(400).json({ message: "Invalid 'to' date" });
}
const npiProviderId = req.query.npiProviderId
? Number(req.query.npiProviderId)
: null;
// use the new storage method that returns only the paged balances
const balancesResult = await storage.getPatientsBalancesByDoctor(
staffId,
limit,
cursor,
from,
to,
npiProviderId
);
res.json({
balances: balancesResult?.balances ?? [],
totalCount: Number(balancesResult?.totalCount ?? 0),
nextCursor: balancesResult?.nextCursor ?? null,
hasMore: Boolean(balancesResult?.hasMore ?? false),
});
} catch (err: any) {
console.error(
"GET /api/payments-reports/by-doctor/balances error:",
err?.message ?? err,
err?.stack
);
res.status(500).json({
message: "Failed to fetch doctor balances",
detail: err?.message ?? String(err),
});
}
}
);
/**
* GET /api/payments-reports/by-doctor/summary
* Query params:
* - staffId (required)
* - from/to (optional ISO date strings) - filter payments by createdAt in the range (inclusive)
*
* Response: { totalPatients, totalOutstanding, totalCollected, patientsWithBalance }
*/
router.get(
"/by-doctor/summary",
async (req: Request, res: Response): Promise<any> => {
try {
const staffIdRaw = req.query.staffId;
if (!staffIdRaw) {
return res
.status(400)
.json({ message: "Missing required 'staffId' query parameter" });
}
const staffId = Number(staffIdRaw);
if (!Number.isFinite(staffId) || staffId <= 0) {
return res
.status(400)
.json({ message: "Invalid 'staffId' query parameter" });
}
const from = req.query.from
? new Date(String(req.query.from))
: undefined;
const to = req.query.to ? new Date(String(req.query.to)) : undefined;
if (req.query.from && isNaN(from?.getTime() ?? NaN)) {
return res.status(400).json({ message: "Invalid 'from' date" });
}
if (req.query.to && isNaN(to?.getTime() ?? NaN)) {
return res.status(400).json({ message: "Invalid 'to' date" });
}
const npiProviderId = req.query.npiProviderId
? Number(req.query.npiProviderId)
: null;
// use the new storage method that returns only the summary for the staff
const summary = await storage.getSummaryByDoctor(staffId, from, to, npiProviderId);
res.json(summary);
} catch (err: any) {
console.error(
"GET /api/payments-reports/by-doctor/summary error:",
err?.message ?? err,
err?.stack
);
res.status(500).json({
message: "Failed to fetch doctor summary",
detail: err?.message ?? String(err),
});
}
}
);
export default router;