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>
This commit is contained in:
Gitead
2026-05-15 23:51:39 -04:00
parent 25a20e8a16
commit 7360b1930b
366 changed files with 10822 additions and 388 deletions

View File

@@ -46,6 +46,7 @@ export const claimsStorage: IStorage = {
serviceLines: true,
staff: true,
claimFiles: true,
npiProvider: true,
},
});
},
@@ -63,7 +64,7 @@ export const claimsStorage: IStorage = {
status: { notIn: ["CANCELLED"] },
},
orderBy: { createdAt: "desc" },
include: { serviceLines: true, claimFiles: true, staff: true },
include: { serviceLines: true, claimFiles: true, staff: true, npiProvider: true },
}) as Promise<ClaimWithServiceLines | null>;
},
@@ -79,7 +80,7 @@ export const claimsStorage: IStorage = {
orderBy: { createdAt: "desc" },
skip: offset,
take: limit,
include: { serviceLines: true, staff: true, claimFiles: true },
include: { serviceLines: true, staff: true, claimFiles: true, npiProvider: true },
});
},

View File

@@ -0,0 +1,193 @@
import { prisma as db } from "@repo/db/client";
export interface EligiblePaymentRow {
id: number;
claimNumber: string | null;
patientName: string;
serviceDate: Date | null;
mhPaidAmount: number;
copayment: number;
collectionAmount: number;
paymentDate: Date;
isOcr: boolean;
}
export interface CommissionBatchRecord {
id: number;
npiProviderId: number;
totalCollection: number;
commissionAmount: number;
notes: string | null;
createdAt: Date;
providerName: string;
itemCount: number;
}
export const commissionsStorage = {
/**
* Eligible payments for commission — two sources:
* 1. Claims owned by this provider (Claim.npiProviderId) whose linked payment
* has actual collection and has not been commissioned yet.
* 2. OCR / PDF-import payments that have no linked claim but carry the
* provider on Payment.npiProviderId directly.
*/
async getEligiblePayments(
npiProviderId: number,
from?: Date | null,
to?: Date | null
): Promise<EligiblePaymentRow[]> {
const dateFilter: any = {};
if (from) dateFilter.gte = from;
if (to) {
const toEnd = new Date(to);
toEnd.setUTCHours(23, 59, 59, 999);
dateFilter.lte = toEnd;
}
const hasDateFilter = Object.keys(dateFilter).length > 0;
// ── 1. Claim-centric: provider lives on the Claim ──────────────────────
const claimWhere: any = {
npiProviderId,
payment: {
is: {
commissionBatchItems: { none: {} },
OR: [{ mhPaidAmount: { gt: 0 } }, { copayment: { gt: 0 } }],
...(hasDateFilter ? { createdAt: dateFilter } : {}),
},
},
};
const claims = await db.claim.findMany({
where: claimWhere,
orderBy: { id: "desc" },
include: {
payment: {
select: { id: true, mhPaidAmount: true, copayment: true, createdAt: true, notes: true },
},
},
});
const rows: EligiblePaymentRow[] = claims
.filter((c) => c.payment !== null)
.map((c) => {
const p = c.payment!;
const mh = Number(p.mhPaidAmount ?? 0);
const co = Number(p.copayment ?? 0);
return {
id: p.id,
claimNumber: c.claimNumber ?? null,
patientName: c.patientName,
serviceDate: c.serviceDate,
mhPaidAmount: mh,
copayment: co,
collectionAmount: mh + co,
paymentDate: p.createdAt,
isOcr: false,
};
});
// ── 2. OCR / PDF payments — no claim, matched by Payment.npiProviderId ──
const ocrWhere: any = {
npiProviderId,
claimId: null,
commissionBatchItems: { none: {} },
OR: [{ mhPaidAmount: { gt: 0 } }, { copayment: { gt: 0 } }],
...(hasDateFilter ? { createdAt: dateFilter } : {}),
};
const ocrPayments = await db.payment.findMany({
where: ocrWhere,
orderBy: { createdAt: "desc" },
include: {
patient: { select: { firstName: true, lastName: true } },
},
});
for (const p of ocrPayments) {
const mh = Number(p.mhPaidAmount ?? 0);
const co = Number(p.copayment ?? 0);
const patientName =
`${p.patient?.firstName ?? ""} ${p.patient?.lastName ?? ""}`.trim() ||
"Unknown";
rows.push({
id: p.id,
claimNumber: null,
patientName,
serviceDate: null,
mhPaidAmount: mh,
copayment: co,
collectionAmount: mh + co,
paymentDate: p.createdAt,
isOcr: true,
});
}
// Sort combined list by most recent first
rows.sort((a, b) => b.paymentDate.getTime() - a.paymentDate.getTime());
return rows;
},
/** Create a commission batch, linking the selected payments */
async createCommissionBatch(data: {
npiProviderId: number;
paymentIds: number[];
totalCollection: number;
commissionAmount: number;
notes?: string;
}) {
// Verify none of the payments are already commissioned
const alreadyDone = await db.commissionBatchItem.count({
where: { paymentId: { in: data.paymentIds } },
});
if (alreadyDone > 0) {
throw new Error("One or more selected payments have already been commissioned.");
}
// Calculate collection amounts per payment
const payments = await db.payment.findMany({
where: { id: { in: data.paymentIds } },
select: { id: true, mhPaidAmount: true, copayment: true },
});
const batch = await db.commissionBatch.create({
data: {
npiProviderId: data.npiProviderId,
totalCollection: data.totalCollection,
commissionAmount: data.commissionAmount,
notes: data.notes ?? null,
items: {
create: payments.map((p) => ({
paymentId: p.id,
collectionAmount: Number(p.mhPaidAmount ?? 0) + Number(p.copayment ?? 0),
})),
},
},
include: { items: true, npiProvider: true },
});
return batch;
},
/** Past commission batches for a provider */
async getCommissionBatches(npiProviderId?: number): Promise<CommissionBatchRecord[]> {
const batches = await db.commissionBatch.findMany({
where: npiProviderId ? { npiProviderId } : undefined,
orderBy: { createdAt: "desc" },
include: {
npiProvider: { select: { providerName: true } },
_count: { select: { items: true } },
},
});
return batches.map((b) => ({
id: b.id,
npiProviderId: b.npiProviderId,
totalCollection: Number(b.totalCollection),
commissionAmount: Number(b.commissionAmount),
notes: b.notes,
createdAt: b.createdAt,
providerName: b.npiProvider.providerName,
itemCount: b._count.items,
}));
},
};

