insurance.mid-term-adjustment Unreviewed
Additional or return premium for a mid-term policy change, pro rata on the days left, plus an admin fee.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 17 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 additional premium (AP) or return premium (RP) when a policy is changed part-way through its term: a driver added, a car swapped, cover reduced.
## How it is worked out
For example
mid_term_adjustment(£500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, £25.00, half-up)→ total days 365, remaining days 183, premium change £60.16, admin fee £25.00, amount due £85.16, kind additional adding a driver half way: 120.00 more a year for 183 of 365 days is 60.16, plus a 25.00 feemid_term_adjustment(£500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, £0.00, half-up)→ total days 365, remaining days 183, premium change £60.16, admin fee £0.00, amount due £60.16, kind additional one rounding of the difference, not two: pro-rating 500.00 and 620.00 separately would give 60.17mid_term_adjustment(£620.00, £500.00, 2026-01-01, 2027-01-01, 2026-07-02, £25.00, half-up)→ total days 365, remaining days 183, premium change -£60.16, admin fee £25.00, amount due -£35.16, kind return removing the driver returns 60.16, less the 25.00 fee
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 mid_term_adjustment(old_premium: Money, new_premium: Money, inception_date: str, expiry_date: str, change_date: str, admin_fee: Money, mode: RoundingMode) -> MidTermAdjustment
| old_premium | Money | the full-term premium before the change |
| new_premium | Money | the full-term premium the policy would have cost with the change from inception |
| inception_date | date | the first day of the policy term |
| expiry_date | date | the day the term ends, exclusive: a year from 2026-01-01 ends 2027-01-01 |
| change_date | date | the first day of the changed cover, from inceptionDate to expiryDate |
| admin_fee | Money | the insurer's or broker's charge for making the change, 0 or more |
| mode | RoundingMode | how the pro-rata premium change rounds to a minor unit |
| returns | MidTermAdjustment |
The types it declares, generated into your project
AdjustmentKind = Literal["additional", "return", "none"]
@dataclass(frozen=True)
class MidTermAdjustment:
"""The premium change for the rest of the term, the fee and what is due."""
total_days: int
#: days from changeDate to expiryDate
remaining_days: int
#: the difference in premium for the remaining days; negative is a return premium
premium_change: Money
admin_fee: Money
#: premiumChange plus adminFee; negative is owed to the policyholder
amount_due: Money
#: additional or return premium by the sign of premiumChange
kind: AdjustmentKind
Your code names it in one line, in the file that uses it
from fune.insurance.mid_term_adjustment import mid_term_adjustment # insurance.mid-term-adjustment@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from .dates_days_between import days_between ← from dates.days-between ^1.0.0 · built alongside by fune
from .insurance_mid_term_adjustment_types import MidTermAdjustment
from .math_round_div import RoundingMode, round_div ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency, money ← from money.amount ^1.0.0 · built alongside by fune
def mid_term_adjustment(
old_premium: Money,
new_premium: Money,
inception_date: str,
expiry_date: str,
change_date: str,
admin_fee: Money,
mode: RoundingMode,
) -> MidTermAdjustment:
"""The additional or return premium for a change part-way through a policy term.
The difference between the two full-term premiums is pro-rated over the
days left, with one rounding. Pro-rating the old and new premiums
separately and subtracting rounds twice and can be a penny out.
"""
assert_same_currency(old_premium, new_premium)
assert_same_currency(old_premium, admin_fee)
if old_premium.minor < 0 or new_premium.minor < 0:
raise ValueError("premiums must not be negative")
if admin_fee.minor < 0:
raise ValueError("adminFee must not be negative, received %d" % (admin_fee.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)
)
remaining_days = days_between(change_date, expiry_date)
if remaining_days < 0 or remaining_days > total_days:
raise ValueError("changeDate must be from inceptionDate to expiryDate, received %s" % (change_date,))
change = round_div((new_premium.minor - old_premium.minor) * remaining_days, total_days, mode)
currency = old_premium.currency
return MidTermAdjustment(
total_days=total_days,
remaining_days=remaining_days,
premium_change=money(change, currency),
admin_fee=admin_fee,
amount_due=money(change + admin_fee.minor, currency),
kind="additional" if change > 0 else "return" if change < 0 else "none",
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 3 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.mid-term-adjustment
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./insurance.mid-term-adjustment-1.0.1-python.fune, or fetch it from a terminal with fune pull insurance.mid-term-adjustment@1.0.1:python.
The whole function, every language, is one file too: insurance.mid-term-adjustment-1.0.1.fune, 23,546 bytes, sha256 20a8cb56d87453621b8f8019e80a7a541baf3ddc581fffb67386f6c0b352b594. 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.mid-term-adjustment
after — your function gets the result and the arguments, and returns the final result.
# fune: after insurance.mid-term-adjustment
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.days-between in insurance.mid-term-adjustment
# fune: replace math.round-div in insurance.mid-term-adjustment
# fune: replace money.amount in insurance.mid-term-adjustment
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.mid-term-adjustment --steps.
# fune: step insurance.mid-term-adjustment 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 | |
|---|---|---|---|
| adding a driver half way: 120.00 more a year for 183 of 365 days is 60.16, plus a 25.00 fee | £500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, £25.00, half-up | → | total days 365, remaining days 183, premium change £60.16, admin fee £25.00, amount due £85.16, kind additional |
| one rounding of the difference, not two: pro-rating 500.00 and 620.00 separately would give 60.17 | £500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, £0.00, half-up | → | total days 365, remaining days 183, premium change £60.16, admin fee £0.00, amount due £60.16, kind additional |
| removing the driver returns 60.16, less the 25.00 fee | £620.00, £500.00, 2026-01-01, 2027-01-01, 2026-07-02, £25.00, half-up | → | total days 365, remaining days 183, premium change -£60.16, admin fee £25.00, amount due -£35.16, kind return |
| rounding up charges the part penny | £500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, £0.00, up | → | total days 365, remaining days 183, premium change £60.17, admin fee £0.00, amount due £60.17, kind additional |
| a change from inception is the whole difference | £500.00, £620.00, 2026-01-01, 2027-01-01, 2026-01-01, £0.00, half-up | → | total days 365, remaining days 365, premium change £120.00, admin fee £0.00, amount due £120.00, kind additional |
| a change on the expiry date costs nothing but the fee | £500.00, £620.00, 2026-01-01, 2027-01-01, 2027-01-01, £25.00, half-up | → | total days 365, remaining days 0, premium change £0.00, admin fee £25.00, amount due £25.00, kind none |
| a change with no premium effect still carries its fee | £500.00, £500.00, 2026-01-01, 2027-01-01, 2026-03-15, £15.00, half-up | → | total days 365, remaining days 292, premium change £0.00, admin fee £15.00, amount due £15.00, kind none |
| a leap-year term has 366 days: 366.00 more for the last 31 is 31.00 | £0.00, £366.00, 2028-01-01, 2029-01-01, 2028-12-01, £0.00, half-up | → | total days 366, remaining days 31, premium change £31.00, admin fee £0.00, amount due £31.00, kind additional |
| exactly half a penny rounds up half-up | £100.00, £100.01, 2026-01-01, 2026-01-05, 2026-01-03, £0.00, half-up | → | total days 4, remaining days 2, premium change £0.01, admin fee £0.00, amount due £0.01, kind additional |
| exactly half a penny rounds to even: nothing to charge | £100.00, £100.01, 2026-01-01, 2026-01-05, 2026-01-03, £0.00, half-even | → | total days 4, remaining days 2, premium change £0.00, admin fee £0.00, amount due £0.00, kind none |
Show the other 7 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a return premium smaller than the fee still leaves something to pay | £500.00, £490.00, 2026-01-01, 2027-01-01, 2026-07-02, £25.00, half-up | → | total days 365, remaining days 183, premium change -£5.01, admin fee £25.00, amount due £19.99, kind return |
| a change before inception is refused | £500.00, £620.00, 2026-01-01, 2027-01-01, 2025-12-31, £0.00, half-up | → | error: changeDate must be from inceptionDate to expiryDate |
| a change after expiry is refused | £500.00, £620.00, 2026-01-01, 2027-01-01, 2027-01-02, £0.00, half-up | → | error: changeDate must be from inceptionDate to expiryDate |
| expiry must be after inception | £500.00, £620.00, 2027-01-01, 2026-01-01, 2026-01-01, £0.00, half-up | → | error: expiryDate must be after inceptionDate |
| a negative fee is refused | £500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, -£0.01, half-up | → | error: adminFee must not be negative |
| a negative premium is refused | -£500.00, £620.00, 2026-01-01, 2027-01-01, 2026-07-02, £0.00, half-up | → | error: premiums must not be negative |
| premiums in two currencies are refused | £500.00, €620.00, 2026-01-01, 2027-01-01, 2026-07-02, £0.00, half-up | → | error: currency mismatch |
More from the author
Give it the full-term premium before the change and the full-term premium the policy would cost with the change (as your rating engine quotes it), the term dates and the date the change takes effect. The difference is pro-rated on the days left:
premiumChange = (newPremium - oldPremium) x remainingDays / totalDays
with one rounding, in the caller's mode. Pro-rating the old and the new premium separately and subtracting rounds twice and can be a penny out (the vectors have one such case: 60.16, not 60.17).
`expiryDate` is exclusive (a year from 2026-01-01 ends 2027-01-01), and the days left are `expiryDate - changeDate`, so a change on the inception date is the whole difference and a change on the expiry date is nothing. Leap years count their 366 days.
## The admin fee
`adminFee` is added to what is due whichever way the premium moves, which is how most UK insurers and brokers apply an MTA fee: on a return premium the fee is deducted from the refund, and if the fee is larger the policyholder still pays the difference (`amountDue` positive while `kind` is `return`). Pass zero when no fee is charged. The fee is kept separate from the premium change because it is usually not premium: IPT (`insurance.ipt`) applies to `premiumChange`, and to the fee only when the insurer charges it as part of the premium.
## Not covered here
Short-period loadings on MTAs, minimum additional premiums and "no refund below X" rules are insurer-specific terms of business; apply them to the result.
## 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 | 2,789 |
| impl/python.py | 1,992 |
| impl/rust.rs | 2,851 |
| impl/typescript.ts | 1,921 |
| vectors.json | 8,637 |