Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
96 lines
2.4 KiB
TypeScript
96 lines
2.4 KiB
TypeScript
// 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 };
|
|
});
|
|
}
|