Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 01:49:02 +00:00
co-authored by renee-png
parent f085202d68
commit da390accae
+95
View File
@@ -0,0 +1,95 @@
// Priority order for payment allocation: Bank → Interest → Late → Legal → Admin → Violations → Assessments
export const BUCKETS = ["bank", "interest", "late", "legal", "admin", "viol", "assess"] as const;
export type Bucket = (typeof BUCKETS)[number];
export const BUCKET_LABEL: Record<Bucket, string> = {
bank: "Bank Fees",
interest: "Interest",
late: "Late Fees",
legal: "Legal Fees",
admin: "Admin Fees",
viol: "Violations",
assess: "Assessments",
};
export const BUCKET_SHORT: Record<Bucket, string> = {
bank: "Bank",
interest: "Int",
late: "Late",
legal: "Legal",
admin: "Admin",
viol: "Viol",
assess: "Assess",
};
export interface LedgerEntry {
id: string;
collection_id: string;
entry_date: string;
description: string | null;
account: string | null;
assess: number;
late: number;
admin: number;
legal: number;
viol: number;
interest: number;
bank: number;
payment: number;
debit?: number;
credit?: number;
transaction_type?: string;
}
export const num = (v: any) => Number(v) || 0;
/** Sum of all charge columns for one row */
export const rowCharges = (e: Pick<LedgerEntry, Bucket>) =>
BUCKETS.reduce((s, b) => s + num((e as any)[b]), 0);
/** Compute live remaining balance per bucket given chronological entries (FIFO payment allocation by priority). */
export function computeBucketBalances(
entries: LedgerEntry[],
opening = 0,
): Record<Bucket, number> & { total: number } {
const bal: Record<Bucket, number> = {
bank: 0,
interest: 0,
late: 0,
legal: 0,
admin: 0,
viol: 0,
assess: 0,
};
// Treat opening balance as outstanding assessments
bal.assess += opening;
for (const e of entries) {
BUCKETS.forEach((b) => {
bal[b] += num((e as any)[b]);
});
let pay = num(e.payment);
if (pay > 0) {
for (const b of BUCKETS) {
if (pay <= 0) break;
if (bal[b] <= 0) continue;
const take = Math.min(bal[b], pay);
bal[b] -= take;
pay -= take;
}
// any leftover credit goes against assessments (overpayment)
if (pay > 0) bal.assess -= pay;
}
}
const total = BUCKETS.reduce((s, b) => s + bal[b], 0);
return { ...bal, total };
}
/** Running balance per row */
export function withRunningBalance(entries: LedgerEntry[], opening = 0) {
let bal = opening;
return entries.map((e) => {
bal += rowCharges(e) - num(e.payment);
return { ...e, runningBalance: bal };
});
}