insurance.instalment-plan Unreviewed
Pay a premium by monthly instalments: deposit, flat credit charge, instalments exact to the penny, and the APR.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 15 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
A premium paid monthly: an optional deposit, a credit charge on the rest, the instalments to the penny, and the APR of the credit.
## The credit charge: flat, then the APR from it
For example
instalment_plan(£600.00, £0.00, 10, 10%, 1)→ deposit £0.00, amount financed £600.00, credit charge £60.00, instalments £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, total payable £660.00, ap… 600.00 over ten months at a flat 10%: 66.00 a month, and an APR of 23.5%, not 10%instalment_plan(£745.50, £149.10, 11, 9.9%, 1)→ deposit £149.10, amount financed £596.40, credit charge £59.04, instalments £59.64, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, total payable £… a 20% deposit, 9.9% flat on the rest, eleven instalments: the odd 6p go on the firstinstalment_plan(£600.00, £0.00, 12, 0%, 1)→ deposit £0.00, amount financed £600.00, credit charge £0.00, instalments £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, total paya… interest-free instalments have an APR of 0.0%
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 instalment_plan(premium: &Money, deposit: &Money, instalment_count: i64, credit_charge_basis_points: i64, first_instalment_month: i64) -> InstalmentPlan
| premium | Money | the premium to be paid, IPT included |
| deposit | Money | paid at the start, 0 or more and less than the premium |
| instalment_count | int | monthly instalments, 1 or more |
| credit_charge_basis_points | int | the flat charge on the amount financed: 990 = 9.9% |
| first_instalment_month | int | months from the start to the first instalment: 0 at inception, 1 a month later |
| returns | InstalmentPlan |
The type it declares, generated into your project
/// The deposit, what is financed, what it costs and how it is repaid.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstalmentPlan {
pub deposit: Money,
/// premium minus deposit
pub amount_financed: Money,
/// amountFinanced times the flat rate, rounded half-up
pub credit_charge: Money,
/// monthly, in order; the first carries any odd pennies
pub instalments: Vec<Money>,
/// deposit plus every instalment
pub total_payable: Money,
/// the APR of the credit, by the FCA's total charge for credit rules
pub apr: AprResult,
}
Your code names it in one line, in the file that uses it
fune!(insurance.instalment-plan@^1); // then call instalment_plan(…)
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::lending_apr::{apr, apr_result_to_value, CreditFlow}; ← from lending.apr ^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
/// A premium paid by monthly instalments.
///
/// The credit charge is a flat percentage of the amount financed (premium
/// less deposit), the way UK insurers quote it. The instalments are the
/// amount financed plus the charge, split so every penny is accounted for:
/// equal instalments rounded down, with the odd pennies on the first. The APR
/// is then worked from those exact cash flows by lending.apr, because a flat
/// 10% repaid over ten months costs far more than 10% a year.
///
/// # Panics
/// Panics on mixed currencies, a deposit that is negative or covers the whole
/// premium, bad counts or months, or instalments smaller than a minor unit.
pub fn instalment_plan(
premium: &Money,
deposit: &Money,
instalment_count: i64,
credit_charge_basis_points: i64,
first_instalment_month: i64,
) -> InstalmentPlan {
assert_same_currency(premium, deposit);
if instalment_count < 1 {
panic!("instalmentCount must be a whole number of 1 or more, received {}", instalment_count);
}
if credit_charge_basis_points < 0 {
panic!(
"creditChargeBasisPoints must be a whole number of 0 or more, received {}",
credit_charge_basis_points
);
}
if first_instalment_month < 0 {
panic!("firstInstalmentMonth must be a whole number of 0 or more, received {}", first_instalment_month);
}
if deposit.minor < 0 {
panic!("deposit must not be negative, received {}", deposit.minor);
}
if deposit.minor >= premium.minor {
panic!("the deposit leaves nothing to pay by instalments");
}
if instalment_count == 1 && first_instalment_month == 0 {
panic!("a single instalment at inception is payment in full, not credit");
}
let c = &premium.currency;
let financed = money(premium.minor - deposit.minor, c);
let charge = apply_rate(&financed, credit_charge_basis_points, "half-up");
let repayable = financed.minor + charge.minor;
let each = repayable / instalment_count;
if each < 1 {
panic!("each instalment must be at least one minor unit");
}
let first = repayable - each * (instalment_count - 1);
let mut instalments = Vec::new();
let mut repayments = Vec::new();
for i in 0..instalment_count {
let amount = money(if i == 0 { first } else { each }, c);
repayments.push(CreditFlow {
period: first_instalment_month + i,
amount: amount.clone(),
});
instalments.push(amount);
}
let rate = apr(
&[CreditFlow {
period: 0,
amount: financed.clone(),
}],
&repayments,
12,
);
InstalmentPlan {
deposit: deposit.clone(),
amount_financed: financed,
credit_charge: charge,
instalments,
total_payable: money(deposit.minor + repayable, c),
apr: rate,
}
}
pub fn instalment_plan_to_value(p: &InstalmentPlan) -> Value {
Value::obj(vec![
("deposit", money_to_value(&p.deposit)),
("amountFinanced", money_to_value(&p.amount_financed)),
("creditCharge", money_to_value(&p.credit_charge)),
("instalments", Value::Arr(p.instalments.iter().map(money_to_value).collect())),
("totalPayable", money_to_value(&p.total_payable)),
("apr", apr_result_to_value(&p.apr)),
])
}
fn int_arg(v: &Value, message: &str) -> i64 {
if let Value::Float(f) = v {
panic!("{}, received {}", message, f);
}
v.as_i64()
}
pub fn fune_vector(args: &[Value]) -> Value {
instalment_plan_to_value(&instalment_plan(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
int_arg(&args[2], "instalmentCount must be a whole number of 1 or more"),
int_arg(&args[3], "creditChargeBasisPoints must be a whole number of 0 or more"),
int_arg(&args[4], "firstInstalmentMonth must be a whole number of 0 or more"),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 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.instalment-plan
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./insurance.instalment-plan-1.0.1-rust.fune, or fetch it from a terminal with fune pull insurance.instalment-plan@1.0.1:rust.
The whole function, every language, is one file too: insurance.instalment-plan-1.0.1.fune, 27,422 bytes, sha256 73ebf4fcf9f102c86ff18e2e44f3495f7f985cc4dd9631f7b89d73928641c6d3. 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.instalment-plan
after — your function gets the result and the arguments, and returns the final result.
// fune: after insurance.instalment-plan
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 lending.apr in insurance.instalment-plan
// fune: replace money.amount in insurance.instalment-plan
// fune: replace money.apply-rate in insurance.instalment-plan
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.instalment-plan --steps.
// fune: step insurance.instalment-plan 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 | |
|---|---|---|---|
| 600.00 over ten months at a flat 10%: 66.00 a month, and an APR of 23.5%, not 10% | £600.00, £0.00, 10, 10%, 1 | → | deposit £0.00, amount financed £600.00, credit charge £60.00, instalments £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, £66.00, total payable £660.00, ap… |
| a 20% deposit, 9.9% flat on the rest, eleven instalments: the odd 6p go on the first | £745.50, £149.10, 11, 9.9%, 1 | → | deposit £149.10, amount financed £596.40, credit charge £59.04, instalments £59.64, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, £59.58, total payable £… |
| interest-free instalments have an APR of 0.0% | £600.00, £0.00, 12, 0%, 1 | → | deposit £0.00, amount financed £600.00, credit charge £0.00, instalments £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, £50.00, total paya… |
| the first instalment taken at inception brings the APR up | £480.00, £0.00, 12, 15%, 0 | → | deposit £0.00, amount financed £480.00, credit charge £72.00, instalments £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, £46.00, total pay… |
| one payment a month later at 5% is 1.05^12 - 1 = 79.59% a year | £100.00, £0.00, 1, 5%, 1 | → | deposit £0.00, amount financed £100.00, credit charge £5.00, instalments £105.00, total payable £105.00, apr … |
| 100.00 over three interest-free instalments: the odd penny goes on the first | £100.00, £0.00, 3, 0%, 1 | → | deposit £0.00, amount financed £100.00, credit charge £0.00, instalments £33.34, £33.33, £33.33, total payable £100.00, apr … |
| a deposit of the whole premium is refused | £600.00, £600.00, 10, 10%, 1 | → | error: the deposit leaves nothing to pay by instalments |
| a negative deposit is refused | £600.00, -£0.01, 10, 10%, 1 | → | error: deposit must not be negative |
| no instalments is refused | £600.00, £0.00, 0, 10%, 1 | → | error: instalmentCount must be a whole number of 1 or more |
| a fractional count is refused | £600.00, £0.00, 10.5, 10%, 1 | → | error: instalmentCount must be a whole number of 1 or more |
Show the other 5 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a negative charge is refused | £600.00, £0.00, 10, -0.01%, 1 | → | error: creditChargeBasisPoints must be a whole number of 0 or more |
| a negative first month is refused | £600.00, £0.00, 10, 10%, -1 | → | error: firstInstalmentMonth must be a whole number of 0 or more |
| one instalment at inception is not credit | £600.00, £0.00, 1, 10%, 0 | → | error: a single instalment at inception is payment in full, not credit |
| instalments smaller than a penny are refused | £0.05, £0.00, 10, 0%, 1 | → | error: each instalment must be at least one minor unit |
| a deposit in another currency is refused | £600.00, €1.00, 10, 10%, 1 | → | error: currency mismatch |
More from the author
UK insurers price monthly payment as a **flat charge** on the amount financed ("pay monthly: 9.9% charge"), so that is what the function takes:
amountFinanced = premium - deposit creditCharge = amountFinanced x creditChargeBasisPoints / 10000, half-up instalments = (amountFinanced + creditCharge) over instalmentCount months
A flat rate is not an annual rate, and consumer credit must be disclosed with its **APR**, so the plan also carries the APR worked from the exact cash flows by `lending.apr` (FCA CONC App 1.2.6R): the amount financed advanced at month 0, each instalment at its month, a year of 12 months. A flat 10% repaid over ten months is an APR of 23.5%, because on average only about half the money is borrowed for the whole time. The function does not go the other way (take an APR and derive the charge): the insurer sets the flat charge, and the APR is the disclosure measure computed from it. To price to a target APR, try charges and compare.
The deposit is paid at the start and is not credit, so it is left out of the APR. Only the charge for paying by instalments is in it; any other fee paid up front would be a repayment at month 0 and belongs in a call to `lending.apr` directly.
## Instalments to the penny
Every instalment is the repayable total divided by the count, rounded down, and the pennies left over go on the **first** instalment, so the schedule adds up exactly: 100.00 over three is 33.34, 33.33, 33.33. `firstInstalmentMonth` says when the first is taken: 0 at inception (common with no deposit), 1 a month later. A single instalment at inception is not credit and is refused.
## Notes
- `premium` should include IPT: IPT is charged on the premium, and the instalment charge for paying an insurer by instalments is itself part of the taxable premium (HMRC IPT03400). Where the finance is arranged by a third-party premium finance company it is not; apply `insurance.ipt` to suit. - Whether an instalment agreement is regulated credit (and so needs the APR disclosed) depends on how it is arranged; see the FCA's CONC and PERG 2.
## 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,387 |
| impl/python.py | 3,067 |
| impl/rust.rs | 4,189 |
| impl/typescript.ts | 2,930 |
| vectors.json | 8,901 |