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