Compare commits
No commits in common. "99fdf4f9ea9284795cfdb569f9a5d216d0047b6c" and "003f45620330440f433bc91e05df88e959fc32e3" have entirely different histories.
99fdf4f9ea
...
003f456203
2 changed files with 66 additions and 5 deletions
50
src/hooks/useBudget.test.ts
Normal file
50
src/hooks/useBudget.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
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,
|
||||||
|
* 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("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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should skip null category_id entries", () => {
|
||||||
|
const actuals = [{ category_id: null, actual: -100 }];
|
||||||
|
const map = buildPrevYearTotalMap(actuals);
|
||||||
|
expect(map.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle zero actuals", () => {
|
||||||
|
const actuals = [{ category_id: 3, actual: 0 }];
|
||||||
|
const map = buildPrevYearTotalMap(actuals);
|
||||||
|
expect(map.get(3)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -63,6 +63,21 @@ function reducer(state: BudgetState, action: BudgetAction): BudgetState {
|
||||||
|
|
||||||
const TYPE_ORDER: Record<string, number> = { expense: 0, income: 1, transfer: 2 };
|
const TYPE_ORDER: Record<string, number> = { 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<number, number> {
|
||||||
|
const prevYearTotalMap = new Map<number, number>();
|
||||||
|
for (const a of actuals) {
|
||||||
|
if (a.category_id != null) prevYearTotalMap.set(a.category_id, a.actual);
|
||||||
|
}
|
||||||
|
return prevYearTotalMap;
|
||||||
|
}
|
||||||
|
|
||||||
export function useBudget() {
|
export function useBudget() {
|
||||||
const [state, dispatch] = useReducer(reducer, undefined, initialState);
|
const [state, dispatch] = useReducer(reducer, undefined, initialState);
|
||||||
const fetchIdRef = useRef(0);
|
const fetchIdRef = useRef(0);
|
||||||
|
|
@ -90,11 +105,7 @@ export function useBudget() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a map for previous year actuals: categoryId -> annual actual total
|
// Build a map for previous year actuals: categoryId -> annual actual total
|
||||||
// Amounts are already signed (expenses negative, income positive) — stored as-is.
|
const prevYearTotalMap = buildPrevYearTotalMap(prevYearActuals);
|
||||||
const prevYearTotalMap = new Map<number, number>();
|
|
||||||
for (const a of prevYearActuals) {
|
|
||||||
if (a.category_id != null) prevYearTotalMap.set(a.category_id, a.actual);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper: build months array from entryMap
|
// Helper: build months array from entryMap
|
||||||
const buildMonths = (catId: number) => {
|
const buildMonths = (catId: number) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue