diff --git a/src/components/budget/BudgetTable.tsx b/src/components/budget/BudgetTable.tsx
index aa92e4f..b398c95 100644
--- a/src/components/budget/BudgetTable.tsx
+++ b/src/components/budget/BudgetTable.tsx
@@ -150,7 +150,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
monthTotals[m] += row.months[m] * sign;
}
annualTotal += row.annual * sign;
- prevYearTotal += row.previousYearTotal * sign;
+ prevYearTotal += row.previousYearTotal; // actuals are already signed in the DB
}
const totalCols = 15; // category + prev year + annual + 12 months
@@ -197,7 +197,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
- {formatSigned(row.previousYearTotal * sign)}
+ {formatSigned(row.previousYearTotal)}
|
{formatSigned(row.annual * sign)}
@@ -230,7 +230,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
{/* Previous year total — read-only */}
|
- {formatSigned(row.previousYearTotal * sign)}
+ {formatSigned(row.previousYearTotal)}
|
{/* Annual total — editable */}
@@ -340,7 +340,7 @@ export default function BudgetTable({ rows, onUpdatePlanned, onSplitEvenly }: Bu
sectionMonthTotals[m] += row.months[m] * sign;
}
sectionAnnualTotal += row.annual * sign;
- sectionPrevYearTotal += row.previousYearTotal * sign;
+ sectionPrevYearTotal += row.previousYearTotal; // actuals are already signed in the DB
}
return (
diff --git a/src/hooks/useBudget.test.ts b/src/hooks/useBudget.test.ts
index baa88af..220b684 100644
--- a/src/hooks/useBudget.test.ts
+++ b/src/hooks/useBudget.test.ts
@@ -1,35 +1,23 @@
import { describe, it, expect } from "vitest";
+import { buildPrevYearTotalMap } from "./useBudget";
/**
* Unit tests for the previous-year actuals normalization logic used in useBudget.
*
- * Transaction amounts in the database use signed values (expenses are negative).
- * Budget entries are always stored as positive values, with sign applied at display time.
- * When building the previousYearTotal map from raw actuals, we must normalize with
- * Math.abs() to match the budget convention. Without this, expenses would show
- * inverted signs (negative × -1 = positive instead of positive × -1 = negative).
+ * Transaction amounts in the database use signed values (expenses are negative,
+ * income is positive). The buildPrevYearTotalMap function preserves these signs
+ * as-is, because the budget display layer does NOT apply a sign multiplier to
+ * previous year actuals (unlike planned budget amounts).
*/
-describe("previous year actuals normalization", () => {
- // Simulates the normalization logic from useBudget.ts
- function buildPrevYearTotalMap(
- actuals: Array<{ category_id: number | null; actual: number }>
- ): Map {
- const prevYearTotalMap = new Map();
- for (const a of actuals) {
- if (a.category_id != null)
- prevYearTotalMap.set(a.category_id, Math.abs(a.actual));
- }
- return prevYearTotalMap;
- }
-
- it("should store absolute values for expense actuals (negative in DB)", () => {
+describe("buildPrevYearTotalMap", () => {
+ it("should preserve negative sign for expense actuals", () => {
const actuals = [{ category_id: 1, actual: -500 }]; // expense: negative in DB
const map = buildPrevYearTotalMap(actuals);
- expect(map.get(1)).toBe(500);
+ expect(map.get(1)).toBe(-500);
});
- it("should store absolute values for income actuals (positive in DB)", () => {
+ it("should preserve positive sign for income actuals", () => {
const actuals = [{ category_id: 2, actual: 3000 }]; // income: positive in DB
const map = buildPrevYearTotalMap(actuals);
expect(map.get(2)).toBe(3000);
@@ -47,16 +35,16 @@ describe("previous year actuals normalization", () => {
expect(map.get(3)).toBe(0);
});
- it("should display correctly with sign multiplier applied", () => {
- // Simulate the display logic: previousYearTotal * sign
- const signFor = (type: string) => (type === "expense" ? -1 : 1);
-
- // Expense category: actual was -500 in DB → normalized to 500 → displayed as 500 * -1 = -500
- const expenseActual = Math.abs(-500);
- expect(expenseActual * signFor("expense")).toBe(-500);
-
- // Income category: actual was 3000 in DB → normalized to 3000 → displayed as 3000 * 1 = 3000
- const incomeActual = Math.abs(3000);
- expect(incomeActual * signFor("income")).toBe(3000);
+ it("should handle multiple categories", () => {
+ const actuals = [
+ { category_id: 1, actual: -200 },
+ { category_id: 2, actual: 1500 },
+ { category_id: 3, actual: -75.5 },
+ ];
+ const map = buildPrevYearTotalMap(actuals);
+ expect(map.size).toBe(3);
+ expect(map.get(1)).toBe(-200);
+ expect(map.get(2)).toBe(1500);
+ expect(map.get(3)).toBe(-75.5);
});
});
diff --git a/src/hooks/useBudget.ts b/src/hooks/useBudget.ts
index aa04a99..2f41eab 100644
--- a/src/hooks/useBudget.ts
+++ b/src/hooks/useBudget.ts
@@ -63,6 +63,21 @@ function reducer(state: BudgetState, action: BudgetAction): BudgetState {
const TYPE_ORDER: Record = { expense: 0, income: 1, transfer: 2 };
+/**
+ * Build a map of category_id -> annual actual total from raw actuals.
+ * Transaction amounts are already signed (expenses negative, income positive),
+ * so they are stored as-is without normalization.
+ */
+export function buildPrevYearTotalMap(
+ actuals: Array<{ category_id: number | null; actual: number }>
+): Map {
+ const prevYearTotalMap = new Map();
+ for (const a of actuals) {
+ if (a.category_id != null) prevYearTotalMap.set(a.category_id, a.actual);
+ }
+ return prevYearTotalMap;
+}
+
export function useBudget() {
const [state, dispatch] = useReducer(reducer, undefined, initialState);
const fetchIdRef = useRef(0);
@@ -90,12 +105,7 @@ export function useBudget() {
}
// Build a map for previous year actuals: categoryId -> annual actual total
- // Use Math.abs because transaction amounts are stored with sign (expenses negative),
- // but budget entries are always positive — the sign is applied at display time.
- const prevYearTotalMap = new Map();
- for (const a of prevYearActuals) {
- if (a.category_id != null) prevYearTotalMap.set(a.category_id, Math.abs(a.actual));
- }
+ const prevYearTotalMap = buildPrevYearTotalMap(prevYearActuals);
// Helper: build months array from entryMap
const buildMonths = (catId: number) => {