diff --git a/src/lib/ledger.ts b/src/lib/ledger.ts new file mode 100644 index 0000000..a937f60 --- /dev/null +++ b/src/lib/ledger.ts @@ -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 = { + bank: "Bank Fees", + interest: "Interest", + late: "Late Fees", + legal: "Legal Fees", + admin: "Admin Fees", + viol: "Violations", + assess: "Assessments", +}; + +export const BUCKET_SHORT: Record = { + 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) => + 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 & { total: number } { + const bal: Record = { + 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 }; + }); +}