/// A single line item in the balance computation. class BalanceEntry { final String label; final int amountCents; final int runningBalanceCents; const BalanceEntry({ required this.label, required this.amountCents, required this.runningBalanceCents, }); } /// Snapshot of a pay period item (gas, groceries, other, etc.) class ItemSnapshot { final String label; final int amountCents; // always positive const ItemSnapshot({required this.label, required this.amountCents}); } /// Snapshot of a paycheck with its associated period items. class PaycheckSnapshot { final String label; final int amountCents; final List items; const PaycheckSnapshot({ required this.label, required this.amountCents, required this.items, }); } /// Snapshot of a bill for balance computation. class BillSnapshot { final int id; final String label; final int defaultAmountCents; final int? amountOverrideCents; final bool isFreeform; const BillSnapshot({ required this.id, required this.label, required this.defaultAmountCents, this.amountOverrideCents, this.isFreeform = false, }); int get effectiveAmountCents => amountOverrideCents ?? defaultAmountCents; } /// All data needed to compute a month's rolling balance. class MonthSnapshot { final int openingBalanceCents; final List paychecks; final List priorityPayouts; final List bills; final List notablePayouts; const MonthSnapshot({ required this.openingBalanceCents, this.paychecks = const [], this.priorityPayouts = const [], this.bills = const [], this.notablePayouts = const [], }); } /// Result of the balance computation. class BalanceResult { final List entries; final int remainingCents; final int balanceBeforeCcCents; const BalanceResult({ required this.entries, required this.remainingCents, required this.balanceBeforeCcCents, }); } /// Computes the rolling balance for a month. Pure function. BalanceResult computeBalance({ required MonthSnapshot month, Map ccPayments = const {}, }) { final entries = []; var balance = month.openingBalanceCents; void add(String label, int amount) { balance += amount; entries.add(BalanceEntry( label: label, amountCents: amount, runningBalanceCents: balance, )); } entries.add(BalanceEntry( label: 'Opening Balance', amountCents: 0, runningBalanceCents: balance, )); for (final paycheck in month.paychecks) { add(paycheck.label, paycheck.amountCents); for (final item in paycheck.items) { add(item.label, -item.amountCents); } } for (final payout in month.priorityPayouts) { add(payout.label, -payout.amountCents); } // All bills (rule-computed amounts are already baked into defaultAmountCents by assembler) for (final bill in month.bills) { add(bill.label, -bill.effectiveAmountCents); } final balanceBeforeCc = balance; for (final payout in month.notablePayouts) { add(payout.label, -payout.amountCents); } return BalanceResult( entries: entries, remainingCents: balance, balanceBeforeCcCents: balanceBeforeCc, ); }