lending.overpayment-effect Unreviewed
What a one-off overpayment does to a loan: a shorter term at the same payment, or a lower payment over the same term.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 14 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
What a one-off lump-sum overpayment does to a repayment loan, with both of the options UK lenders offer shown side by side:
- **Reduce the term**: keep paying the same amount; the loan ends sooner. `reducedTermPayments`, `reducedTermFinalPayment`, `reducedTermInterest`, and `paymentsSaved`. - **Reduce the payment**: keep the end date; each payment is recalculated on the lower balance over the remaining term. `reducedPayment`, `reducedPaymentInterest`.
For example
overpayment_effect(£100,000.00, 5%, 300, 12, £584.59, £10,000.00, half-up)→ new balance £90,000.00, baseline payments 300, baseline interest £75,377.05, reduced term payments 247, reduced term final payment £406.29, reduced term interest £54,215.43, payme… £10,000 off a new £100,000 5% 25-year mortgageoverpayment_effect(£1,000.00, 12%, 12, 12, £88.85, £200.00, half-up)→ new balance £800.00, baseline payments 12, baseline interest £66.19, reduced term payments 10, reduced term final payment £42.99, reduced term interest £42.64, payments saved 2, r… £200 off £1,000 at 12% with a year leftoverpayment_effect(£1,000.00, 12%, 12, 12, £88.85, £200.00, down)→ new balance £800.00, baseline payments 12, baseline interest £66.19, reduced term payments 10, reduced term final payment £42.99, reduced term interest £42.64, payments saved 2, r… the recalculated payment rounded down
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 overpayment_effect(balance: Money, annual_rate_basis_points: int, remaining_term_months: int, payments_per_year: int, payment: Money, overpayment: Money, mode: RoundingMode) -> OverpaymentEffect
| balance | Money | the balance just before the overpayment, after the last regular payment |
| annual_rate_basis_points | int | nominal annual rate, 0 to 100000 |
| remaining_term_months | int | the term left, for the keep-the-term option |
| payments_per_year | int | 1 to 52 |
| payment | Money | the current regular payment |
| overpayment | Money | the lump sum, more than zero and less than the balance |
| mode | RoundingMode | rounding of the recalculated payment, as lending.loan-payment |
| returns | OverpaymentEffect |
The type it declares, generated into your project
@dataclass(frozen=True)
class OverpaymentEffect:
"""Both options side by side, with the no-overpayment baseline to compare against."""
#: balance after the overpayment
new_balance: Money
#: payments to clear the old balance at the current payment
baseline_payments: int
#: interest paid doing that
baseline_interest: Money
#: payments to clear the new balance at the current payment
reduced_term_payments: int
#: the smaller last payment of that option
reduced_term_final_payment: Money
reduced_term_interest: Money
#: baselinePayments minus reducedTermPayments
payments_saved: int
#: the new level payment over the remaining term
reduced_payment: Money
reduced_payment_interest: Money
Your code names it in one line, in the file that uses it
from fune.lending.overpayment_effect import overpayment_effect # lending.overpayment-effect@^1
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 Tuple
from .lending_amortisation_schedule import amortisation_schedule, period_interest ← from lending.amortisation-schedule ^1.0.0 · built alongside by fune
from .lending_loan_payment import payment_count ← from lending.loan-payment ^1.0.0 · built alongside by fune
from .lending_overpayment_effect_types import OverpaymentEffect
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, money ← from money.amount ^1.0.0 · built alongside by fune
def _pay_off(
balance: int, annual_rate_basis_points: int, payments_per_year: int, payment: int, term: int
) -> Tuple[int, int, int]:
"""Run a balance down at a fixed payment, with the schedule's rounding,
until it is clear or the term ends: (payments, final payment, interest).
As in lending.amortisation-schedule, the last payment of the term is
whatever clears the balance."""
owed = balance
payments = 0
interest = 0
final_payment = 0
while owed > 0:
accrued = period_interest(owed, annual_rate_basis_points, payments_per_year)
if payment <= accrued:
raise ValueError("the payment of %d does not cover the interest of %d" % (payment, accrued))
due = owed + accrued if payments + 1 == term or owed + accrued <= payment else payment
owed -= due - accrued
interest += accrued
payments += 1
final_payment = due
return payments, final_payment, interest
def overpayment_effect(
balance: Money,
annual_rate_basis_points: int,
remaining_term_months: int,
payments_per_year: int,
payment: Money,
overpayment: Money,
mode: RoundingMode,
) -> OverpaymentEffect:
"""The two things a lender offers after a lump-sum overpayment, side by
side: keep paying the same amount and finish sooner, or keep the same end
date and pay less each period. Both are built period by period with the
same rounding as lending.amortisation-schedule, and compared with carrying
on as if nothing had been overpaid.
"""
term = payment_count(annual_rate_basis_points, remaining_term_months, payments_per_year)
currency = balance.currency
for other in (payment, overpayment):
if other.currency != currency:
raise ValueError("currency mismatch: %s and %s" % (currency, other.currency))
if payment.minor <= 0:
raise ValueError("payment must be greater than zero, received %r" % (payment.minor,))
if overpayment.minor <= 0:
raise ValueError("overpayment must be greater than zero, received %r" % (overpayment.minor,))
if overpayment.minor >= balance.minor:
raise ValueError("overpayment must be less than the balance; paying it all off is an early settlement")
new_balance = balance.minor - overpayment.minor
base_n, _, base_interest = _pay_off(balance.minor, annual_rate_basis_points, payments_per_year, payment.minor, term)
short_n, short_final, short_interest = _pay_off(
new_balance, annual_rate_basis_points, payments_per_year, payment.minor, term
)
lower = amortisation_schedule(
money(new_balance, currency), annual_rate_basis_points, remaining_term_months, payments_per_year, mode
)
return OverpaymentEffect(
new_balance=money(new_balance, currency),
baseline_payments=base_n,
baseline_interest=money(base_interest, currency),
reduced_term_payments=short_n,
reduced_term_final_payment=money(short_final, currency),
reduced_term_interest=money(short_interest, currency),
payments_saved=base_n - short_n,
reduced_payment=lower.payment,
reduced_payment_interest=lower.total_interest,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 4 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.overpayment-effect
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./lending.overpayment-effect-1.0.1-python.fune, or fetch it from a terminal with fune pull lending.overpayment-effect@1.0.1:python.
The whole function, every language, is one file too: lending.overpayment-effect-1.0.1.fune, 29,666 bytes, sha256 fc3aabd047cbf3d1b5254941c6ebef4ebd6a7293999346017809571b78e00bc8. 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.overpayment-effect
after — your function gets the result and the arguments, and returns the final result.
# fune: after lending.overpayment-effect
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.amortisation-schedule in lending.overpayment-effect
# fune: replace lending.loan-payment in lending.overpayment-effect
# fune: replace math.round-div in lending.overpayment-effect
# fune: replace money.amount in lending.overpayment-effect
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.overpayment-effect --steps.
# fune: step lending.overpayment-effect 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 | |
|---|---|---|---|
| £10,000 off a new £100,000 5% 25-year mortgage | £100,000.00, 5%, 300, 12, £584.59, £10,000.00, half-up | → | new balance £90,000.00, baseline payments 300, baseline interest £75,377.05, reduced term payments 247, reduced term final payment £406.29, reduced term interest £54,215.43, payme… |
| £200 off £1,000 at 12% with a year left | £1,000.00, 12%, 12, 12, £88.85, £200.00, half-up | → | new balance £800.00, baseline payments 12, baseline interest £66.19, reduced term payments 10, reduced term final payment £42.99, reduced term interest £42.64, payments saved 2, r… |
| the recalculated payment rounded down | £1,000.00, 12%, 12, 12, £88.85, £200.00, down | → | new balance £800.00, baseline payments 12, baseline interest £66.19, reduced term payments 10, reduced term final payment £42.99, reduced term interest £42.64, payments saved 2, r… |
| interest-free: the term shortens by whole payments only | £1,200.00, 0%, 12, 12, £100.00, £250.00, half-up | → | new balance £950.00, baseline payments 12, baseline interest £0.00, reduced term payments 10, reduced term final payment £50.00, reduced term interest £0.00, payments saved 2, red… |
| a penny overpayment saves no payments | £5,000.00, 6%, 6, 12, £847.98, £0.01, half-up | → | new balance £4,999.99, baseline payments 6, baseline interest £87.87, reduced term payments 6, reduced term final payment £847.96, reduced term interest £87.87, payments saved 0, … |
| quarterly loan, half the balance overpaid | £10,000.00, 8%, 24, 4, £1,365.10, £5,000.00, half-up | → | new balance £5,000.00, baseline payments 8, baseline interest £920.80, reduced term payments 4, reduced term final payment £1,150.86, reduced term interest £246.16, payments saved… |
| a payment above the level payment clears even the baseline early | £1,000.00, 12%, 12, 12, £100.00, £100.00, half-up | → | new balance £900.00, baseline payments 11, baseline interest £58.98, reduced term payments 10, reduced term final payment £47.94, reduced term interest £47.94, payments saved 1, r… |
| mid-term: 10 years left on £60,000 at 4% | £60,000.00, 4%, 120, 12, £607.47, £5,000.00, half-up | → | new balance £55,000.00, baseline payments 120, baseline interest £12,896.51, reduced term payments 108, reduced term final payment £579.32, reduced term interest £10,578.61, payme… |
| an overpayment of the whole balance is a settlement, not an overpayment | £1,000.00, 12%, 12, 12, £88.85, £1,000.00, half-up | → | error: overpayment must be less than the balance |
| a zero overpayment is refused | £1,000.00, 12%, 12, 12, £88.85, £0.00, half-up | → | error: overpayment must be greater than zero |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| mixed currencies are refused | £1,000.00, 12%, 12, 12, £88.85, €10.00, half-up | → | error: currency mismatch |
| a payment that does not cover the interest never repays | £1,000.00, 12%, 12, 12, £10.00, £10.00, half-up | → | error: does not cover the interest |
| a zero payment is refused | £1,000.00, 12%, 12, 12, £0.00, £10.00, half-up | → | error: payment must be greater than zero |
| the remaining term must hold whole payments | £1,000.00, 12%, 13, 4, £88.85, £10.00, half-up | → | error: is not a whole number of payments |
More from the author
Both are compared with a baseline: carrying on at the current payment as if nothing had been overpaid (`baselinePayments`, `baselineInterest`). Interest saved is baseline minus option. Reducing the term always saves at least as much interest as reducing the payment, because the balance falls faster; reducing the payment gives cash-flow room instead. Which to choose is the borrower's, which is why both are returned.
## Conventions
The overpayment is applied just after a regular payment, to the balance that payment left, and takes effect from the next period. Every period is built exactly as in lending.amortisation-schedule: interest on the opening balance rounded half-up to a whole minor unit, the payment covers interest first, and the last payment of the term (or the one that would overshoot) is exactly what is owed, so no option ever ends with a stray penny outstanding.
The baseline and the reduce-the-term option pay the `payment` you pass; it need not be the level payment (a borrower who already pays extra each month is modelled by passing what they pay). The reduce-the-payment option recalculates with lending.loan-payment and your rounding `mode`.
## What it does not do
- Early repayment charges. Many fixed-rate mortgages charge 1-5% of an overpayment above an annual allowance (often 10% of the balance); take that off before calling, or compare it with the interest saved. - Overpaying the whole balance. That is an early settlement: the lender's settlement figure (for regulated consumer credit, lending.early-settlement) is the right tool, so an overpayment of the whole balance is an error. - Rate changes, daily interest or payment holidays.
## Errors
A payment that does not cover the first period's interest never repays and is refused. Currencies must match. The rate, frequency and remaining term are checked as in lending.loan-payment.
## 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
| Path | Bytes |
|---|---|
| README.md | 3,511 |
| impl/python.py | 3,510 |
| impl/rust.rs | 4,905 |
| impl/typescript.ts | 3,591 |
| vectors.json | 8,530 |