Functional Weave
Code in Python

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.05
  • cancellation_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.

def cancellation_return_premium(premium: Money, inception_date: str, expiry_date: str, cancellation_date: str, basis: CancellationBasis, short_period_scale: Sequence[ShortPeriodBand], minimum_retained: Optional[Money]) -> CancellationRefund
premiumMoneythe premium charged for the whole policy period, 0 or more
inception_datedatethe first day of cover
expiry_datedatethe day cover would have ended, exclusive: a year from 2026-01-01 ends 2027-01-01
cancellation_datedatethe day cover stops, from inceptionDate to expiryDate
basisCancellationBasispro-rata, or short-period on the caller's scale
short_period_scaleShortPeriodBand[]the insurer's scale, shortest period first; ignored for pro-rata
minimum_retainedMoney?the least the insurer keeps whatever the basis, or null for none
returnsCancellationRefund

The types it declares, generated into your project

CancellationBasis = Literal["pro-rata", "short-period"]

PeriodUnit = Literal["days", "months"]

@dataclass(frozen=True)
class ShortPeriodBand:
    """One line of a short-period scale: cover in force for no more than this long keeps this share of the premium."""

    up_to: int
    unit: PeriodUnit
    #: share of the premium the insurer keeps, 10000 = all of it
    retained_basis_points: int

@dataclass(frozen=True)
class CancellationRefund:
    """What the insurer keeps and what goes back."""

    days_in_force: int
    total_days: int
    retained: Money
    return_premium: Money
    #: true when the minimum retained premium raised what is kept
    minimum_applied: bool

Your code names it in one line, in the file that uses it

from fune.insurance.premium_proration import cancellation_return_premium  # insurance.premium-proration@^1
impl/python.py · 92 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

from typing import Optional, Sequence

from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_days_between import days_between  ← from dates.days-between ^1.0.0 · built alongside by fune
from .finance_proration import prorate  ← from finance.proration ^1.0.0 · built alongside by fune
from .insurance_premium_proration_types import CancellationBasis, CancellationRefund, ShortPeriodBand
from .money_amount import Money, assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate  ← from money.apply-rate ^1.0.0 · built alongside by fune


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def cancellation_return_premium(
    premium: Money,
    inception_date: str,
    expiry_date: str,
    cancellation_date: str,
    basis: CancellationBasis,
    short_period_scale: Sequence[ShortPeriodBand],
    minimum_retained: Optional[Money],
) -> CancellationRefund:
    """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.
    """
    if premium.minor < 0:
        raise ValueError("premium must not be negative, received %d" % (premium.minor,))
    total_days = days_between(inception_date, expiry_date)
    if total_days <= 0:
        raise ValueError(
            "expiryDate must be after inceptionDate, received %s to %s" % (inception_date, expiry_date)
        )
    days_in_force = days_between(inception_date, cancellation_date)
    if days_in_force < 0 or days_in_force > total_days:
        raise ValueError(
            "cancellationDate must be from inceptionDate to expiryDate, received %s" % (cancellation_date,)
        )

    if basis == "pro-rata":
        retained = prorate(premium, total_days, days_in_force).used
    elif basis == "short-period":
        if len(short_period_scale) == 0:
            raise ValueError("a short-period cancellation needs a scale")
        share = 10000
        found = False
        previous = 0
        for band in short_period_scale:
            if not _is_int(band.up_to) or band.up_to < 1:
                raise ValueError("upTo must be a whole number of at least 1, received %s" % (band.up_to,))
            if band.unit not in ("days", "months"):
                raise ValueError('unit must be days or months, received "%s"' % (band.unit,))
            bp = band.retained_basis_points
            if not _is_int(bp) or bp < 0 or bp > 10000:
                raise ValueError("retainedBasisPoints must be from 0 to 10000, received %s" % (bp,))
            if bp < previous:
                raise ValueError("a short-period scale must not keep less for a longer period")
            previous = bp
            if found:
                continue
            if band.unit == "days":
                within = days_in_force <= band.up_to
            else:
                within = cancellation_date <= add_months(inception_date, band.up_to)
            if within:
                share = bp
                found = True
        retained = apply_rate(premium, share, "half-up")
    else:
        raise ValueError('unknown basis "%s": use pro-rata or short-period' % (basis,))

    minimum_applied = False
    if minimum_retained is not None:
        assert_same_currency(premium, minimum_retained)
        if minimum_retained.minor < 0:
            raise ValueError("minimumRetained must not be negative, received %d" % (minimum_retained.minor,))
        # The insurer can never keep more than it was paid.
        floor = min(minimum_retained.minor, premium.minor)
        if floor > retained.minor:
            retained = money(floor, premium.currency)
            minimum_applied = True
    return CancellationRefund(
        days_in_force=days_in_force,
        total_days=total_days,
        retained=retained,
        return_premium=money(premium.minor - retained.minor, premium.currency),
        minimum_applied=minimum_applied,
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 5 dependencies, pins them in fune.lock, downloads only the Python 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
Download for Python insurance.premium-proration-1.0.1-python.fune · 31,049 bytes sha256 6481f42f7398b1a9cb7ac679969b5610e78f51947f0b1c17885d4cc1d571e877

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./insurance.premium-proration-1.0.1-python.fune, or fetch it from a terminal with fune pull insurance.premium-proration@1.0.1:python.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md3,358
impl/python.py3,989
impl/rust.rs5,471
impl/typescript.ts3,871
vectors.json16,886