import { splitEvenly } from "./money"; import { addDays, addMonths, daysInMonth } from "./recurrence"; /** * Kredi kartı ve KMH dönemleri (Bölüm 6.8). Saf fonksiyonlar; tarihler "YYYY-MM-DD" (İstanbul). * * - Kesim günü ayda yoksa (ör. 31) ayın son günü kullanılır. * - Kesim günü DAHİL o gün ve öncesi "Hesap Kesim Dönemi"ne girer (Bölüm 10 varsayılanı). * - Kesim günü bitmeden dönem kapanmış sayılmaz: bugün kesim günüyse işlemler hâlâ "Güncel Dönem"de. */ export type DueRule = { kind: "daysAfter" | "fixedDay"; value: number }; export type Period = { start: string; end: string }; /** Asgari ödeme varsayılanı (Faz 7 kararı): limit < 50.000 TL → %20, üstü → %40. */ export const MINIMUM_RATE_THRESHOLD_MINOR = 50_000_00; export const MAX_INSTALLMENTS = 36; /** O ayın kesim tarihi. */ export function closingDateIn(ym: string, statementDay: number): string { const [y, m] = ym.split("-").map(Number) as [number, number]; const day = Math.min(statementDay, daysInMonth(y, m)); return `${ym}-${String(day).padStart(2, "0")}`; } /** Tarihi içeren dönemin kesim tarihi (tarih kesim gününe eşitse o gün). */ export function closingOnOrAfter(ymd: string, statementDay: number): string { const c = closingDateIn(ymd.slice(0, 7), statementDay); return ymd <= c ? c : closingDateIn(addMonths(`${ymd.slice(0, 7)}-01`, 1).slice(0, 7), statementDay); } /** Tarihi içeren dönem: önceki kesimin ertesi günü → bu kesim (dahil). */ export function periodContaining(ymd: string, statementDay: number): Period { const end = closingOnOrAfter(ymd, statementDay); const prevMonth = addMonths(`${end.slice(0, 7)}-01`, -1).slice(0, 7); return { start: addDays(closingDateIn(prevMonth, statementDay), 1), end }; } export function previousPeriod(p: Period, statementDay: number): Period { return periodContaining(addDays(p.start, -1), statementDay); } export function nextPeriod(p: Period, statementDay: number): Period { return periodContaining(addDays(p.end, 1), statementDay); } /** Son ödeme tarihi: kesimden N gün sonra veya kesimden sonraki ilk "ayın N. günü". */ export function dueDate(closing: string, rule: DueRule): string { if (rule.kind === "daysAfter") return addDays(closing, rule.value); const same = addMonths(closing, 0, rule.value); return same > closing ? same : addMonths(closing, 1, rule.value); } /** Bugüne göre güncel dönem (bugünü içerir) ve son kesilmiş dönem (ekstre). */ export function cardPeriods(today: string, statementDay: number) { const current = periodContaining(today, statementDay); return { current, statement: previousPeriod(current, statementDay) }; } export function minimumRateBp(limitMinor: number, overrideBp: number | null | undefined): number { if (overrideBp !== null && overrideBp !== undefined) return overrideBp; return limitMinor < MINIMUM_RATE_THRESHOLD_MINOR ? 2000 : 4000; } /** Asgari ödeme (kuruş, yukarı yuvarlanır). */ export function minimumPayment(debtMinor: number, rateBp: number): number { if (debtMinor <= 0) return 0; return Math.ceil((debtMinor * rateBp) / 10_000); } /** * Taksit planı: toplam eşit bölünür (kuruş farkı ilk taksitlere), her taksit alışveriş gününden * itibaren aylık (ayda o gün yoksa son gün) — böylece her taksit ardışık bir döneme düşer. */ export function installmentSchedule( purchaseYmd: string, totalMinor: number, count: number, ): { index: number; date: string; amountMinor: number }[] { const day = Number(purchaseYmd.slice(8, 10)); return splitEvenly(totalMinor, count).map((amountMinor, index) => ({ index, date: addMonths(purchaseYmd, index, day), amountMinor, })); } /** * KMH faizi tahmini: dönemdeki her günün gün sonu eksi bakiyesi × (aylık oran / 30). * `startBalance`: dönem başından önceki bakiye; `deltas`: gün → o günün net hareketi. * Vergiler (KKDF/BSMV) dahil değildir; kullanıcı kesim gününde kesinleştirir. */ export function estimateOverdraftInterest( period: Period, startBalanceMinor: number, deltas: ReadonlyMap, monthlyRateBp: number, until?: string, ): { interestMinor: number; averageUsedMinor: number; days: number } { let balance = startBalanceMinor; let used = 0; let days = 0; const last = until && until < period.end ? until : period.end; for (let d = period.start; d <= last; d = addDays(d, 1)) { balance += deltas.get(d) ?? 0; used += Math.max(0, -balance); days++; } const interestMinor = Math.round((used * monthlyRateBp) / 10_000 / 30); return { interestMinor, averageUsedMinor: days ? Math.round(used / days) : 0, days }; }