Functional Weave
Code in Rust

lending.amortisation-schedule@1.0.1

impl/typescript.ts

2,672 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 { loanPayment, paymentCount, roundWide } from "./lending_loan_payment.ts";  ← from lending.loan-payment ^1.0.0 · built alongside by fune
import { type AmortisationRow, type AmortisationSchedule } from "./lending_amortisation_schedule_types.ts";

/**
 * Interest for one period on a positive balance: balance × b / D, rounded
 * half-up to the minor unit, which is what gets posted to the account.
 */
export function periodInterest(balance: number, annualRateBasisPoints: number, paymentsPerYear: number): number {
  if (balance <= 0 || annualRateBasisPoints === 0) return 0;
  const product = BigInt(balance) * BigInt(annualRateBasisPoints);
  // Rust holds the product in an i64; refuse it everywhere rather than in one language.
  if (product > 9223372036854775807n) throw new RangeError("balance too large for exact interest");
  return roundWide(product, 10000n * BigInt(paymentsPerYear), "half-up");
}

/**
 * The schedule a lender's system produces: each period's interest is
 * computed on the opening balance and rounded to a whole minor unit, the
 * level payment repays interest first and principal with the rest, and the
 * last payment is whatever clears the balance, so it ends at exactly zero.
 */
export function amortisationSchedule(
  principal: Money,
  annualRateBasisPoints: number,
  termMonths: number,
  paymentsPerYear: number,
  mode: RoundingMode,
): AmortisationSchedule {
  const payment = loanPayment(principal, annualRateBasisPoints, termMonths, paymentsPerYear, mode);
  const n = paymentCount(annualRateBasisPoints, termMonths, paymentsPerYear);
  const currency = principal.currency;
  const rows: AmortisationRow[] = [];
  let balance = principal.minor;
  let totalInterest = 0;
  let totalPaid = 0;
  for (let period = 1; period <= n && balance > 0; period += 1) {
    const interest = periodInterest(balance, annualRateBasisPoints, paymentsPerYear);
    // The last period, or one where the level payment would overshoot (a
    // rounded-up payment can clear the loan a period early), pays exactly
    // what is owed.
    const due = period === n || balance + interest <= payment.minor ? balance + interest : payment.minor;
    const repaid = due - interest;
    balance -= repaid;
    totalInterest += interest;
    totalPaid += due;
    rows.push({
      period,
      payment: money(due, currency),
      interest: money(interest, currency),
      principal: money(repaid, currency),
      balance: money(balance, currency),
    });
  }
  return {
    payment,
    rows,
    totalInterest: money(totalInterest, currency),
    totalPaid: money(totalPaid, currency),
  };
}