Functional Weave
Code in TypeScript

insurance.rating-factors@1.0.1

impl/typescript.ts

4,338 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, assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type AppliedFactor, type FactorTable, type PremiumAdjustment, type RatedPremium } from "./insurance_rating_factors_types.ts";

function roundQuotient(numerator: bigint, denominator: bigint, mode: RoundingMode): bigint {
  // Both are non-negative here: a base of 0 or more times multipliers of 0 or more.
  const q = numerator / denominator;
  const twice = (numerator % denominator) * 2n;
  switch (mode) {
    case "down":
      return q;
    case "up":
      return twice > 0n ? q + 1n : q;
    case "half-up":
      return twice >= denominator ? q + 1n : q;
    case "half-even":
      return twice > denominator || (twice === denominator && q % 2n === 1n) ? q + 1n : q;
    default:
      throw new RangeError(`unknown rounding mode "${mode}"`);
  }
}

/**
 * Rate a premium: the base times each table's multiplier for the risk, then
 * each loading or discount, with one rounding at the end, then held within
 * the minimum and maximum premiums.
 *
 * Rounding after every factor drifts: 1.01 at 1.005 three times is 1.03
 * exactly but 1.04 rounded at each step. The product is exact integer
 * arithmetic of any size, so there is no limit on the number of factors.
 */
export function ratePremium(
  basePremium: Money,
  tables: readonly FactorTable[],
  risk: Readonly<Record<string, string>>,
  adjustments: readonly PremiumAdjustment[],
  minimumPremium: Money | null,
  maximumPremium: Money | null,
  mode: RoundingMode
): RatedPremium {
  if (basePremium.minor < 0) {
    throw new RangeError(`basePremium must not be negative, received ${basePremium.minor}`);
  }
  const hasMin = minimumPremium !== null && minimumPremium !== undefined;
  const hasMax = maximumPremium !== null && maximumPremium !== undefined;
  if (hasMin) assertSameCurrency(basePremium, minimumPremium);
  if (hasMax) assertSameCurrency(basePremium, maximumPremium);
  if (hasMin && hasMax && minimumPremium.minor > maximumPremium.minor) {
    throw new RangeError("minimumPremium must not be more than maximumPremium");
  }

  const factors: AppliedFactor[] = [];
  const names = new Set<string>();
  for (const table of tables) {
    if (names.has(table.name)) throw new RangeError(`rating factor "${table.name}" appears twice`);
    names.add(table.name);
    if (!Object.prototype.hasOwnProperty.call(risk, table.name)) {
      throw new RangeError(`no value for rating factor "${table.name}"`);
    }
    const key = risk[table.name];
    let found: number | null = null;
    for (const row of table.rows) {
      if (row.key === key) {
        if (found !== null) throw new RangeError(`rating factor "${table.name}" has two rows for "${key}"`);
        if (!Number.isInteger(row.basisPoints) || row.basisPoints < 0) {
          throw new RangeError(`factor basisPoints must be a whole number of 0 or more, received ${row.basisPoints}`);
        }
        found = row.basisPoints;
      }
    }
    if (found === null) throw new RangeError(`rating factor "${table.name}" has no row for "${key}"`);
    factors.push({ name: table.name, basisPoints: found });
  }
  for (const name of Object.keys(risk)) {
    if (!names.has(name)) throw new RangeError(`risk has a value for "${name}", which no table rates`);
  }
  for (const adjustment of adjustments) {
    if (!Number.isInteger(adjustment.basisPoints) || adjustment.basisPoints < -10000) {
      throw new RangeError(`adjustment basisPoints must be a whole number of -10000 or more, received ${adjustment.basisPoints}`);
    }
    factors.push({ name: adjustment.name, basisPoints: 10000 + adjustment.basisPoints });
  }

  let numerator = BigInt(basePremium.minor);
  let denominator = 1n;
  for (const f of factors) {
    numerator *= BigInt(f.basisPoints);
    denominator *= 10000n;
  }
  const c = basePremium.currency;
  const calculated = money(Number(roundQuotient(numerator, denominator, mode)), c);
  let premium = calculated;
  let cap: RatedPremium["cap"] = "none";
  if (hasMin && premium.minor < minimumPremium.minor) {
    premium = minimumPremium;
    cap = "minimum";
  } else if (hasMax && premium.minor > maximumPremium.minor) {
    premium = maximumPremium;
    cap = "maximum";
  }
  return { factors, calculated, premium, cap };
}