import '../database/daos/month_dao.dart'; import '../database/database.dart'; import 'balance_calculator.dart'; import 'rule_solver.dart'; /// Result of assembling and computing a full month. class ComputedMonth { final MonthData data; final BalanceResult balance; final RuleSolverResult ruleResult; final MathSourceValues? mathSources; const ComputedMonth({ required this.data, required this.balance, required this.ruleResult, this.mathSources, }); int get remainingCents => balance.remainingCents; } /// Bridges DB data → pure function computation. /// /// [rules] are the per-month PaymentRules for this month. ComputedMonth assembleMonth(MonthData data, {List rules = const []}) { // Run rule solver RuleSolverResult ruleResult = RuleSolverResult.empty; MathSourceValues? mathSources; final ruleBillPayments = {}; // billDefinitionId → payment cents if (rules.isNotEmpty) { final ruleBillIds = rules.map((r) => r.billDefinitionId).toSet(); mathSources = computeMathSources( data: data, ruleBillIds: ruleBillIds, bufferCents: data.plan.bufferAmountCents, ); final billModes = {}; final billOutstanding = {}; for (final bill in data.bills) { billModes[bill.id] = bill.paymentMode; billOutstanding[bill.id] = bill.outstandingBalanceCents; } ruleResult = solveRules( sources: mathSources, rules: rules, billModes: billModes, billOutstanding: billOutstanding, ); ruleBillPayments.addAll(ruleResult.billPayments); } final snapshot = _buildSnapshot(data, ruleBillPayments); final balance = computeBalance(month: snapshot); // Reorder entries by date and recompute running balances final reordered = _reorderByDate(balance.entries, data); return ComputedMonth( data: data, balance: BalanceResult( entries: reordered, remainingCents: balance.remainingCents, balanceBeforeCcCents: balance.balanceBeforeCcCents, ), ruleResult: ruleResult, mathSources: mathSources, ); } List _reorderByDate(List entries, MonthData data) { if (entries.isEmpty) return entries; final dateMap = {}; final year = data.plan.year; final month = data.plan.month; for (final pw in data.paychecks) { dateMap['Paycheck #${pw.paycheck.sequence}'] = pw.paycheck.date; for (final item in pw.items) { dateMap[item.description] = item.date; } } for (final p in data.priorityPayouts) { dateMap[p.description] = p.date; } for (final p in data.notablePayouts) { dateMap[p.description] = p.date; } for (final b in data.bills) { if (b.isOutside) continue; final label = b.subtitle != null ? '${b.name} (${b.subtitle})' : b.name; final day = b.dueDayOverride ?? b.dueDay; dateMap[label] = DateTime(year, month, day); } final opening = entries.first; final rest = entries.sublist(1); rest.sort((a, b) { final dateA = dateMap[a.label]; final dateB = dateMap[b.label]; if (dateA == null && dateB == null) return 0; if (dateA == null) return -1; if (dateB == null) return 1; final cmp = dateA.compareTo(dateB); if (cmp != 0) return cmp; return b.amountCents.compareTo(a.amountCents); }); var balance = opening.runningBalanceCents; final result = [opening]; for (final entry in rest) { balance += entry.amountCents; result.add(BalanceEntry( label: entry.label, amountCents: entry.amountCents, runningBalanceCents: balance, )); } return result; } MonthSnapshot _buildSnapshot(MonthData data, Map ruleBillPayments) { return MonthSnapshot( openingBalanceCents: data.plan.openingBalanceCents, paychecks: data.paychecks.map((pw) { return PaycheckSnapshot( label: 'Paycheck #${pw.paycheck.sequence}', amountCents: pw.paycheck.amountCents, items: pw.items.map((i) { return ItemSnapshot(label: i.description, amountCents: i.amountCents); }).toList(), ); }).toList(), priorityPayouts: data.priorityPayouts.map((p) { return ItemSnapshot(label: p.description, amountCents: p.amountCents); }).toList(), bills: data.bills.where((b) => !b.isOutside).map((b) { final rulePayment = ruleBillPayments[b.id]; return BillSnapshot( id: b.id, label: b.subtitle != null ? '${b.name} (${b.subtitle})' : b.name, // Override takes precedence, then rule payment, then default defaultAmountCents: b.amountOverrideCents ?? rulePayment ?? b.defaultAmountCents, amountOverrideCents: null, isFreeform: false, ); }).toList(), notablePayouts: data.notablePayouts.map((p) { return ItemSnapshot(label: p.description, amountCents: p.amountCents); }).toList(), ); }