feat(cards): kart/KMH dönem hesabı, taksitli harcama, kart ve KMH özeti servisleri, 006 migration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-05 11:19:17 +03:00
parent 57067b0fd0
commit cc624af69f
22 changed files with 1113 additions and 19 deletions
+82
View File
@@ -0,0 +1,82 @@
import { describe, expect, it } from "vitest";
import {
cardPeriods,
closingOnOrAfter,
dueDate,
estimateOverdraftInterest,
installmentSchedule,
minimumPayment,
minimumRateBp,
nextPeriod,
periodContaining,
} from "./card-periods";
describe("dönem hesabı", () => {
it("kesim günü dahil hesap kesim dönemine girer; ertesi gün yeni dönem", () => {
expect(periodContaining("2026-10-29", 29)).toEqual({ start: "2026-09-30", end: "2026-10-29" });
expect(periodContaining("2026-10-30", 29)).toEqual({ start: "2026-10-30", end: "2026-11-29" });
});
it("kesim günü ayda yoksa ayın son günü (31 → Şubat 28/29)", () => {
expect(closingOnOrAfter("2026-02-10", 31)).toBe("2026-02-28");
expect(periodContaining("2028-03-05", 31)).toEqual({ start: "2028-03-01", end: "2028-03-31" });
expect(periodContaining("2026-03-01", 30)).toEqual({ start: "2026-03-01", end: "2026-03-30" });
expect(periodContaining("2026-02-28", 30)).toEqual({ start: "2026-01-31", end: "2026-02-28" });
});
it("yıl dönümü ve sonraki dönem", () => {
const p = periodContaining("2026-12-31", 15);
expect(p).toEqual({ start: "2026-12-16", end: "2027-01-15" });
expect(nextPeriod(p, 15)).toEqual({ start: "2027-01-16", end: "2027-02-15" });
});
it("güncel dönem ve son ekstre; kesim günü bugün iken dönem henüz kapanmamış", () => {
expect(cardPeriods("2026-11-05", 29)).toEqual({
current: { start: "2026-10-30", end: "2026-11-29" },
statement: { start: "2026-09-30", end: "2026-10-29" },
});
expect(cardPeriods("2026-11-29", 29).current.end).toBe("2026-11-29");
});
it("son ödeme tarihi", () => {
expect(dueDate("2026-10-29", { kind: "daysAfter", value: 10 })).toBe("2026-11-08");
expect(dueDate("2026-10-29", { kind: "fixedDay", value: 9 })).toBe("2026-11-09");
expect(dueDate("2026-10-05", { kind: "fixedDay", value: 20 })).toBe("2026-10-20");
expect(dueDate("2026-01-29", { kind: "fixedDay", value: 31 })).toBe("2026-01-31");
});
});
describe("asgari ödeme", () => {
it("limite göre %20 / %40; kart bazında oran öncelikli; yukarı yuvarlama", () => {
expect(minimumRateBp(49_999_99, null)).toBe(2000);
expect(minimumRateBp(50_000_00, null)).toBe(4000);
expect(minimumRateBp(60_000_00, 2500)).toBe(2500);
expect(minimumPayment(10_000_01, 2000)).toBe(2_000_01);
expect(minimumPayment(-5, 2000)).toBe(0);
});
});
describe("taksit planı", () => {
it("eşit bölme (kuruş farkı ilk taksitlerde), aylık tarihler, ay sonu", () => {
const s = installmentSchedule("2026-01-31", 10_000_00, 3);
expect(s.map((x) => x.amountMinor)).toEqual([3_333_34, 3_333_33, 3_333_33]);
expect(s.reduce((a, x) => a + x.amountMinor, 0)).toBe(10_000_00);
expect(s.map((x) => x.date)).toEqual(["2026-01-31", "2026-02-28", "2026-03-31"]);
});
});
describe("KMH faiz tahmini", () => {
it("gün sonu eksi bakiyeler üzerinden", () => {
const period = { start: "2026-11-01", end: "2026-11-30" };
// 10 gün boyunca -3.000 TL; sonra sıfır.
const deltas = new Map([
["2026-11-01", -3_000_00],
["2026-11-11", 3_000_00],
]);
const r = estimateOverdraftInterest(period, 0, deltas, 425);
// 10 gün × 3.000 TL × %4,25 / 30 = 42,50 TL
expect(r.interestMinor).toBe(42_50);
expect(r.days).toBe(30);
expect(estimateOverdraftInterest(period, 5_000_00, deltas, 425).interestMinor).toBe(0);
});
});