cc624af69f
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
4.6 KiB
TypeScript
114 lines
4.6 KiB
TypeScript
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<string, number>,
|
||
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 };
|
||
}
|