View File

@@ -23,6 +23,7 @@ import { officeHoursStorage } from "./office-hours-storage";
import { officeContactStorage } from "./office-contact-storage";
import { procedureTimeslotStorage } from "./procedure-timeslot-storage";
import { insuranceContactStorage } from "./insurance-contact-storage";
import { commissionsStorage } from "./commissions-storage";
export const storage = {
@@ -49,6 +50,7 @@ export const storage = {
...officeContactStorage,
...procedureTimeslotStorage,
...insuranceContactStorage,
...commissionsStorage,
};

View File

@@ -5,10 +5,10 @@ import {
} from "../../../../packages/db/types/payments-reports-types";
export interface IPaymentsReportsStorage {
// summary now returns an extra field patientsWithBalance
getSummary(
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
): Promise<{
totalPatients: number;
totalOutstanding: number;
@@ -16,44 +16,28 @@ export interface IPaymentsReportsStorage {
patientsWithBalance: number;
}>;
/**
* Cursor-based pagination:
* - limit: page size
* - cursorToken: base64(JSON) token for last-seen row (or null for first page)
* - from/to: optional date range filter applied to Payment."createdAt"
*/
getPatientsWithBalances(
limit: number,
cursorToken?: string | null,
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
): Promise<GetPatientBalancesResult>;
/**
* Returns the paginated patient balances for a specific staff (doctor).
* Same semantics / columns / ordering / cursor behavior as the previous combined function.
*
* - staffId required
* - limit: page size
* - cursorToken: optional base64 cursor (must have been produced for same staffId)
* - from/to: optional date range applied to Payment."createdAt"
*/
getPatientsBalancesByDoctor(
staffId: number,
limit: number,
cursorToken?: string | null,
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
): Promise<GetPatientBalancesResult>;
/**
* Returns only the summary object for the given staff (doctor).
* Same summary shape as getSummary(), but scoped to claims/payments associated with the given staffId.
*/
getSummaryByDoctor(
staffId: number,
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
): Promise<{
totalPatients: number;
totalOutstanding: number;
@@ -129,208 +113,72 @@ function decodeCursor(token?: string | null): {
}
export const paymentsReportsStorage: IPaymentsReportsStorage = {
async getSummary(from?: Date | null, to?: Date | null) {
async getSummary(from?: Date | null, to?: Date | null, npiProviderId?: number | null) {
try {
const hasFrom = from !== undefined && from !== null;
const hasTo = to !== undefined && to !== null;
const fromStart = isoStartOfDayLiteral(from);
const toNextStart = isoStartOfNextDayLiteral(to);
// Use inclusive start-of-day for 'from' and exclusive start-of-next-day for 'to'
const fromStart = isoStartOfDayLiteral(from); // 'YYYY-MM-DDT00:00:00.000Z'
const toNextStart = isoStartOfNextDayLiteral(to); // 'YYYY-MM-DDT00:00:00.000Z' of next day
// Build a shared WHERE clause for Payment rows
const conditions: string[] = [];
if (from) conditions.push(`pay."createdAt" >= ${fromStart}`);
if (to) conditions.push(`pay."createdAt" <= ${toNextStart}`);
if (npiProviderId) conditions.push(`pay."npiProviderId" = ${Number(npiProviderId)}`);
const payWhereClause = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
// totalPatients: distinct patients who had payments in the date range
let patientsCountSql = "";
if (hasFrom && hasTo) {
patientsCountSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id
FROM "Payment" pay
WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}
GROUP BY pay."patientId"
) t
`;
} else if (hasFrom) {
patientsCountSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id
FROM "Payment" pay
WHERE pay."createdAt" >= ${fromStart}
GROUP BY pay."patientId"
) t
`;
} else if (hasTo) {
patientsCountSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id
FROM "Payment" pay
WHERE pay."createdAt" <= ${toNextStart}
GROUP BY pay."patientId"
) t
`;
} else {
patientsCountSql = `SELECT COUNT(DISTINCT "patientId")::int AS cnt FROM "Payment"`;
}
const patientsCntRows = (await prisma.$queryRawUnsafe(
patientsCountSql
)) as { cnt: number }[];
const patientsCountSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id
FROM "Payment" pay
${payWhereClause}
GROUP BY pay."patientId"
) t
`;
const patientsCntRows = (await prisma.$queryRawUnsafe(patientsCountSql)) as { cnt: number }[];
const totalPatients = patientsCntRows?.[0]?.cnt ?? 0;
// totalOutstanding: totalBilled - mhPaidAmount - copayment - adjustment
let outstandingSql = "";
if (hasFrom && hasTo) {
outstandingSql = `
SELECT COALESCE(SUM(
GREATEST(COALESCE(pm.total_charges,0) - COALESCE(pm.mh_paid,0) - COALESCE(pm.copayment,0) - COALESCE(pm.adjustment,0), 0)
),0)::numeric(14,2) AS outstanding
FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}
GROUP BY pay."patientId"
) pm
`;
} else if (hasFrom) {
outstandingSql = `
SELECT COALESCE(SUM(
GREATEST(COALESCE(pm.total_charges,0) - COALESCE(pm.mh_paid,0) - COALESCE(pm.copayment,0) - COALESCE(pm.adjustment,0), 0)
),0)::numeric(14,2) AS outstanding
FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
WHERE pay."createdAt" >= ${fromStart}
GROUP BY pay."patientId"
) pm
`;
} else if (hasTo) {
outstandingSql = `
SELECT COALESCE(SUM(
GREATEST(COALESCE(pm.total_charges,0) - COALESCE(pm.mh_paid,0) - COALESCE(pm.copayment,0) - COALESCE(pm.adjustment,0), 0)
),0)::numeric(14,2) AS outstanding
FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
WHERE pay."createdAt" <= ${toNextStart}
GROUP BY pay."patientId"
) pm
`;
} else {
outstandingSql = `
SELECT COALESCE(SUM(
GREATEST(COALESCE(pm.total_charges,0) - COALESCE(pm.mh_paid,0) - COALESCE(pm.copayment,0) - COALESCE(pm.adjustment,0), 0)
),0)::numeric(14,2) AS outstanding
FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
GROUP BY pay."patientId"
) pm
`;
}
const outstandingRows = (await prisma.$queryRawUnsafe(
outstandingSql
)) as { outstanding: string }[];
const outstandingSql = `
SELECT COALESCE(SUM(
GREATEST(COALESCE(pm.total_charges,0) - COALESCE(pm.mh_paid,0) - COALESCE(pm.copayment,0) - COALESCE(pm.adjustment,0), 0)
),0)::numeric(14,2) AS outstanding
FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
${payWhereClause}
GROUP BY pay."patientId"
) pm
`;
const outstandingRows = (await prisma.$queryRawUnsafe(outstandingSql)) as { outstanding: string }[];
const totalOutstanding = Number(outstandingRows?.[0]?.outstanding ?? 0);
// totalCollected: mhPaidAmount + copayment (adjustment is write-off, not collected)
let collSql = "";
if (hasFrom && hasTo) {
collSql = `SELECT COALESCE(SUM(COALESCE("mhPaidAmount",0) + "copayment"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" >= ${fromStart} AND "createdAt" <= ${toNextStart}`;
} else if (hasFrom) {
collSql = `SELECT COALESCE(SUM(COALESCE("mhPaidAmount",0) + "copayment"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" >= ${fromStart}`;
} else if (hasTo) {
collSql = `SELECT COALESCE(SUM(COALESCE("mhPaidAmount",0) + "copayment"),0)::numeric(14,2) AS collected FROM "Payment" WHERE "createdAt" <= ${toNextStart}`;
} else {
collSql = `SELECT COALESCE(SUM(COALESCE("mhPaidAmount",0) + "copayment"),0)::numeric(14,2) AS collected FROM "Payment"`;
}
const collRows = (await prisma.$queryRawUnsafe(collSql)) as {
collected: string;
}[];
const collSql = `
SELECT COALESCE(SUM(COALESCE(pay."mhPaidAmount",0) + pay."copayment"),0)::numeric(14,2) AS collected
FROM "Payment" pay
${payWhereClause}
`;
const collRows = (await prisma.$queryRawUnsafe(collSql)) as { collected: string }[];
const totalCollected = Number(collRows?.[0]?.collected ?? 0);
// patientsWithBalance: patients where (billed - mhPaid - copayment - adjustment) > 0
let patientsWithBalanceSql = "";
if (hasFrom && hasTo) {
patientsWithBalanceSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}
GROUP BY pay."patientId"
) t
WHERE (COALESCE(t.total_charges,0) - COALESCE(t.mh_paid,0) - COALESCE(t.copayment,0) - COALESCE(t.adjustment,0)) > 0
`;
} else if (hasFrom) {
patientsWithBalanceSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
WHERE pay."createdAt" >= ${fromStart}
GROUP BY pay."patientId"
) t
WHERE (COALESCE(t.total_charges,0) - COALESCE(t.mh_paid,0) - COALESCE(t.copayment,0) - COALESCE(t.adjustment,0)) > 0
`;
} else if (hasTo) {
patientsWithBalanceSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
WHERE pay."createdAt" <= ${toNextStart}
GROUP BY pay."patientId"
) t
WHERE (COALESCE(t.total_charges,0) - COALESCE(t.mh_paid,0) - COALESCE(t.copayment,0) - COALESCE(t.adjustment,0)) > 0
`;
} else {
patientsWithBalanceSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
GROUP BY pay."patientId"
) t
WHERE (COALESCE(t.total_charges,0) - COALESCE(t.mh_paid,0) - COALESCE(t.copayment,0) - COALESCE(t.adjustment,0)) > 0
`;
}
const pwbRows = (await prisma.$queryRawUnsafe(
patientsWithBalanceSql
)) as { cnt: number }[];
const patientsWithBalanceSql = `
SELECT COUNT(*)::int AS cnt FROM (
SELECT pay."patientId" AS patient_id,
SUM(pay."totalBilled")::numeric(14,2) AS total_charges,
SUM(COALESCE(pay."mhPaidAmount",0))::numeric(14,2) AS mh_paid,
SUM(pay."copayment")::numeric(14,2) AS copayment,
SUM(pay."adjustment")::numeric(14,2) AS adjustment
FROM "Payment" pay
${payWhereClause}
GROUP BY pay."patientId"
) t
WHERE (COALESCE(t.total_charges,0) - COALESCE(t.mh_paid,0) - COALESCE(t.copayment,0) - COALESCE(t.adjustment,0)) > 0
`;
const pwbRows = (await prisma.$queryRawUnsafe(patientsWithBalanceSql)) as { cnt: number }[];
const patientsWithBalance = pwbRows?.[0]?.cnt ?? 0;
return {
totalPatients,
totalOutstanding,
totalCollected,
patientsWithBalance,
};
return { totalPatients, totalOutstanding, totalCollected, patientsWithBalance };
} catch (err) {
console.error("[paymentsReportsStorage.getSummary] error:", err);
throw err;
@@ -348,7 +196,8 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = {
limit = 25,
cursorToken?: string | null,
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
) {
try {
type RawRow = {
@@ -373,15 +222,12 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = {
const fromStart = isoStartOfDayLiteral(from); // 'YYYY-MM-DDT00:00:00.000Z'
const toNextStart = isoStartOfNextDayLiteral(to); // 'YYYY-MM-DDT00:00:00.000Z' of next day
// Build payment subquery (aggregated payments by patient, filtered by createdAt if provided)
const paymentWhereClause =
hasFrom && hasTo
? `WHERE pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}`
: hasFrom
? `WHERE pay."createdAt" >= ${fromStart}`
: hasTo
? `WHERE pay."createdAt" <= ${toNextStart}`
: "";
// Build payment subquery WHERE clause (date + optional provider filter)
const payConditions: string[] = [];
if (hasFrom) payConditions.push(`pay."createdAt" >= ${fromStart}`);
if (hasTo) payConditions.push(`pay."createdAt" <= ${toNextStart}`);
if (npiProviderId) payConditions.push(`pay."npiProviderId" = ${Number(npiProviderId)}`);
const paymentWhereClause = payConditions.length ? `WHERE ${payConditions.join(" AND ")}` : "";
const pmSubquery = `
(
@@ -542,7 +388,8 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = {
limit = 25,
cursorToken?: string | null,
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
): Promise<{
balances: PatientBalanceRow[];
totalCount: number;
@@ -571,15 +418,14 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = {
const fromStart = isoStartOfDayLiteral(from);
const toNextStart = isoStartOfNextDayLiteral(to);
// Filter payments by createdAt (time window) when provided
const paymentTimeFilter =
hasFrom && hasTo
? `AND pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}`
: hasFrom
? `AND pay."createdAt" >= ${fromStart}`
: hasTo
? `AND pay."createdAt" <= ${toNextStart}`
: "";
// Filter payments by createdAt and/or provider
const payAndConditions: string[] = [];
if (hasFrom) payAndConditions.push(`pay."createdAt" >= ${fromStart}`);
if (hasTo) payAndConditions.push(`pay."createdAt" <= ${toNextStart}`);
if (npiProviderId) payAndConditions.push(`pay."npiProviderId" = ${Number(npiProviderId)}`);
const paymentTimeFilter = payAndConditions.length
? `AND ${payAndConditions.join(" AND ")}`
: "";
// Keyset predicate — prefer numeric epoch-ms comparison for stability
let pageKeysetPredicate = "";
@@ -801,7 +647,8 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = {
async getSummaryByDoctor(
staffId: number,
from?: Date | null,
to?: Date | null
to?: Date | null,
npiProviderId?: number | null
): Promise<{
totalPatients: number;
totalOutstanding: number;
@@ -818,14 +665,13 @@ export const paymentsReportsStorage: IPaymentsReportsStorage = {
const fromStart = isoStartOfDayLiteral(from);
const toNextStart = isoStartOfNextDayLiteral(to);
const paymentTimeFilter =
hasFrom && hasTo
? `AND pay."createdAt" >= ${fromStart} AND pay."createdAt" <= ${toNextStart}`
: hasFrom
? `AND pay."createdAt" >= ${fromStart}`
: hasTo
? `AND pay."createdAt" <= ${toNextStart}`
: "";
const drAndConditions: string[] = [];
if (hasFrom) drAndConditions.push(`pay."createdAt" >= ${fromStart}`);
if (hasTo) drAndConditions.push(`pay."createdAt" <= ${toNextStart}`);
if (npiProviderId) drAndConditions.push(`pay."npiProviderId" = ${Number(npiProviderId)}`);
const paymentTimeFilter = drAndConditions.length
? `AND ${drAndConditions.join(" AND ")}`
: "";
const summaryQuery = `
WITH

View File

@@ -109,6 +109,8 @@ export const paymentsStorage: IStorage = {
},
updatedBy: true,
patient: true,
npiProvider: true,
commissionBatchItems: { include: { commissionBatch: true } },
},
});
@@ -144,6 +146,8 @@ export const paymentsStorage: IStorage = {
},
updatedBy: true,
patient: true,
npiProvider: true,
commissionBatchItems: { include: { commissionBatch: true } },
},
});
@@ -177,6 +181,8 @@ export const paymentsStorage: IStorage = {
},
updatedBy: true,
patient: true,
npiProvider: true,
commissionBatchItems: { include: { commissionBatch: true } },
},
});
@@ -213,6 +219,8 @@ export const paymentsStorage: IStorage = {
},
updatedBy: true,
patient: true,
npiProvider: true,
commissionBatchItems: { include: { commissionBatch: true } },
},
});
@@ -251,6 +259,8 @@ export const paymentsStorage: IStorage = {
},
updatedBy: true,
patient: true,
npiProvider: true,
commissionBatchItems: { include: { commissionBatch: true } },
},
});