insurance.no-claims-discount@1.0.1
impl/typescript.ts
3,038 bytes · the TypeScript implementation · view raw
import { type NcdLevel, type NcdOutcome, type NcdProtection } from "./insurance_no_claims_discount_types.ts";
function wholeAtLeastZero(value: number, name: string): void {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${name} must be a whole number of 0 or more, received ${value}`);
}
}
function levelFor(scale: readonly NcdLevel[], years: number): NcdLevel {
let found = scale[0];
for (const level of scale) if (level.years <= years) found = level;
return found;
}
/**
* The no-claims discount held at renewal.
*
* A claim-free year earns a year. A year with claims steps back to what the
* scale says for the level held and the number of claims, unless protected
* NCD covers them, in which case the years stay where they were (a year with
* a claim earns nothing). Years keep counting past the top of the scale; the
* discount is the highest level not above them.
*/
export function nextNoClaimsDiscount(
scale: readonly NcdLevel[],
currentYears: number,
claims: number,
protection: NcdProtection | null
): NcdOutcome {
if (scale.length === 0) throw new RangeError("the NCD scale must not be empty");
if (scale[0].years !== 0) throw new RangeError("the NCD scale must start at 0 years");
for (let i = 0; i < scale.length; i++) {
const level = scale[i];
wholeAtLeastZero(level.years, "years");
if (i > 0 && level.years <= scale[i - 1].years) {
throw new RangeError("the NCD scale must be in ascending order of years");
}
if (!Number.isInteger(level.discountBasisPoints) || level.discountBasisPoints < 0 || level.discountBasisPoints > 10000) {
throw new RangeError(`discountBasisPoints must be from 0 to 10000, received ${level.discountBasisPoints}`);
}
let previous = level.years;
for (const back of level.stepBack) {
wholeAtLeastZero(back, "stepBack");
if (back > previous) {
throw new RangeError(`stepBack from ${level.years} years must not rise above the level or the step before it`);
}
previous = back;
}
}
wholeAtLeastZero(currentYears, "currentYears");
wholeAtLeastZero(claims, "claims");
let years: number;
let protectionApplied = false;
if (claims === 0) {
years = currentYears + 1;
} else {
let covered = false;
if (protection !== null && protection !== undefined) {
wholeAtLeastZero(protection.claimLimit, "claimLimit");
wholeAtLeastZero(protection.claimsInWindow, "claimsInWindow");
if (protection.claimsInWindow < claims) {
throw new RangeError("claimsInWindow must include this year's claims");
}
covered = protection.claimsInWindow <= protection.claimLimit;
}
if (covered) {
years = currentYears;
protectionApplied = true;
} else {
const steps = levelFor(scale, currentYears).stepBack;
years = steps.length === 0 ? 0 : steps[Math.min(claims, steps.length) - 1];
}
}
return { years, discountBasisPoints: levelFor(scale, years).discountBasisPoints, protectionApplied };
}