banking.overdraft-interest@1.0.0
impl/typescript.ts
2,552 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type DayCountConvention } from "./dates_day_count_fraction.ts"; ← from dates.day-count-fraction ^1.0.0 · built alongside by fune
import { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { type DatedBalance, dailyInterest } from "./lending_daily_interest.ts"; ← from lending.daily-interest ^1.0.0 · built alongside by fune
import { type OverdraftInterest } from "./banking_overdraft_interest_types.ts";
/**
* Overdraft interest over a statement period. Each day the account is
* overdrawn, the part of the overdrawn amount above the interest-free buffer
* accrues interest; credit balances earn nothing here. The accrual itself is
* lending.daily-interest on those chargeable amounts, so it is summed exactly
* and rounded once for the period.
*/
export function overdraftInterest(
balances: readonly DatedBalance[],
fromIso: string,
toIso: string,
annualRateBasisPoints: number,
interestFreeBuffer: Money,
convention: DayCountConvention,
mode: RoundingMode,
): OverdraftInterest {
if (!Number.isInteger(annualRateBasisPoints) || annualRateBasisPoints < 0) {
throw new RangeError(`annualRateBasisPoints must not be negative, received ${annualRateBasisPoints}`);
}
if (!Number.isInteger(interestFreeBuffer.minor) || interestFreeBuffer.minor < 0) {
throw new RangeError(`interestFreeBuffer must not be negative, received ${interestFreeBuffer.minor}`);
}
for (const entry of balances) {
if (entry.balance.currency !== interestFreeBuffer.currency) {
throw new RangeError(`currency mismatch: ${entry.balance.currency} and ${interestFreeBuffer.currency}`);
}
}
const chargeable: DatedBalance[] = balances.map((entry) => ({
date: entry.date,
balance: money(Math.max(0, -entry.balance.minor - interestFreeBuffer.minor), entry.balance.currency),
}));
// Validates the dates, their order and the period before anything is counted.
const accrued = dailyInterest(chargeable, fromIso, toIso, annualRateBasisPoints, convention, mode);
let daysOverdrawn = 0;
let daysCharged = 0;
for (let i = 0; i < balances.length; i += 1) {
const start = balances[i].date > fromIso ? balances[i].date : fromIso;
const next = i + 1 < balances.length ? balances[i + 1].date : toIso;
const end = next < toIso ? next : toIso;
if (start >= end) continue;
const days = daysBetween(start, end);
if (balances[i].balance.minor < 0) daysOverdrawn += days;
if (chargeable[i].balance.minor > 0) daysCharged += days;
}
return { interest: accrued.interest, daysOverdrawn, daysCharged };
}