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
cancellation_return_premium(£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.05cancellation_return_premium(£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%cancellation_return_premium(£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.
pub fn cancellation_return_premium(premium: &Money, inception_date: &str, expiry_date: &str, cancellation_date: &str, basis: &str, short_period_scale: &[ShortPeriodBand], minimum_retained: Option<&Money>) -> CancellationRefund
| premium | Money | the premium charged for the whole policy period, 0 or more |
| inception_date | date | the first day of cover |
| expiry_date | date | the day cover would have ended, exclusive: a year from 2026-01-01 ends 2027-01-01 |
| cancellation_date | date | the day cover stops, from inceptionDate to expiryDate |
| basis | CancellationBasis | pro-rata, or short-period on the caller's scale |
| short_period_scale | ShortPeriodBand[] | the insurer's scale, shortest period first; ignored for pro-rata |
| minimum_retained | Money? | the least the insurer keeps whatever the basis, or null for none |
| returns | CancellationRefund |
The types it declares, generated into your project
// CancellationBasis is a string in Rust, one of: "pro-rata", "short-period".
// Parameters take it as &str and results hold it as String.
// PeriodUnit is a string in Rust, one of: "days", "months".
// Parameters take it as &str and results hold it as String.
/// One line of a short-period scale: cover in force for no more than this long keeps this share of the premium.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShortPeriodBand {
pub up_to: i64,
pub unit: String,
/// share of the premium the insurer keeps, 10000 = all of it
pub retained_basis_points: i64,
}
/// What the insurer keeps and what goes back.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CancellationRefund {
pub days_in_force: i64,
pub total_days: i64,
pub retained: Money,
pub return_premium: Money,
/// true when the minimum retained premium raised what is kept
pub minimum_applied: bool,
}
Your code names it in one line, in the file that uses it
fune!(insurance.premium-proration@^1); // then call cancellation_return_premium(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_months::add_months; ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_days_between::days_between; ← from dates.days-between ^1.0.0 · built alongside by fune
use super::finance_proration::prorate; ← from finance.proration ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate; ← from money.apply-rate ^1.0.0 · built alongside by fune
/// 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.
///
/// # Panics
/// Panics on a negative premium, dates out of order, a bad scale or a
/// minimum in another currency.
pub fn cancellation_return_premium(
premium: &Money,
inception_date: &str,
expiry_date: &str,
cancellation_date: &str,
basis: &str,
short_period_scale: &[ShortPeriodBand],
minimum_retained: Option<&Money>,
) -> CancellationRefund {
if premium.minor < 0 {
panic!("premium must not be negative, received {}", premium.minor);
}
let total_days = days_between(inception_date, expiry_date);
if total_days <= 0 {
panic!("expiryDate must be after inceptionDate, received {} to {}", inception_date, expiry_date);
}
let days_in_force = days_between(inception_date, cancellation_date);
if days_in_force < 0 || days_in_force > total_days {
panic!("cancellationDate must be from inceptionDate to expiryDate, received {}", cancellation_date);
}
let mut retained = match basis {
"pro-rata" => prorate(premium, total_days, days_in_force).used,
"short-period" => {
if short_period_scale.is_empty() {
panic!("a short-period cancellation needs a scale");
}
let mut share = 10000;
let mut found = false;
let mut previous = 0;
for band in short_period_scale {
if band.up_to < 1 {
panic!("upTo must be a whole number of at least 1, received {}", band.up_to);
}
if band.unit != "days" && band.unit != "months" {
panic!("unit must be days or months, received \"{}\"", band.unit);
}
let bp = band.retained_basis_points;
if !(0..=10000).contains(&bp) {
panic!("retainedBasisPoints must be from 0 to 10000, received {}", bp);
}
if bp < previous {
panic!("a short-period scale must not keep less for a longer period");
}
previous = bp;
if found {
continue;
}
let within = if band.unit == "days" {
days_in_force <= band.up_to
} else {
cancellation_date <= add_months(inception_date, band.up_to).as_str()
};
if within {
share = bp;
found = true;
}
}
apply_rate(premium, share, "half-up")
}
other => panic!("unknown basis \"{}\": use pro-rata or short-period", other),
};
let mut minimum_applied = false;
if let Some(minimum) = minimum_retained {
assert_same_currency(premium, minimum);
if minimum.minor < 0 {
panic!("minimumRetained must not be negative, received {}", minimum.minor);
}
// The insurer can never keep more than it was paid.
let floor = minimum.minor.min(premium.minor);
if floor > retained.minor {
retained = money(floor, &premium.currency);
minimum_applied = true;
}
}
let return_premium = money(premium.minor - retained.minor, &premium.currency);
CancellationRefund {
days_in_force,
total_days,
retained,
return_premium,
minimum_applied,
}
}
pub fn short_period_band_from_value(v: &Value) -> ShortPeriodBand {
if let Value::Float(f) = v.get("upTo") {
panic!("upTo must be a whole number of at least 1, received {}", f);
}
if let Value::Float(f) = v.get("retainedBasisPoints") {
panic!("retainedBasisPoints must be from 0 to 10000, received {}", f);
}
ShortPeriodBand {
up_to: v.get("upTo").as_i64(),
unit: v.get("unit").as_str().to_string(),
retained_basis_points: v.get("retainedBasisPoints").as_i64(),
}
}
pub fn cancellation_refund_to_value(r: &CancellationRefund) -> Value {
Value::obj(vec![
("daysInForce", Value::Int(r.days_in_force)),
("totalDays", Value::Int(r.total_days)),
("retained", money_to_value(&r.retained)),
("returnPremium", money_to_value(&r.return_premium)),
("minimumApplied", Value::Bool(r.minimum_applied)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let scale: Vec<ShortPeriodBand> = args[5].as_arr().iter().map(short_period_band_from_value).collect();
let minimum = if args[6].is_null() { None } else { Some(money_from_value(&args[6])) };
cancellation_refund_to_value(&cancellation_return_premium(
&money_from_value(&args[0]),
args[1].as_str(),
args[2].as_str(),
args[3].as_str(),
args[4].as_str(),
&scale,
minimum.as_ref(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. 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 Rust implementation. Install it without the registry with fune add ./insurance.premium-proration-1.0.1-rust.fune, or fetch it from a terminal with fune pull insurance.premium-proration@1.0.1:rust.
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 |