Functional Weave
Code in Python

lending.early-settlement Unreviewed

Early settlement figure and rebate on UK regulated consumer credit, per the Early Settlement Regulations 2004.

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 18 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 consumer-credit compliance specialist 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 lending 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 a consumer-credit compliance specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

**Status: needs review by a consumer-credit specialist before publishing.**

The figure a debtor pays to settle a regulated consumer credit agreement (a personal loan, hire purchase, a car finance agreement) early, and the rebate of charges they are entitled to, under the Consumer Credit (Early Settlement) Regulations 2004 (SI 2004/1483, as amended by the Consumer Credit (EU Directive) Regulations 2010). Source: https://www.legislation.gov.uk/uksi/2004/1483, text read 2026-09-23.

For example

  • early_settlement(advances ×1, repayments ×36, 9.9%, 2026-03-01, —, none) → settlement date 2026-03-29, settlement amount £3,218.32, remaining repayments £3,542.00, rebate £323.68 £5,000 over 36 months at 9.9% APR: settlement 28 days after the notice
  • early_settlement(advances ×1, repayments ×36, 9.9%, 2026-03-01, —, one-month) → settlement date 2026-04-29, settlement amount £3,082.65, remaining repayments £3,381.00, rebate £298.35 the creditor defers by one month under regulation 6
  • early_settlement(advances ×1, repayments ×36, 9.9%, 2026-03-01, —, thirty-days) → settlement date 2026-04-28, settlement amount £3,081.85, remaining repayments £3,381.00, rebate £299.15 the creditor elects 30 days instead of a month

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 early_settlement(advances: Sequence[DatedAmount], repayments: Sequence[DatedAmount], apr_basis_points: int, notice_received_iso: str, requested_settlement_iso: Optional[str], deferral: SettlementDeferral) -> EarlySettlement
advancesDatedAmount[]the credit drawn, with dates
repaymentsDatedAmount[]the agreement's whole repayment schedule, charges included, with due dates
apr_basis_pointsintthe agreement's APR, 0 or more; 1990 = 19.9%
notice_received_isodatethe day the creditor received the debtor's notice to settle (CCA s.94)
requested_settlement_isodate?a later settlement date named in the notice, or null
deferralSettlementDeferralthe creditor's election under regulation 6: none, one-month or thirty-days
returnsEarlySettlement

The types it declares, generated into your project

@dataclass(frozen=True)
class DatedAmount:
    """An amount on a date."""

    date: str
    #: greater than zero
    amount: Money

SettlementDeferral = Literal["none", "one-month", "thirty-days"]

@dataclass(frozen=True)
class EarlySettlement:
    """The figure to quote and how it was reached."""

    #: the date the rebate is calculated to
    settlement_date: str
    #: what the debtor pays to settle, rounded down to the minor unit
    settlement_amount: Money
    #: the scheduled repayments on or after the settlement date
    remaining_repayments: Money
    #: remainingRepayments minus settlementAmount, never negative
    rebate: Money

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

from fune.lending.early_settlement import early_settlement  # lending.early-settlement@^1
impl/python.py · 99 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_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 .lending_early_settlement_types import DatedAmount, EarlySettlement, SettlementDeferral
from .math_fractional_power import FIXED_SCALE, pow_fixed, root_fixed  ← from math.fractional-power ^1.0.0 · built alongside by fune
from .money_amount import money  ← from money.amount ^1.0.0 · built alongside by fune


def _settlement_date(
    advances: Sequence[DatedAmount],
    repayments: Sequence[DatedAmount],
    notice_received_iso: str,
    requested_settlement_iso: Optional[str],
    deferral: SettlementDeferral,
) -> str:
    """The settlement date: 28 days after the creditor receives the notice
    (regulation 5(1)(a)), or a later date the notice names, then deferred by a
    month or 30 days where the creditor elects and regulation 6 allows it
    (credit repaid over more than a year from the first drawdown)."""
    date = add_days(notice_received_iso, 28)
    if requested_settlement_iso is not None:
        days_between(requested_settlement_iso, requested_settlement_iso)
        if requested_settlement_iso > date:
            date = requested_settlement_iso
    if deferral == "none":
        return date
    if deferral not in ("one-month", "thirty-days"):
        raise ValueError('unknown deferral "%s": expected none, one-month or thirty-days' % (deferral,))
    first = min(a.date for a in advances)
    last = max(r.date for r in repayments)
    if last <= add_months(first, 12):
        raise ValueError("deferral is only allowed when the credit is repaid over more than a year (regulation 6)")
    return add_months(date, 1) if deferral == "one-month" else add_days(date, 30)


