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