21b3715d35
$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>
158 lines
5.6 KiB
TypeScript
158 lines
5.6 KiB
TypeScript
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);
|
||
});
|
||
});
|