insurance.premium-proration Unreviewed
Return premium on mid-term cancellation, pro rata or on a short-period scale, with an optional minimum retained premium.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 26 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified actuary has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates insurance figures from published rules. It is a software component for developers, not financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have an actuary review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
The return premium when a policy is cancelled before it expires: how much of the premium the insurer keeps and how much goes back.
## Two bases
For example
cancellationReturnPremium(£600.00, 2026-01-01, 2027-01-01, 2026-04-01, pro-rata, short period scale ×7, —)→ days in force 90, total days 365, retained £147.95, return premium £452.05, minimum applied false pro rata after 90 of 365 days: 600.00 keeps 147.95 and returns 452.05cancellationReturnPremium(£600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×7, —)→ days in force 90, total days 365, retained £240.00, return premium £360.00, minimum applied false short period on the last day of the 3-month band keeps 40%cancellationReturnPremium(£600.00, 2026-01-01, 2027-01-01, 2026-04-02, short-period, short period scale ×7, —)→ days in force 91, total days 365, retained £300.00, return premium £300.00, minimum applied false one day into the 4th month moves to the 50% band
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function cancellationReturnPremium(premium: Money, inceptionDate: string, expiryDate: string, cancellationDate: string, basis: CancellationBasis, shortPeriodScale: readonly ShortPeriodBand[], minimumRetained: Money | null): CancellationRefund
| premium | Money | the premium charged for the whole policy period, 0 or more |
| inceptionDate | date | the first day of cover |
| expiryDate | date | the day cover would have ended, exclusive: a year from 2026-01-01 ends 2027-01-01 |
| cancellationDate | date | the day cover stops, from inceptionDate to expiryDate |
| basis | CancellationBasis | pro-rata, or short-period on the caller's scale |
| shortPeriodScale | ShortPeriodBand[] | the insurer's scale, shortest period first; ignored for pro-rata |
| minimumRetained | Money? | the least the insurer keeps whatever the basis, or null for none |
| returns | CancellationRefund |
The types it declares, generated into your project
export type CancellationBasis = "pro-rata" | "short-period";
export type PeriodUnit = "days" | "months";
/** One line of a short-period scale: cover in force for no more than this long keeps this share of the premium. */
export interface ShortPeriodBand {
readonly upTo: number;
readonly unit: PeriodUnit;
/** share of the premium the insurer keeps, 10000 = all of it */
readonly retainedBasisPoints: number;
}
/** What the insurer keeps and what goes back. */
export interface CancellationRefund {
readonly daysInForce: number;
readonly totalDays: number;
readonly retained: Money;
readonly returnPremium: Money;
/** true when the minimum retained premium raised what is kept */
readonly minimumApplied: boolean;
}
Your code names it in one line, in the file that uses it
import { cancellationReturnPremium } from "#fune/insurance.premium-proration@^1";
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,
};
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add insurance.premium-proration
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./insurance.premium-proration-1.0.1-typescript.fune, or fetch it from a terminal with fune pull insurance.premium-proration@1.0.1:typescript.
The whole function, every language, is one file too: insurance.premium-proration-1.0.1.fune, 40,755 bytes, sha256 18aebce0eb051a6d189b00076f06d1f6267ea13396e37cf4bcfffaab9f195b97. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before insurance.premium-proration
after — your function gets the result and the arguments, and returns the final result.
// fune: after insurance.premium-proration
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace dates.add-months in insurance.premium-proration
// fune: replace dates.days-between in insurance.premium-proration
// fune: replace finance.proration in insurance.premium-proration
// fune: replace money.amount in insurance.premium-proration
// fune: replace money.apply-rate in insurance.premium-proration
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show insurance.premium-proration --steps.
// fune: step insurance.premium-proration after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| pro rata after 90 of 365 days: 600.00 keeps 147.95 and returns 452.05 | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, pro-rata, short period scale ×7, — | → | days in force 90, total days 365, retained £147.95, return premium £452.05, minimum applied false |
| short period on the last day of the 3-month band keeps 40% | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×7, — | → | days in force 90, total days 365, retained £240.00, return premium £360.00, minimum applied false |
| one day into the 4th month moves to the 50% band | £600.00, 2026-01-01, 2027-01-01, 2026-04-02, short-period, short period scale ×7, — | → | days in force 91, total days 365, retained £300.00, return premium £300.00, minimum applied false |
| four days in: the one-week band keeps 10% | £600.00, 2026-01-01, 2027-01-01, 2026-01-05, short-period, short period scale ×7, — | → | days in force 4, total days 365, retained £60.00, return premium £540.00, minimum applied false |
| exactly 8 months keeps 80% | £600.00, 2026-01-01, 2027-01-01, 2026-09-01, short-period, short period scale ×7, — | → | days in force 243, total days 365, retained £480.00, return premium £120.00, minimum applied false |
| longer than every band keeps the whole premium | £600.00, 2026-01-01, 2027-01-01, 2026-09-02, short-period, short period scale ×7, — | → | days in force 244, total days 365, retained £600.00, return premium £0.00, minimum applied false |
| pro rata cancelled on the inception date returns everything | £600.00, 2026-01-01, 2027-01-01, 2026-01-01, pro-rata, , — | → | days in force 0, total days 365, retained £0.00, return premium £600.00, minimum applied false |
| pro rata cancelled on the expiry date returns nothing | £600.00, 2026-01-01, 2027-01-01, 2027-01-01, pro-rata, , — | → | days in force 365, total days 365, retained £600.00, return premium £0.00, minimum applied false |
| a minimum retained premium of 75.00 beats 23.01 pro rata | £600.00, 2026-01-01, 2027-01-01, 2026-01-15, pro-rata, , £75.00 | → | days in force 14, total days 365, retained £75.00, return premium £525.00, minimum applied true |
| the minimum is capped at the premium: nothing more than was paid is kept | £50.00, 2026-01-01, 2027-01-01, 2026-01-11, pro-rata, , £75.00 | → | days in force 10, total days 365, retained £50.00, return premium £0.00, minimum applied true |
Show the other 16 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a minimum below the scale's figure changes nothing | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×7, £75.00 | → | days in force 90, total days 365, retained £240.00, return premium £360.00, minimum applied false |
| 20% of 333.33 is 66.666, kept as 66.67 | £333.33, 2026-01-01, 2027-01-01, 2026-01-20, short-period, short period scale ×7, — | → | days in force 19, total days 365, retained £66.67, return premium £266.66, minimum applied false |
| a month is a calendar month: 30 days from 31 January passes 28 February, so the 2-month band | £600.00, 2026-01-31, 2027-01-31, 2026-03-02, short-period, short period scale ×7, — | → | days in force 30, total days 365, retained £180.00, return premium £420.00, minimum applied false |
| 28 February is exactly one month from 31 January | £600.00, 2026-01-31, 2027-01-31, 2026-02-28, short-period, short period scale ×7, — | → | days in force 28, total days 365, retained £120.00, return premium £480.00, minimum applied false |
| a leap-year policy has 366 days: 274 of them keep 274.00 of 366.00 | £366.00, 2027-06-01, 2028-06-01, 2028-03-01, pro-rata, , — | → | days in force 274, total days 366, retained £274.00, return premium £92.00, minimum applied false |
| a zero premium returns zero | £0.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×7, — | → | days in force 90, total days 365, retained £0.00, return premium £0.00, minimum applied false |
| cancelling after expiry is refused | £600.00, 2026-01-01, 2027-01-01, 2027-01-02, pro-rata, , — | → | error: cancellationDate must be from inceptionDate to expiryDate |
| cancelling before inception is refused | £600.00, 2026-01-01, 2027-01-01, 2025-12-31, pro-rata, , — | → | error: cancellationDate must be from inceptionDate to expiryDate |
| expiry must be after inception | £600.00, 2026-01-01, 2026-01-01, 2026-01-01, pro-rata, , — | → | error: expiryDate must be after inceptionDate |
| short period without a scale is refused | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, , — | → | error: a short-period cancellation needs a scale |
| a share above 100% is refused | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×1, — | → | error: retainedBasisPoints must be from 0 to 10000 |
| a scale that keeps less for longer is refused | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×2, — | → | error: must not keep less for a longer period |
| a fractional upTo is refused | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, short-period, short period scale ×1, — | → | error: upTo must be a whole number |
| a negative premium is refused | -£1.00, 2026-01-01, 2027-01-01, 2026-04-01, pro-rata, , — | → | error: premium must not be negative |
| a minimum in another currency is refused | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, pro-rata, , €75.00 | → | error: currency mismatch |
| an unknown basis is refused | £600.00, 2026-01-01, 2027-01-01, 2026-04-01, flat, , — | → | error: unknown basis |
More from the author
- **pro-rata**: the insurer keeps the premium for the days cover ran. The split is done by `finance.proration`, so kept + returned is always exactly the premium, with no penny invented by rounding each side on its own. - **short-period**: the insurer keeps a share of the premium from its own short-period (short-rate) scale, for example:
| cover in force, not exceeding | kept | |---|---| | 1 week | 10% | | 1 month | 20% | | 2 months | 30% | | 3 months | 40% | | 4 months | 50% | | 6 months | 70% | | 8 months | 80% | | longer | 100% |
Scales differ between insurers and products, so the caller supplies it (the one above is only an illustration, used in the vectors). The bands are read in order and the first one the period does not exceed applies; a period longer than every band keeps the whole premium. The kept share rounds half-up to the minor unit.
## Dates
`expiryDate` is exclusive (a year's cover from 2026-01-01 has expiry 2027-01-01), and days in force are `cancellationDate - inceptionDate`, so cancelling on the inception date means no days on cover. A band in months is measured in calendar months from inception with `dates.add-months`: one month from 31 January is 28 February, so 2 March is in the second month even though it is only 30 days on. A band in days is compared with days in force.
## Minimum retained premium
`minimumRetained` is the least the insurer keeps on any cancellation (often a flat amount, sometimes the premium's administration element). It never raises what is kept above the premium itself. `minimumApplied` says whether it changed the answer.
## Not covered here
- The statutory 14-day cancellation right for consumers (ICOBS 7): the insurer may only keep a proportionate charge for cover given, which is pro-rata; the caller chooses the basis. - IPT: the return premium carries its IPT back, at the rate the premium was taxed at (`insurance.ipt` with a negative amount). - Policy fees and instalment credit charges, which are refundable or not by the terms of business rather than by the premium arithmetic.
## Before you rely on this
**Not professional advice.** This capability calculates insurance figures from published rules. It is a software component for developers, not financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have an actuary review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified actuary has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
1.0.1 marks it unreviewed. The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 3,358 |
| impl/python.py | 3,989 |
| impl/rust.rs | 5,471 |
| impl/typescript.ts | 3,871 |
| vectors.json | 16,886 |