Files
butcem/src/shared/budget.test.ts
T
suphicakir 21b3715d35 fix(budget): sabit gider ayrımı ve taban hızlı ay sonu tahmini
$ne ile eksik alan yolu her işlemi sabit sayıyordu; $ifNull ile düzeltildi.
Tahmin, bu ayın hızını önceki dönemin değişken hızıyla karıştırır.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 01:22:38 +03:00

158 lines
5.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from "vitest";
import {
budgetPeriod,
computeBudget,
daysBetween,
forecast,
periodOfDate,
roundUpLimit,
shiftMonth,
statusOf,
type CategoryAmounts,
} from "./budget";
describe("bütçe dönemi", () => {
it("takvim ayı ve 15'inde başlayan ay", () => {
expect(budgetPeriod("2026-11")).toEqual({ from: "2026-11-01", to: "2026-11-30" });
expect(budgetPeriod("2026-12", 15)).toEqual({ from: "2026-12-15", to: "2027-01-14" });
expect(budgetPeriod("2027-01", 28)).toEqual({ from: "2027-01-28", to: "2027-02-27" });
expect(periodOfDate("2026-11-14", 15)).toBe("2026-10");
expect(periodOfDate("2026-11-15", 15)).toBe("2026-11");
expect(periodOfDate("2027-01-03", 15)).toBe("2026-12");
expect(periodOfDate("2026-11-30")).toBe("2026-11");
expect(shiftMonth("2026-12", 1)).toBe("2027-01");
expect(shiftMonth("2026-01", -1)).toBe("2025-12");
expect(daysBetween("2026-11-01", "2026-11-30")).toBe(30);
expect(daysBetween("2026-12-15", "2027-01-14")).toBe(31);
});
it("durum eşikleri %80 ve %100", () => {
expect(statusOf(79_99, 100_00)).toBe("ok");
expect(statusOf(80_00, 100_00)).toBe("warning");
expect(statusOf(100_00, 100_00)).toBe("over");
expect(statusOf(5, null)).toBe("none");
expect(statusOf(1, 0)).toBe("over");
});
it("ay sonu tahmini: sabit + planlı + değişken tempo", () => {
const a: CategoryAmounts = {
fixedMinor: 1_000_00,
variableMinor: 3_000_00,
plannedMinor: 500_00,
};
// Geçmiş yok: 10/30 gün geçti, değişken 3.000 → 9.000
expect(forecast(a, 10, 30)).toBe(10_500_00);
expect(forecast(a, 30, 30)).toBe(4_500_00);
// Henüz başlamamış dönem: gerçekleşmiş değişken olduğu gibi, tempo yok.
expect(forecast(a, 0, 30)).toBe(4_500_00);
expect(roundUpLimit(1_234_56)).toBe(1_300_00);
expect(roundUpLimit(1_200_00)).toBe(1_200_00);
});
it("taban hız: önceki ayın temposu ay başında ağır basar", () => {
// Ayın 2'sinde 6.000'lik tek harcama; önceki ay 30 günde 9.000 (günde 300).
const a: CategoryAmounts = {
fixedMinor: 0,
variableMinor: 6_000_00,
plannedMinor: 0,
baselineMinor: 9_000_00,
};
// Yalnızca bu ay: 6.000 × 30/2 = 90.000.
expect(forecast(a, 2, 30)).toBe(90_000_00);
// Karışım: hız = 2/30 × 3.000 + 28/30 × 300 = 480 → 6.000 + 28 × 480
expect(forecast(a, 2, 30, 30)).toBe(6_000_00 + 28 * 480_00);
// Gelecek ay: yalnızca taban; geçmiş ay: gerçekleşen.
expect(forecast({ ...a, variableMinor: 0 }, 0, 30, 30)).toBe(9_000_00);
expect(forecast(a, 30, 30, 30)).toBe(6_000_00);
});
});
describe("bütçe toplama", () => {
const categories = [
{ id: "market", parentId: null, name: "Market", order: 1 },
{ id: "faturalar", parentId: null, name: "Faturalar", order: 2 },
{ id: "elektrik", parentId: "faturalar", name: "Elektrik", order: 1 },
{ id: "su", parentId: "faturalar", name: "Su", order: 2 },
{ id: "eglence", parentId: null, name: "Eğlence", order: 3 },
{ id: "sinema", parentId: "eglence", name: "Sinema", order: 1 },
{ id: "giyim", parentId: null, name: "Giyim", order: 4 },
];
const amt = (variable: number, fixed = 0, planned = 0): CategoryAmounts => ({
fixedMinor: fixed,
variableMinor: variable,
plannedMinor: planned,
});
it("üst limit alt kategorileri kapsar; alt limit iki kez sayılmaz; bütçesiz harcama", () => {
const r = computeBudget({
categories,
limits: new Map([
["market", 10_000_00],
["faturalar", 3_000_00],
["elektrik", 1_500_00],
["sinema", 500_00],
]),
amounts: new Map([
["market", amt(8_500_00)],
["elektrik", amt(0, 1_600_00)],
["su", amt(0, 400_00, 300_00)],
["faturalar", amt(100_00)],
["sinema", amt(200_00)],
["eglence", amt(300_00)],
["giyim", amt(1_000_00)],
]),
elapsedDays: 30,
totalDays: 30,
});
// Toplam limit: market + faturalar + sinema (elektrik faturaların içinde).
expect(r.totals.limitMinor).toBe(13_500_00);
expect(r.totals.actualMinor).toBe(12_100_00);
// Bütçesiz: eğlencenin kendi harcaması + giyim.
expect(r.totals.unbudgetedMinor).toBe(1_300_00);
const line = (id: string) => r.lines.find((l) => l.categoryId === id)!;
expect(line("faturalar")).toMatchObject({
actualMinor: 2_100_00,
plannedMinor: 300_00,
status: "ok",
ratioBp: 7000,
});
expect(line("elektrik")).toMatchObject({ status: "over", remainingMinor: -100_00 });
expect(line("market").status).toBe("warning");
expect(line("eglence")).toMatchObject({
limitMinor: null,
actualMinor: 500_00,
status: "none",
});
// Sıra: ağaç sırası, derinlik.
expect(r.lines.map((l) => `${l.depth}:${l.categoryId}`)).toEqual([
"0:market",
"0:faturalar",
"1:elektrik",
"1:su",
"0:eglence",
"1:sinema",
"0:giyim",
]);
});
it("tahmin limiti aşıyor uyarısı; limit yoksa durum none", () => {
const r = computeBudget({
categories,
limits: new Map([["market", 10_000_00]]),
amounts: new Map([["market", amt(4_000_00)]]),
elapsedDays: 10,
totalDays: 30,
});
expect(r.totals).toMatchObject({ status: "ok", forecastMinor: 12_000_00, forecastOver: true });
const empty = computeBudget({
categories,
limits: new Map(),
amounts: new Map(),
elapsedDays: 1,
totalDays: 30,
});
expect(empty.totals.status).toBe("none");
expect(empty.lines).toHaveLength(0);
});
});