finance.late-payment-interest
UK statutory late payment interest (8% over Bank Rate) and fixed compensation under the Late Payment Act 1998.
1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra
Pinned by 18 tests, run in TypeScript, Python and Rust.
Not professional advice. This capability calculates statutory interest figures from published rules. It is a software component for developers, not legal 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 a solicitor review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
What a UK business can claim when a business customer pays late, under the Late Payment of Commercial Debts (Interest) Act 1998: statutory interest, and a fixed sum of compensation for each debt.
**When interest runs.** Statutory interest starts on the day after the relevant day, the date payment was due (s.4(2)), and runs to the day of payment. So a debt due on 31 January and paid on 22 March 2026 is 50 days late. Paid on or before the due date, nothing is due and the reference fields are null.
For example
late_payment_interest(£1,000.00, 2026-01-31, 2026-03-22)→ days late 50, reference date 2025-12-31, reference rate basis points 3.75%, interest rate basis points 11.75%, interest £16.10, compensation £70.00, total £86.10 GBP 1,000 fifty days late in 2026: 11.75% on the 31 December 2025 rate, and GBP 70late_payment_interest(£1,000.00, 2010-06-15, 2010-08-04)→ days late 50, reference date 2009-12-31, reference rate basis points 0.5%, interest rate basis points 8.5%, interest £11.64, compensation £70.00, total £81.64 gov.uk's example at 8.5%: exactly 11.64, not 50 days at a rounded 23p (11.50)late_payment_interest(£5,000.00, 2022-06-30, 2023-06-30)→ days late 365, reference date 2022-06-30, reference rate basis points 1.25%, interest rate basis points 9.25%, interest £462.50, compensation £70.00, total £532.50 the reference rate is fixed when interest starts, however Bank Rate moves after
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 late_payment_interest(debt: &Money, due_date: &str, payment_date: &str) -> LatePaymentClaim
| debt | Money | the qualifying debt, in GBP |
| due_date | date | the last day payment was on time (the "relevant day") |
| payment_date | date | the day it was paid, or the day interest is claimed to if still unpaid |
| returns | LatePaymentClaim |
The type it declares, generated into your project
/// Statutory interest and the fixed sum for one late debt.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LatePaymentClaim {
/// days from the day after the due date to the payment date, both counted; 0 if paid on time
pub days_late: i64,
/// the 30 June or 31 December before interest started; null if paid on time
pub reference_date: Option<String>,
/// Bank Rate in force on the reference date; null if paid on time
pub reference_rate_basis_points: Option<i64>,
/// 8% over the reference rate; null if paid on time
pub interest_rate_basis_points: Option<i64>,
/// simple interest, daily at 1/365 of the yearly rate, rounded half-up once
pub interest: Money,
/// the fixed sum: 40, 70 or 100 pounds by debt size; zero if paid on time
pub compensation: Money,
/// interest plus compensation, not including the debt itself
pub total: Money,
}
Your code names it in one line, in the file that uses it
fune!(finance.late-payment-interest@^1); // then call late_payment_interest(…)
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_days::add_days; ← from dates.add-days ^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_late_payment_interest_data::{FIXED_SUMS, FIXED_SUMS_HISTORY}; ← this capability’s own data, compiled from data/fixed-sums.json into the same file by fune build
use super::finance_uk_bank_rate::uk_bank_rate; ← from finance.uk-bank-rate ^1.0.0 · built alongside by fune
use super::math_round_div::round_div; ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_money; ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
const STATUTORY_MARGIN: i64 = 800; // 8% over the reference rate, SI 2002/1675 art. 4
const MAX_EXACT: i128 = 9_007_199_254_740_991;
fn in_force(on_date: &str, valid_from: &str, valid_to: Option<&str>) -> bool {
on_date >= valid_from && valid_to.map_or(true, |to| on_date <= to)
}
fn pruned_horizon(history: &str, froms: &[&str], on_date: &str, what: &str) {
// A history=current build keeps only rows still in force; answering an older
// date with today's figure is the failure dated data exists to prevent.
if history == "full" || froms.is_empty() {
return;
}
let earliest = froms.iter().min().unwrap();
if on_date < *earliest {
panic!(
"no {} on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for older debts.",
what, on_date, history, earliest
);
}
}
fn fixed_sum_for(debt_minor: i64, on_date: &str) -> i64 {
let mut best: Option<(i64, i64)> = None;
for row in FIXED_SUMS {
if !in_force(on_date, row.valid_from, row.valid_to) || debt_minor < row.min_debt_minor {
continue;
}
if best.map_or(true, |(min, _)| row.min_debt_minor > min) {
best = Some((row.min_debt_minor, row.compensation_minor));
}
}
match best {
Some((_, compensation)) => compensation,
None => {
let froms: Vec<&str> = FIXED_SUMS.iter().map(|r| r.valid_from).collect();
pruned_horizon(FIXED_SUMS_HISTORY, &froms, on_date, "Late Payment Act fixed sum");
panic!("no Late Payment Act fixed sum in force on {}", on_date);
}
}
}
/// The 30 June or 31 December immediately before the day interest starts.
fn reference_date_for(start_date: &str) -> String {
let year: i64 = start_date[0..4].parse().unwrap();
let month: i64 = start_date[5..7].parse().unwrap();
if month <= 6 {
format!("{:04}-12-31", year - 1)
} else {
format!("{}-06-30", &start_date[0..4])
}
}
/// Statutory interest and the fixed sum on a late commercial debt (UK).
///
/// The reference rate is fixed when interest starts, not when it is paid, and
/// interest is simple at 1/365 of the yearly rate per day, rounded once.
///
/// # Panics
/// Panics on a non-GBP or non-positive debt, malformed dates, dates outside the
/// rules' data, or a product beyond the exact integer range.
pub fn late_payment_interest(debt: &Money, due_date: &str, payment_date: &str) -> LatePaymentClaim {
if debt.currency != "GBP" {
panic!("late payment interest is defined for GBP debts, received {}", debt.currency);
}
if debt.minor <= 0 {
panic!("debt must be greater than zero");
}
let late = days_between(due_date, payment_date);
let zero = money(0, "GBP");
if late <= 0 {
return LatePaymentClaim {
days_late: 0,
reference_date: None,
reference_rate_basis_points: None,
interest_rate_basis_points: None,
interest: zero.clone(),
compensation: zero.clone(),
total: zero,
};
}
let start_date = add_days(due_date, 1);
let compensation = money(fixed_sum_for(debt.minor, &start_date), "GBP");
let reference_date = reference_date_for(&start_date);
let reference_rate = uk_bank_rate(&reference_date).basis_points;
let rate = STATUTORY_MARGIN + reference_rate;
if (debt.minor as i128) * (rate as i128) * (late as i128) > MAX_EXACT {
panic!("the interest calculation exceeds the exact integer range");
}
let interest = money(round_div(debt.minor * rate * late, 10000 * 365, "half-up"), "GBP");
let total = add_money(&interest, &compensation);
LatePaymentClaim {
days_late: late,
reference_date: Some(reference_date),
reference_rate_basis_points: Some(reference_rate),
interest_rate_basis_points: Some(rate),
interest,
compensation,
total,
}
}
fn optional_int(value: Option<i64>) -> Value {
value.map_or(Value::Null, Value::Int)
}
pub fn late_payment_claim_to_value(c: &LatePaymentClaim) -> Value {
Value::obj(vec![
("daysLate", Value::Int(c.days_late)),
("referenceDate", c.reference_date.as_deref().map_or(Value::Null, Value::str)),
("referenceRateBasisPoints", optional_int(c.reference_rate_basis_points)),
("interestRateBasisPoints", optional_int(c.interest_rate_basis_points)),
("interest", money_to_value(&c.interest)),
("compensation", money_to_value(&c.compensation)),
("total", money_to_value(&c.total)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
late_payment_claim_to_value(&late_payment_interest(
&money_from_value(&args[0]),
args[1].as_str(),
args[2].as_str(),
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 6 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 finance.late-payment-interest
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./finance.late-payment-interest-1.0.0-rust.fune, or fetch it from a terminal with fune pull finance.late-payment-interest@1.0.0:rust.
The whole function, every language, is one file too: finance.late-payment-interest-1.0.0.fune, 28,717 bytes, sha256 681b9c39f016a4d30f6fe3fd0c204142e0db2b63ef15691e2fcbdec21a9a721f. 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 finance.late-payment-interest
after — your function gets the result and the arguments, and returns the final result.
// fune: after finance.late-payment-interest
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-days in finance.late-payment-interest
// fune: replace dates.days-between in finance.late-payment-interest
// fune: replace finance.uk-bank-rate in finance.late-payment-interest
// fune: replace math.round-div in finance.late-payment-interest
// fune: replace money.add in finance.late-payment-interest
// fune: replace money.amount in finance.late-payment-interest
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 finance.late-payment-interest --steps.
// fune: step finance.late-payment-interest 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 | |
|---|---|---|---|
| GBP 1,000 fifty days late in 2026: 11.75% on the 31 December 2025 rate, and GBP 70 | £1,000.00, 2026-01-31, 2026-03-22 | → | days late 50, reference date 2025-12-31, reference rate basis points 3.75%, interest rate basis points 11.75%, interest £16.10, compensation £70.00, total £86.10 |
| gov.uk's example at 8.5%: exactly 11.64, not 50 days at a rounded 23p (11.50) | £1,000.00, 2010-06-15, 2010-08-04 | → | days late 50, reference date 2009-12-31, reference rate basis points 0.5%, interest rate basis points 8.5%, interest £11.64, compensation £70.00, total £81.64 |
| the reference rate is fixed when interest starts, however Bank Rate moves after | £5,000.00, 2022-06-30, 2023-06-30 | → | days late 365, reference date 2022-06-30, reference rate basis points 1.25%, interest rate basis points 9.25%, interest £462.50, compensation £70.00, total £532.50 |
| due 29 June 2023: interest starts 30 June, so the reference date is 31 December 2022 | £1,000.00, 2023-06-29, 2023-07-09 | → | days late 10, reference date 2022-12-31, reference rate basis points 3.5%, interest rate basis points 11.5%, interest £3.15, compensation £70.00, total £73.15 |
| due a day later: interest starts 1 July, so the reference date is 30 June 2023 | £1,000.00, 2023-06-30, 2023-07-10 | → | days late 10, reference date 2023-06-30, reference rate basis points 5%, interest rate basis points 13%, interest £3.56, compensation £70.00, total £73.56 |
| one day late on a debt under GBP 1,000: GBP 40 | £999.99, 2026-07-31, 2026-08-01 | → | days late 1, reference date 2026-06-30, reference rate basis points 3.75%, interest rate basis points 11.75%, interest £0.32, compensation £40.00, total £40.32 |
| GBP 10,000 exactly is in the GBP 100 band, at 8.1% after the 2020 cut | £10,000.00, 2020-12-31, 2021-01-31 | → | days late 31, reference date 2020-12-31, reference rate basis points 0.1%, interest rate basis points 8.1%, interest £68.79, compensation £100.00, total £168.79 |
| GBP 9,999.99 is still in the GBP 70 band | £9,999.99, 2026-01-31, 2026-02-01 | → | days late 1, reference date 2025-12-31, reference rate basis points 3.75%, interest rate basis points 11.75%, interest £3.22, compensation £70.00, total £73.22 |
| a year spanning 29 February is 366 days, each 1/365 of the yearly rate | £1,000.00, 2024-02-01, 2025-02-01 | → | days late 366, reference date 2023-12-31, reference rate basis points 5.25%, interest rate basis points 13.25%, interest £132.86, compensation £70.00, total £202.86 |
| an exact half penny rounds up | £1.46, 2006-01-10, 2006-01-20 | → | days late 10, reference date 2005-12-31, reference rate basis points 4.5%, interest rate basis points 12.5%, interest £0.01, compensation £40.00, total £40.01 |
Show the other 8 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| paid on the due date: nothing is due | £1,000.00, 2026-03-31, 2026-03-31 | → | days late 0, reference date —, reference rate basis points —, interest rate basis points —, interest £0.00, compensation £0.00, total £0.00 |
| paid early: nothing is due | £1,000.00, 2026-03-31, 2026-03-01 | → | days late 0, reference date —, reference rate basis points —, interest rate basis points —, interest £0.00, compensation £0.00, total £0.00 |
| the earliest start the 2002 rules cover | £1,000.00, 2002-08-06, 2002-08-07 | → | days late 1, reference date 2002-06-30, reference rate basis points 4%, interest rate basis points 12%, interest £0.33, compensation £70.00, total £70.33 |
| a debt in another currency is refused | €1,000.00, 2026-01-31, 2026-03-22 | → | error: late payment interest is defined for GBP debts, received EUR |
| a zero debt is refused | £0.00, 2026-01-31, 2026-03-22 | → | error: debt must be greater than zero |
| before the 2002 rules there is no fixed sum | £1,000.00, 2002-01-01, 2002-02-01 | → | error: no Late Payment Act fixed sum in force on 2002-01-02 |
| an impossible payment date is refused | £1,000.00, 2026-01-31, 2026-02-30 | → | error: not a real calendar date |
| a product beyond exact integers is refused | £9,000,000,000,000.00, 2026-01-31, 2026-03-22 | → | error: the interest calculation exceeds the exact integer range |
More from the author
**The rate** is 8% a year over the reference rate, and the reference rate is fixed once, when interest starts: the Bank of England's official dealing rate (Bank Rate) in force on 30 June, for interest that starts to run between 1 July and 31 December, or on 31 December, for interest that starts between 1 January and 30 June (Late Payment of Commercial Debts (Rate of Interest) (No. 3) Order 2002, SI 2002/1675, article 4). It does not move when Bank Rate moves afterwards. A debt due on 29 June 2023 is referenced to 31 December 2022 (3.5%, so 11.5%); one due a day later starts on 1 July and is referenced to 30 June 2023 (5%, so 13%).
**The interest** is simple, one day's interest being 1/365 of a year's, in leap years too, as gov.uk describes it. It is worked out exactly and rounded half-up to the penny once: debt x rate x days / (10000 x 365). The gov.uk worked example rounds the daily figure first (GBP 1,000 at 8.5% is "23p a day", so GBP 11.50 for 50 days); exactly, it is GBP 11.64. Keeping the full precision is the defensible reading of the Act, and the difference is shown in the vectors.
**The fixed sum** (s.5A, in force from 7 August 2002) is GBP 40 for a debt under GBP 1,000, GBP 70 from GBP 1,000 up to but not including GBP 10,000, and GBP 100 from GBP 10,000; once per debt, once interest starts to run. Reasonable recovery costs above the fixed sum (s.5A(2A), from 2013) are a matter of evidence, not arithmetic, and are not included.
The debt must be in GBP: the Act's fixed sums are sterling amounts. Whether a debt qualifies at all (a business-to-business contract, no substantial contractual remedy agreed instead) is a legal question the caller answers first. Where no due date was agreed, the relevant day is 30 days after the later of delivery and the invoice (s.4); pass that date as `dueDate`.
## Data and sources
- Bank Rate comes from finance.uk-bank-rate: every change from 8 November 2001 (4%, the rate on the first reference date the 2002 Order can use, 30 June 2002) to 18 December 2025 (3.75%), from the Bank of England's "Official Bank Rate history", https://www.bankofengland.co.uk/boeapps/database/Bank-Rate.asp, checked on 23 September 2026. - `data/fixed-sums.json`: the three fixed sums of s.5A(2), from 7 August 2002. - Late Payment of Commercial Debts (Interest) Act 1998, ss.4 and 5A: https://www.legislation.gov.uk/ukpga/1998/20 - SI 2002/1675, article 4 (the rate and the reference dates): https://www.legislation.gov.uk/uksi/2002/1675/article/4/made - gov.uk, "Late commercial payments: charging interest and debt recovery": https://www.gov.uk/late-commercial-payments-interest-debt-recovery/charging-interest-commercial-debt
When Bank Rate changes, finance.uk-bank-rate publishes a new version and this capability picks it up through its `^1.0.0` range; nothing here changes. Both data files declare their effective dates, so a `history=current` build keeps only the rows still in force and refuses a date before its horizon rather than answering with today's figure.
Files
| Path | Bytes |
|---|---|
| README.md | 3,580 |
| data/fixed-sums.json | 561 |
| impl/python.py | 3,862 |
| impl/rust.rs | 5,257 |
| impl/typescript.ts | 3,961 |
| vectors.json | 6,379 |