Functional Weave
Code in Rust

finance.late-payment-interest@1.0.1

impl/typescript.ts

3,961 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 { addDays } from "./dates_add_days.ts";  ← from dates.add-days ^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 { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { addMoney } from "./money_add.ts";  ← from money.add ^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 { FIXED_SUMS, FIXED_SUMS_HISTORY } from "./finance_late_payment_interest_data.ts";  ← this capability’s own data, compiled from data/fixed-sums.json into the same file by fune build
import { ukBankRate } from "./finance_uk_bank_rate.ts";  ← from finance.uk-bank-rate ^1.0.0 · built alongside by fune
import { type LatePaymentClaim } from "./finance_late_payment_interest_types.ts";

const STATUTORY_MARGIN = 800; // 8% over the reference rate, SI 2002/1675 art. 4
const MAX_EXACT = BigInt(Number.MAX_SAFE_INTEGER);

function inForce(onDate: string, validFrom: string, validTo: string | null): boolean {
  return onDate >= validFrom && (validTo === null || onDate <= validTo);
}

function prunedHorizon(history: string, froms: readonly string[], onDate: string, what: string): void {
  // A history=current build keeps only rows still in force; answering an older
  // date with today's figure is the failure dated data exists to prevent.
  if (history === "full" || froms.length === 0) return;
  const earliest = froms.reduce((a, b) => (b < a ? b : a));
  if (onDate < earliest) {
    throw new RangeError(
      `no ${what} on ${onDate}: this build was installed with history=${history}, so it only carries rules from ${earliest}. Reinstall with history=full for older debts.`,
    );
  }
}

function fixedSumFor(debtMinor: number, onDate: string): number {
  let best: { minDebtMinor: number; compensationMinor: number } | null = null;
  for (const row of FIXED_SUMS) {
    if (!inForce(onDate, row.validFrom, row.validTo) || debtMinor < row.minDebtMinor) continue;
    if (best === null || row.minDebtMinor > best.minDebtMinor) best = row;
  }
  if (best === null) {
    prunedHorizon(FIXED_SUMS_HISTORY, FIXED_SUMS.map((r) => r.validFrom), onDate, "Late Payment Act fixed sum");
    throw new RangeError(`no Late Payment Act fixed sum in force on ${onDate}`);
  }
  return best.compensationMinor;
}

/** The 30 June or 31 December immediately before the day interest starts. */
function referenceDateFor(startDate: string): string {
  const year = Number(startDate.slice(0, 4));
  const month = Number(startDate.slice(5, 7));
  return month <= 6 ? `${String(year - 1).padStart(4, "0")}-12-31` : `${startDate.slice(0, 4)}-06-30`;
}

/**
 * Statutory interest and the fixed sum on a late commercial debt (UK).
 *
 * The reference rate is fixed when interest starts, not when it is paid, and
 * interest is simple at 1/365 of the yearly rate per day, rounded once.
 */
export function latePaymentInterest(debt: Money, dueDate: string, paymentDate: string): LatePaymentClaim {
  if (debt.currency !== "GBP") {
    throw new RangeError(`late payment interest is defined for GBP debts, received ${debt.currency}`);
  }
  if (debt.minor <= 0) {
    throw new RangeError("debt must be greater than zero");
  }
  const late = daysBetween(dueDate, paymentDate);
  const zero = money(0, "GBP");
  if (late <= 0) {
    return {
      daysLate: 0,
      referenceDate: null,
      referenceRateBasisPoints: null,
      interestRateBasisPoints: null,
      interest: zero,
      compensation: zero,
      total: zero,
    };
  }

  const startDate = addDays(dueDate, 1);
  const compensation = money(fixedSumFor(debt.minor, startDate), "GBP");
  const referenceDate = referenceDateFor(startDate);
  const referenceRate = ukBankRate(referenceDate).basisPoints;
  const rate = STATUTORY_MARGIN + referenceRate;

  if (BigInt(debt.minor) * BigInt(rate) * BigInt(late) > MAX_EXACT) {
    throw new RangeError("the interest calculation exceeds the exact integer range");
  }
  const interest = money(roundDiv(debt.minor * rate * late, 10000 * 365, "half-up"), "GBP");
  return {
    daysLate: late,
    referenceDate,
    referenceRateBasisPoints: referenceRate,
    interestRateBasisPoints: rate,
    interest,
    compensation,
    total: addMoney(interest, compensation),
  };
}