import type { BudgetYearRow } from "../../shared/types"; export type BudgetSectionType = "income" | "expense" | "transfer"; /** * Sign multiplier applied to budgeted (months/annual) magnitudes. Budgeted * amounts are entered/stored as positive magnitudes regardless of type and * flipped for display by category type — mirrors `BudgetTable`'s `signFor`. */ export function signForBudgetType(type: BudgetSectionType): 1 | -1 { return type === "expense" ? -1 : 1; } /** * Aggregate of the budget grid's 3 comparable column groups, already signed: * 12 monthly (budgeted) totals, the annual (budgeted) total, and the * previous-year (actual) total. */ export interface BudgetTotals { /** index 0-11 = Jan-Dec, signed budgeted total */ months: number[]; /** signed budgeted annual total */ annual: number; /** signed actual total from the previous year (already signed in the DB) */ previousYearTotal: number; } function zeroTotals(): BudgetTotals { return { months: Array(12).fill(0) as number[], annual: 0, previousYearTotal: 0 }; } /** * Sum every non-subtotal (leaf) row of a single type into one `BudgetTotals`. * * The type's sign is applied to the budgeted figures (`months`/`annual`), * matching how `BudgetTable` displays them. `previousYearTotal` is NOT * re-signed: it comes from `getActualTotalsForYear` (a plain signed * `SUM(amount)` over the transactions table), already negative for expenses * and ~0-net for balanced transfers — the same convention `compareResults.ts` * relies on for its own previous-period figures. */ export function sumLeavesForType(rows: BudgetYearRow[], type: BudgetSectionType): BudgetTotals { const sign = signForBudgetType(type); const totals = zeroTotals(); for (const row of rows) { if (row.is_parent || row.category_type !== type) continue; for (let m = 0; m < 12; m++) totals.months[m] += row.months[m] * sign; totals.annual += row.annual * sign; totals.previousYearTotal += row.previousYearTotal; } return totals; } /** Component-wise sum of two BudgetTotals (both already signed). */ function combine(a: BudgetTotals, b: BudgetTotals): BudgetTotals { return { months: a.months.map((v, i) => v + b.months[i]), annual: a.annual + b.annual, previousYearTotal: a.previousYearTotal + b.previousYearTotal, }; } export interface BudgetResults { income: BudgetTotals; expense: BudgetTotals; transfer: BudgetTotals; /** Budgeted/actual revenues − expenses: the operating result, before transfers. */ resultBefore: BudgetTotals; /** resultBefore + transfers: the bottom-line total. */ resultNet: BudgetTotals; hasTransfers: boolean; } /** * Income-statement roll-up for the budget grid (Issue #278), mirroring * `compareResults.ts`'s shape (see that module's doc comment for the sign * convention this relies on). * * Operates on the flat `BudgetYearRow[]` the grid already renders (produced * by `useBudget`) — LEAVES only (`is_parent === false`), so parent subtotal * rows are never double-counted. All three column groups (previous-year * actual, budgeted annual, budgeted monthly) are folded into the same result * rows so "Résultat avant transferts" / "Résultat net" read consistently * across every column of the grid. */ export function computeBudgetResults(rows: BudgetYearRow[]): BudgetResults { const income = sumLeavesForType(rows, "income"); const expense = sumLeavesForType(rows, "expense"); const transfer = sumLeavesForType(rows, "transfer"); const resultBefore = combine(income, expense); const resultNet = combine(resultBefore, transfer); const hasTransfers = rows.some((r) => !r.is_parent && r.category_type === "transfer"); return { income, expense, transfer, resultBefore, resultNet, hasTransfers }; }