feat(kakeibo): dört harcama grubu, banka maliyeti ve geri alınabilir migration
- shared/kakeibo: grup devralma, banka maliyeti, kayıt bazında grup, grup özeti (saf, testli) - kategori ve işlem/kalem modellerine kakeiboGroup ve bankCost; form şemaları - migration 016 varsayılanları doldurur; npm run migrate -- --down <ad> ile geri alma - Kakeibo rapor servisi: tutarlar bütçeyle aynı kaynaktan, grup değiştirmeler taşınır Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
classify,
|
||||
classifyAmounts,
|
||||
resolveKakeibo,
|
||||
summarizeGroups,
|
||||
type KakeiboCategory,
|
||||
} from "./kakeibo";
|
||||
|
||||
const cat = (
|
||||
id: string,
|
||||
parentId: string | null,
|
||||
kakeiboGroup: KakeiboCategory["kakeiboGroup"] = null,
|
||||
bankCost = false,
|
||||
): KakeiboCategory => ({ id, parentId, kakeiboGroup, bankCost });
|
||||
|
||||
describe("resolveKakeibo", () => {
|
||||
const map = resolveKakeibo([
|
||||
cat("market", null, "ZORUNLU"),
|
||||
cat("gida", "market"),
|
||||
cat("bakim", "market", "ISTEK"),
|
||||
cat("banka", null, null, true),
|
||||
cat("faiz", "banka"),
|
||||
cat("diger", null),
|
||||
// Döngü: kendine üst (bozuk veri) sonsuz döngüye girmemeli.
|
||||
cat("dongu", "dongu"),
|
||||
]);
|
||||
it("alt kategori grubu üstten devralır; kendi değeri önceliklidir", () => {
|
||||
expect(map.get("gida")).toEqual({ group: "ZORUNLU", inherited: true, bankCost: false });
|
||||
expect(map.get("bakim")).toEqual({ group: "ISTEK", inherited: false, bankCost: false });
|
||||
expect(map.get("market")!.inherited).toBe(false);
|
||||
});
|
||||
it("banka maliyeti alt kategorilere geçer; grupsuz ve döngülü kategori güvenli", () => {
|
||||
expect(map.get("faiz")!.bankCost).toBe(true);
|
||||
expect(map.get("diger")).toEqual({ group: null, inherited: false, bankCost: false });
|
||||
expect(map.get("dongu")!.group).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("classify", () => {
|
||||
const map = resolveKakeibo([cat("market", null, "ZORUNLU"), cat("faiz", null, null, true)]);
|
||||
it("işlem bazında grup kategoriyi ezer; banka maliyeti her zaman banka maliyeti", () => {
|
||||
expect(classify(map.get("market"), null)).toBe("ZORUNLU");
|
||||
expect(classify(map.get("market"), "ISTEK")).toBe("ISTEK");
|
||||
expect(classify(map.get("faiz"), "ISTEK")).toBe("BANK_COST");
|
||||
expect(classify(undefined, null)).toBe("UNGROUPED");
|
||||
expect(classify(undefined, "KULTUR")).toBe("KULTUR");
|
||||
});
|
||||
});
|
||||
|
||||
describe("summarizeGroups", () => {
|
||||
it("banka maliyeti harcamaya ve paylara girmez; grupsuz harcamaya girer", () => {
|
||||
const s = summarizeGroups([
|
||||
{ cls: "ZORUNLU", amountMinor: 4_600 },
|
||||
{ cls: "ISTEK", amountMinor: 3_900 },
|
||||
{ cls: "KULTUR", amountMinor: 150 },
|
||||
{ cls: "BEKLENMEYEN", amountMinor: 1_250 },
|
||||
{ cls: "UNGROUPED", amountMinor: 100 },
|
||||
{ cls: "BANK_COST", amountMinor: 999 },
|
||||
]);
|
||||
expect(s.spendingMinor).toBe(10_000);
|
||||
expect(s.bankCostMinor).toBe(999);
|
||||
expect(s.shares).toEqual({ ZORUNLU: 46, ISTEK: 39, KULTUR: 1.5, BEKLENMEYEN: 12.5 });
|
||||
expect(s.ungroupedShare).toBe(1);
|
||||
});
|
||||
it("harcama yoksa paylar 0 (sıfıra bölme yok)", () => {
|
||||
const s = summarizeGroups([{ cls: "BANK_COST", amountMinor: 50 }]);
|
||||
expect(s.spendingMinor).toBe(0);
|
||||
expect(s.shares.ZORUNLU).toBe(0);
|
||||
expect(s.ungroupedShare).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("classifyAmounts", () => {
|
||||
const resolved = resolveKakeibo([
|
||||
cat("market", null, "ZORUNLU"),
|
||||
cat("faiz", null, null, true),
|
||||
cat("diger", null),
|
||||
]);
|
||||
it("değiştirilen tutar seçilen gruba taşınır, kategori toplamı korunur", () => {
|
||||
const out = classifyAmounts(
|
||||
new Map([
|
||||
["market", 10_000],
|
||||
["faiz", 500],
|
||||
["diger", 300],
|
||||
]),
|
||||
[
|
||||
{ categoryId: "market", group: "ISTEK", amountMinor: 2_500 },
|
||||
// Banka maliyetinde grup değiştirme etkisiz.
|
||||
{ categoryId: "faiz", group: "ISTEK", amountMinor: 500 },
|
||||
// Kategori toplamını aşan değiştirme kırpılır.
|
||||
{ categoryId: "diger", group: "KULTUR", amountMinor: 900 },
|
||||
],
|
||||
resolved,
|
||||
);
|
||||
expect(out).toEqual([
|
||||
{ categoryId: "market", cls: "ISTEK", amountMinor: 2_500 },
|
||||
{ categoryId: "market", cls: "ZORUNLU", amountMinor: 7_500 },
|
||||
{ categoryId: "faiz", cls: "BANK_COST", amountMinor: 500 },
|
||||
{ categoryId: "diger", cls: "KULTUR", amountMinor: 300 },
|
||||
]);
|
||||
const s = summarizeGroups(out);
|
||||
expect(s.spendingMinor + s.bankCostMinor).toBe(10_800);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user