def early_settlement(
    advances: Sequence[DatedAmount],
    repayments: Sequence[DatedAmount],
    apr_basis_points: int,
    notice_received_iso: str,
    requested_settlement_iso: Optional[str],
    deferral: SettlementDeferral,
) -> EarlySettlement:
    """The amount a debtor pays to settle a regulated consumer credit agreement
    early, and the rebate, per the Consumer Credit (Early Settlement)
    Regulations 2004, regulation 4(1).

    The rebate is the scheduled repayments falling due after the settlement
    date less  sum A_i (1 + r)^a_i - sum B_j (1 + r)^b_j  over the advances and
    the repayments made before the settlement date, each carried forward to
    the settlement date at the APR. That difference is the settlement figure.
    Times are days / 365 of a year.
    """
    if len(advances) == 0:
        raise ValueError("advances must not be empty")
    if len(repayments) == 0:
        raise ValueError("repayments must not be empty")
    b = apr_basis_points
    if isinstance(b, bool) or not isinstance(b, int) or b < 0 or b > 1000000:
        raise ValueError("aprBasisPoints must be between 0 and 1000000, received %r" % (b,))
    currency = advances[0].amount.currency
    for flow in list(advances) + list(repayments):
        days_between(flow.date, flow.date)
        if flow.amount.currency != currency:
            raise ValueError("currency mismatch: %s and %s" % (currency, flow.amount.currency))
        if flow.amount.minor <= 0:
            raise ValueError("every amount must be greater than zero, received %r" % (flow.amount.minor,))
    date = _settlement_date(advances, repayments, notice_received_iso, requested_settlement_iso, deferral)
    # (1 + APR)^(1/365), once; each flow is then a whole power of it.
    daily = root_fixed(FIXED_SCALE * (10000 + b) // 10000, 365)

    def carried(flow: DatedAmount) -> int:
        return flow.amount.minor * pow_fixed(daily, days_between(flow.date, date))

    owed = 0
    drawn = False
    for advance in advances:
        if advance.date < date:
            owed += carried(advance)
            drawn = True
    if not drawn:
        raise ValueError("no credit was advanced before the settlement date %s" % (date,))
    remaining = 0
    for repayment in repayments:
        if repayment.date < date:
            owed -= carried(repayment)
        else:
            remaining += repayment.amount.minor
    # Rounded down: the rebate must be at least the one the formula gives.
    formula = owed // FIXED_SCALE if owed > 0 else 0
    settlement = min(formula, remaining)
    return EarlySettlement(
        settlement_date=date,
        settlement_amount=money(settlement, currency),
        remaining_repayments=money(remaining, currency),
        rebate=money(remaining - settlement, currency),
    )

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 lending.early-settlement
Download for Python lending.early-settlement-1.0.1-python.fune · 87,650 bytes sha256 f1d1aab6670c345972a2cc2439395b19085ff1990766d3d6e6ec46be9152d455

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

The whole function, every language, is one file too: lending.early-settlement-1.0.1.fune, 98,562 bytes, sha256 0c7f98fcae78d3b8c877e90e6d3d6858547820c7c16f61b1da4451db3c34e1aa. 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 lending.early-settlement

after — your function gets the result and the arguments, and returns the final result.

# fune: after lending.early-settlement

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 lending.early-settlement
# fune: replace dates.add-months in lending.early-settlement
# fune: replace dates.days-between in lending.early-settlement
# fune: replace math.big-integer in lending.early-settlement
# fune: replace math.fractional-power in lending.early-settlement
# fune: replace money.amount in lending.early-settlement

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 lending.early-settlement --steps.

# fune: step lending.early-settlement 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
£5,000 over 36 months at 9.9% APR: settlement 28 days after the notice advances ×1, repayments ×36, 9.9%, 2026-03-01, —, none → settlement date 2026-03-29, settlement amount £3,218.32, remaining repayments £3,542.00, rebate £323.68
the creditor defers by one month under regulation 6 advances ×1, repayments ×36, 9.9%, 2026-03-01, —, one-month → settlement date 2026-04-29, settlement amount £3,082.65, remaining repayments £3,381.00, rebate £298.35
the creditor elects 30 days instead of a month advances ×1, repayments ×36, 9.9%, 2026-03-01, —, thirty-days → settlement date 2026-04-28, settlement amount £3,081.85, remaining repayments £3,381.00, rebate £299.15
a later date named in the notice is used advances ×1, repayments ×36, 9.9%, 2026-03-01, 2026-05-10, none → settlement date 2026-05-10, settlement amount £3,091.43, remaining repayments £3,381.00, rebate £289.57
an earlier date named in the notice cannot shorten the 28 days advances ×1, repayments ×36, 9.9%, 2026-03-01, 2026-03-05, none → settlement date 2026-03-29, settlement amount £3,218.32, remaining repayments £3,542.00, rebate £323.68
a month's deferral from the 31st lands on the last day of February advances ×1, repayments ×36, 9.9%, 2026-01-03, —, one-month → settlement date 2026-02-28, settlement amount £3,354.65, remaining repayments £3,703.00, rebate £348.35
interest-free credit: no rebate, the balance is owed advances ×1, repayments ×36, 0%, 2026-03-01, —, none → settlement date 2026-03-29, settlement amount £2,200.00, remaining repayments £2,200.00, rebate £0.00
a high-cost loan: 12 months at 49.9% advances ×1, repayments ×12, 49.9%, 2026-05-20, —, none → settlement date 2026-06-17, settlement amount £624.88, remaining repayments £735.00, rebate £110.12
notice straight after drawdown: nearly all the interest is rebated advances ×1, repayments ×36, 9.9%, 2025-01-16, —, none → settlement date 2025-02-13, settlement amount £5,037.64, remaining repayments £5,796.00, rebate £758.36
settling after the last instalment is due: nothing left to pay advances ×1, repayments ×12, 49.9%, 2027-01-01, —, none → settlement date 2027-01-29, settlement amount £0.00, remaining repayments £0.00, rebate £0.00
Show the other 8 tests
CaseArgumentsExpected
a deferral is refused on a one-year agreement advances ×1, repayments ×12, 49.9%, 2026-05-20, —, one-month → error: deferral is only allowed when the credit is repaid over more than a year
an unknown deferral advances ×1, repayments ×36, 9.9%, 2026-03-01, —, two-months → error: unknown deferral
no advances , repayments ×36, 9.9%, 2026-03-01, —, none → error: advances must not be empty
no repayments advances ×1, , 9.9%, 2026-03-01, —, none → error: repayments must not be empty
a negative APR is refused advances ×1, repayments ×36, -0.01%, 2026-03-01, —, none → error: aprBasisPoints must be between 0 and 1000000
mixed currencies are refused advances ×1, repayments ×1, 9.9%, 2026-03-01, —, none → error: currency mismatch
a notice before any drawdown advances ×1, repayments ×36, 9.9%, 2024-11-01, —, none → error: no credit was advanced before the settlement date
an impossible notice date advances ×1, repayments ×36, 9.9%, 2026-02-30, —, none → error: is not a real calendar date

More from the author

## The rules it applies

- **Regulation 4(1), the rebate.** The rebate is "the difference between the total amount of the repayments of credit that would fall due for payment after the settlement date if early settlement did not take place and the amount given by the following formula":

Σ A_i (1 + r)^a_i − Σ B_j (1 + r)^b_j

A_i are the advances and B_j the repayments made before the settlement date; a_i and b_j are the times from each to the settlement date; r is "the periodic rate equivalent of the APR". So the formula is the balance outstanding at the settlement date, carried forward at the APR, and that is the settlement figure; the rebate is what it saves against paying the schedule out. - **Regulation 5(1)(a), the settlement date.** 28 days after the creditor receives the debtor's notice under section 94(1) of the Consumer Credit Act 1974, or any later date the notice names. An earlier date in the notice does not shorten the 28 days. - **Regulation 6, deferment.** Where the credit is repaid over a period of more than a year after the relevant date, the creditor may defer the settlement date by one month, or elect 30 days where a month would be more or less than 30 days. `deferral` is that election; asking for it on an agreement of a year or less is an error. A month is a calendar month, clamped at the month end (31 January + 1 month is 28 February), as dates.add-months does.

## How it is computed

Time is measured in days and a year is 365 days, so each amount is carried forward by (1 + APR)^(days / 365). That fractional power is irrational; it is computed in 18-place fixed point with math.fractional-power (the daily factor (1 + APR)^(1/365) once, then a whole power of it per flow), the same floors in the same order in every language, accurate to around 10^-15 of the amount. The formula's result is rounded **down** to the minor unit, because regulation 2(1) requires a rebate "at least equal" to the calculated one, so the debtor is never asked for a fraction of a penny more.

- Repayments dated before the settlement date are treated as made, on their due dates, which is the creditor's election in regulation 4(2); arrears are not modelled, so add any missed instalments to the figure yourself. - Repayments dated on or after the settlement date are the ones settled early; their total is `remainingRepayments`. - The settlement amount never exceeds `remainingRepayments`, and the rebate is never negative.

## What a specialist should check

- **Rate and time basis.** "The periodic rate equivalent of the APR" is read as (1 + APR)^(days/365), using the APR as disclosed (rounded to one decimal place). Some creditors use monthly periods with part-months, or the unrounded APR; the answers differ by pennies. - **Leap years.** 365 days throughout, as in lending.apr. - **A repayment due on the settlement date itself** is treated as settled early (included in `remainingRepayments`, excluded from the formula). The regulations speak of repayments "before" and "after" the settlement date and are silent on the day itself. - **What is in the total charge for credit.** Regulation 3 allows some items (taxes, linked transactions, broker fees attributable to the period before settlement) to be excluded from the rebate; the flows passed in are taken as given. - **Scope.** Regulation 2(2) excludes running-account credit (credit cards, overdrafts) and some land mortgages; partial early settlement (section 94(3), regulation 4A) is not implemented. - **Relevant date.** Regulation 6 measures "more than a year" from the relevant date; this uses the first advance's date as that date.

## Before you rely on this

**Not professional advice.** This capability calculates lending 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 a consumer-credit compliance specialist 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 consumer-credit compliance specialist 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.md5,350
impl/python.py4,456
impl/rust.rs5,948
impl/typescript.ts4,612
vectors.json63,140