Files
butcem/src/shared/card-periods.ts
T

114 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}