Functional Weave
Code in Rust

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 };
}