Functional Weave
Code in TypeScript

insurance.instalment-plan@1.0.0

impl/typescript.ts

2,930 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 { apr } from "./lending_apr.ts";  ← from lending.apr ^1.0.0 · built alongside by fune
import { type CreditFlow } from "./lending_apr_types.ts";
import { type Money, assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { applyRate } from "./money_apply_rate.ts";  ← from money.apply-rate ^1.0.0 · built alongside by fune
import { type InstalmentPlan } from "./insurance_instalment_plan_types.ts";

/**
 * A premium paid by monthly instalments.
 *
 * The credit charge is a flat percentage of the amount financed (premium less
 * deposit), the way UK insurers quote it. The instalments are the amount
 * financed plus the charge, split so every penny is accounted for: equal
 * instalments rounded down, with the odd pennies on the first. The APR is
 * then worked from those exact cash flows by lending.apr, because a flat
 * 10% repaid over ten months costs far more than 10% a year.
 */
export function instalmentPlan(
  premium: Money,
  deposit: Money,
  instalmentCount: number,
  creditChargeBasisPoints: number,
  firstInstalmentMonth: number
): InstalmentPlan {
  assertSameCurrency(premium, deposit);
  if (!Number.isInteger(instalmentCount) || instalmentCount < 1) {
    throw new RangeError(`instalmentCount must be a whole number of 1 or more, received ${instalmentCount}`);
  }
  if (!Number.isInteger(creditChargeBasisPoints) || creditChargeBasisPoints < 0) {
    throw new RangeError(`creditChargeBasisPoints must be a whole number of 0 or more, received ${creditChargeBasisPoints}`);
  }
  if (!Number.isInteger(firstInstalmentMonth) || firstInstalmentMonth < 0) {
    throw new RangeError(`firstInstalmentMonth must be a whole number of 0 or more, received ${firstInstalmentMonth}`);
  }
  if (deposit.minor < 0) {
    throw new RangeError(`deposit must not be negative, received ${deposit.minor}`);
  }
  if (deposit.minor >= premium.minor) {
    throw new RangeError("the deposit leaves nothing to pay by instalments");
  }
  if (instalmentCount === 1 && firstInstalmentMonth === 0) {
    throw new RangeError("a single instalment at inception is payment in full, not credit");
  }
  const c = premium.currency;
  const financed = money(premium.minor - deposit.minor, c);
  const charge = applyRate(financed, creditChargeBasisPoints, "half-up");
  const repayable = financed.minor + charge.minor;
  const each = Math.floor(repayable / instalmentCount);
  if (each < 1) {
    throw new RangeError("each instalment must be at least one minor unit");
  }
  const first = repayable - each * (instalmentCount - 1);
  const instalments: Money[] = [];
  const repayments: CreditFlow[] = [];
  for (let i = 0; i < instalmentCount; i++) {
    const amount = money(i === 0 ? first : each, c);
    instalments.push(amount);
    repayments.push({ period: firstInstalmentMonth + i, amount });
  }
  return {
    deposit,
    amountFinanced: financed,
    creditCharge: charge,
    instalments,
    totalPayable: money(deposit.minor + repayable, c),
    apr: apr([{ period: 0, amount: financed }], repayments, 12),
  };
}