Functional Weave
Code in Rust

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