Functional Weave
Code in Python

finance.late-payment-interest

UK statutory late payment interest (8% over Bank Rate) and fixed compensation under the Late Payment Act 1998.

1.0.1 · 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 70
  • late_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.

def late_payment_interest(debt: Money, due_date: str, payment_date: str) -> LatePaymentClaim
debtMoneythe qualifying debt, in GBP
due_datedatethe last day payment was on time (the "relevant day")
payment_datedatethe day it was paid, or the day interest is claimed to if still unpaid
returnsLatePaymentClaim

The type it declares, generated into your project

@dataclass(frozen=True)
class LatePaymentClaim:
    """Statutory interest and the fixed sum for one late debt."""

    #: days from the day after the due date to the payment date, both counted; 0 if paid on time
    days_late: int
    #: the 30 June or 31 December before interest started; null if paid on time
    reference_date: Optional[str]
    #: Bank Rate in force on the reference date; null if paid on time
    reference_rate_basis_points: Optional[int]
    #: 8% over the reference rate; null if paid on time
    interest_rate_basis_points: Optional[int]
    #: simple interest, daily at 1/365 of the yearly rate, rounded half-up once
    interest: Money
    #: the fixed sum: 40, 70 or 100 pounds by debt size; zero if paid on time
    compensation: Money
    #: interest plus compensation, not including the debt itself
    total: Money

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

from fune.finance.late_payment_interest import late_payment_interest  # finance.late-payment-interest@^1
impl/python.py · 93 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_days import add_days  ← from dates.add-days ^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_late_payment_interest_data import FIXED_SUMS, FIXED_SUMS_HISTORY  ← this capability’s own data, compiled from data/fixed-sums.json into the same file by fune build
from .finance_late_payment_interest_types import LatePaymentClaim
from .finance_uk_bank_rate import uk_bank_rate  ← from finance.uk-bank-rate ^1.0.0 · built alongside by fune
from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_add import add_money  ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune

_STATUTORY_MARGIN = 800  # 8% over the reference rate, SI 2002/1675 art. 4
_MAX_EXACT = 2**53 - 1


def _in_force(on_date: str, valid_from: str, valid_to: Optional[str]) -> bool:
    return on_date >= valid_from and (valid_to is None or on_date <= valid_to)


def _pruned_horizon(history: str, froms: Sequence[str], on_date: str, what: str) -> None:
    # 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" or len(froms) == 0:
        return
    earliest = min(froms)
    if on_date < earliest:
        raise ValueError(
            "no %s on %s: this build was installed with history=%s, so it only carries rules from %s. "
            "Reinstall with history=full for older debts." % (what, on_date, history, earliest)
        )


def _fixed_sum_for(debt_minor: int, on_date: str) -> int:
    best = None
    for row in FIXED_SUMS:
        if not _in_force(on_date, row.valid_from, row.valid_to) or debt_minor < row.min_debt_minor:
            continue
        if best is None or row.min_debt_minor > best.min_debt_minor:
            best = row
    if best is None:
        _pruned_horizon(FIXED_SUMS_HISTORY, [r.valid_from for r in FIXED_SUMS], on_date, "Late Payment Act fixed sum")
        raise ValueError("no Late Payment Act fixed sum in force on %s" % (on_date,))
    return best.compensation_minor


def _reference_date_for(start_date: str) -> str:
    """The 30 June or 31 December immediately before the day interest starts."""
    year = int(start_date[0:4])
    month = int(start_date[5:7])
    return "%04d-12-31" % (year - 1,) if month <= 6 else "%s-06-30" % (start_date[0:4],)


def late_payment_interest(debt: Money, due_date: str, payment_date: str) -> LatePaymentClaim:
    """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.
    """
    if debt.currency != "GBP":
        raise ValueError("late payment interest is defined for GBP debts, received %s" % (debt.currency,))
    if debt.minor <= 0:
        raise ValueError("debt must be greater than zero")
    late = days_between(due_date, payment_date)
    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,
            compensation=zero,
            total=zero,
        )

    start_date = add_days(due_date, 1)
    compensation = money(_fixed_sum_for(debt.minor, start_date), "GBP")
    reference_date = _reference_date_for(start_date)
    reference_rate = uk_bank_rate(reference_date).basis_points
    rate = _STATUTORY_MARGIN + reference_rate

    if debt.minor * rate * late > _MAX_EXACT:
        raise ValueError("the interest calculation exceeds the exact integer range")
    interest = money(round_div(debt.minor * rate * late, 10000 * 365, "half-up"), "GBP")
    return LatePaymentClaim(
        days_late=late,
        reference_date=reference_date,
        reference_rate_basis_points=reference_rate,
        interest_rate_basis_points=rate,
        interest=interest,
        compensation=compensation,
        total=add_money(interest, compensation),
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 6 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 finance.late-payment-interest
Download for Python finance.late-payment-interest-1.0.1-python.fune · 19,743 bytes sha256 139c848c6c924466de4e714218637a15a502f1f476f0e55884f5c6b15c798de5

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

The whole function, every language, is one file too: finance.late-payment-interest-1.0.1.fune, 29,300 bytes, sha256 4f0ce88e17b75b3668af5374d8eaa335466e2cbc63d97da5ee194d4d6b06b789. 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.

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

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

Legislation: Crown copyright and database right.

1.0.1 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE242
README.md3,888
data/fixed-sums.json561
impl/python.py3,862
impl/rust.rs5,257
impl/typescript.ts3,961
vectors.json6,379