Functional Weave
Code in TypeScript

insurance.premium-proration@1.0.0

impl/typescript.ts

3,871 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 { addMonths } from "./dates_add_months.ts";  ← from dates.add-months ^1.0.0 · built alongside by fune
import { daysBetween } from "./dates_days_between.ts";  ← from dates.days-between ^1.0.0 · built alongside by fune
import { prorate } from "./finance_proration.ts";  ← from finance.proration ^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 { applyRate } from "./money_apply_rate.ts";  ← from money.apply-rate ^1.0.0 · built alongside by fune
import { type CancellationBasis, type CancellationRefund, type ShortPeriodBand } from "./insurance_premium_proration_types.ts";

/**
 * The return premium when a policy is cancelled mid-term.
 *
 * Pro rata keeps the premium for the days on cover. Short period keeps the
 * share the insurer's scale gives for how long cover ran ("not exceeding one
 * month: 20%"), and a period longer than every band keeps the whole premium.
 * A month on the scale is a calendar month from inception, not 30 days.
 */
export function cancellationReturnPremium(
  premium: Money,
  inceptionDate: string,
  expiryDate: string,
  cancellationDate: string,
  basis: CancellationBasis,
  shortPeriodScale: readonly ShortPeriodBand[],
  minimumRetained: Money | null
): CancellationRefund {
  if (premium.minor < 0) {
    throw new RangeError(`premium must not be negative, received ${premium.minor}`);
  }
  const totalDays = daysBetween(inceptionDate, expiryDate);
  if (totalDays <= 0) {
    throw new RangeError(`expiryDate must be after inceptionDate, received ${inceptionDate} to ${expiryDate}`);
  }
  const daysInForce = daysBetween(inceptionDate, cancellationDate);
  if (daysInForce < 0 || daysInForce > totalDays) {
    throw new RangeError(`cancellationDate must be from inceptionDate to expiryDate, received ${cancellationDate}`);
  }

  let retained: Money;
  if (basis === "pro-rata") {
    retained = prorate(premium, totalDays, daysInForce).used;
  } else if (basis === "short-period") {
    if (shortPeriodScale.length === 0) {
      throw new RangeError("a short-period cancellation needs a scale");
    }
    let share = 10000;
    let found = false;
    let previous = 0;
    for (const band of shortPeriodScale) {
      if (!Number.isInteger(band.upTo) || band.upTo < 1) {
        throw new RangeError(`upTo must be a whole number of at least 1, received ${band.upTo}`);
      }
      if (band.unit !== "days" && band.unit !== "months") {
        throw new RangeError(`unit must be days or months, received "${band.unit}"`);
      }
      if (!Number.isInteger(band.retainedBasisPoints) || band.retainedBasisPoints < 0 || band.retainedBasisPoints > 10000) {
        throw new RangeError(`retainedBasisPoints must be from 0 to 10000, received ${band.retainedBasisPoints}`);
      }
      if (band.retainedBasisPoints < previous) {
        throw new RangeError("a short-period scale must not keep less for a longer period");
      }
      previous = band.retainedBasisPoints;
      if (found) continue;
      const within =
        band.unit === "days" ? daysInForce <= band.upTo : cancellationDate <= addMonths(inceptionDate, band.upTo);
      if (within) {
        share = band.retainedBasisPoints;
        found = true;
      }
    }
    retained = applyRate(premium, share, "half-up");
  } else {
    throw new RangeError(`unknown basis "${basis}": use pro-rata or short-period`);
  }

  let minimumApplied = false;
  if (minimumRetained !== null && minimumRetained !== undefined) {
    assertSameCurrency(premium, minimumRetained);
    if (minimumRetained.minor < 0) {
      throw new RangeError(`minimumRetained must not be negative, received ${minimumRetained.minor}`);
    }
    // The insurer can never keep more than it was paid.
    const floor = Math.min(minimumRetained.minor, premium.minor);
    if (floor > retained.minor) {
      retained = money(floor, premium.currency);
      minimumApplied = true;
    }
  }
  return {
    daysInForce,
    totalDays,
    retained,
    returnPremium: money(premium.minor - retained.minor, premium.currency),
    minimumApplied,
  };
}