Functional Weave
Code in Rust

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 mortgage
  • overpayment_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 left
  • overpayment_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.

pub fn overpayment_effect(balance: &Money, annual_rate_basis_points: i64, remaining_term_months: i64, payments_per_year: i64, payment: &Money, overpayment: &Money, mode: &str) -> OverpaymentEffect
balanceMoneythe balance just before the overpayment, after the last regular payment
annual_rate_basis_pointsintnominal annual rate, 0 to 100000
remaining_term_monthsintthe term left, for the keep-the-term option
payments_per_yearint1 to 52
paymentMoneythe current regular payment
overpaymentMoneythe lump sum, more than zero and less than the balance
modeRoundingModerounding of the recalculated payment, as lending.loan-payment
returnsOverpaymentEffect

The type it declares, generated into your project

/// Both options side by side, with the no-overpayment baseline to compare against.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OverpaymentEffect {
    /// balance after the overpayment
    pub new_balance: Money,
    /// payments to clear the old balance at the current payment
    pub baseline_payments: i64,
    /// interest paid doing that
    pub baseline_interest: Money,
    /// payments to clear the new balance at the current payment
    pub reduced_term_payments: i64,
    /// the smaller last payment of that option
    pub reduced_term_final_payment: Money,
    pub reduced_term_interest: Money,
    /// baselinePayments minus reducedTermPayments
    pub payments_saved: i64,
    /// the new level payment over the remaining term
    pub reduced_payment: Money,
    pub reduced_payment_interest: Money,
}

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

fune!(lending.overpayment-effect@^1);  // then call overpayment_effect(…)
impl/rust.rs · 122 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.

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_amortisation_schedule::{amortisation_schedule, period_interest};  ← from lending.amortisation-schedule ^1.0.0 · built alongside by fune
use super::lending_loan_payment::payment_count;  ← from lending.loan-payment ^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

/// 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.
fn pay_off(
    balance: i64,
    annual_rate_basis_points: i64,
    payments_per_year: i64,
    payment: i64,
    term: i64,
) -> (i64, i64, i64) {
    let mut owed = balance;
    let mut payments = 0i64;
    let mut interest = 0i64;
    let mut final_payment = 0i64;
    while owed > 0 {
        let accrued = period_interest(owed, annual_rate_basis_points, payments_per_year);
        if payment <= accrued {
            panic!("the payment of {} does not cover the interest of {}", payment, accrued);
        }
        let due = if payments + 1 == term || owed + accrued <= payment {
            owed + accrued
        } else {
            payment
        };
        owed -= due - accrued;
        interest += accrued;
        payments += 1;
        final_payment = due;
    }
    (payments, final_payment, interest)
}

/// 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.
///
/// # Panics
/// Panics on mismatched currencies, an overpayment of zero or of the whole
/// balance, a payment that does not cover the interest, or the arguments
/// lending.loan-payment refuses.
pub fn overpayment_effect(
    balance: &Money,
    annual_rate_basis_points: i64,
    remaining_term_months: i64,
    payments_per_year: i64,
    payment: &Money,
    overpayment: &Money,
    mode: &str,
) -> OverpaymentEffect {
    let term = payment_count(annual_rate_basis_points, remaining_term_months, payments_per_year);
    let currency = balance.currency.as_str();
    for other in [payment, overpayment] {
        if other.currency != currency {
            panic!("currency mismatch: {} and {}", currency, other.currency);
        }
    }
    if payment.minor <= 0 {
        panic!("payment must be greater than zero, received {}", payment.minor);
    }
    if overpayment.minor <= 0 {
        panic!("overpayment must be greater than zero, received {}", overpayment.minor);
    }
    if overpayment.minor >= balance.minor {
        panic!("overpayment must be less than the balance; paying it all off is an early settlement");
    }
    let new_balance = balance.minor - overpayment.minor;
    let (base_n, _, base_interest) = pay_off(balance.minor, annual_rate_basis_points, payments_per_year, payment.minor, term);
    let (short_n, short_final, short_interest) =
        pay_off(new_balance, annual_rate_basis_points, payments_per_year, payment.minor, term);
    let lower = amortisation_schedule(
        &money(new_balance, currency),
        annual_rate_basis_points,
        remaining_term_months,
        payments_per_year,
        mode,
    );
    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,
    }
}

pub fn overpayment_effect_to_value(effect: &OverpaymentEffect) -> Value {
    Value::obj(vec![
        ("newBalance", money_to_value(&effect.new_balance)),
        ("baselinePayments", Value::Int(effect.baseline_payments)),
        ("baselineInterest", money_to_value(&effect.baseline_interest)),
        ("reducedTermPayments", Value::Int(effect.reduced_term_payments)),
        ("reducedTermFinalPayment", money_to_value(&effect.reduced_term_final_payment)),
        ("reducedTermInterest", money_to_value(&effect.reduced_term_interest)),
        ("paymentsSaved", Value::Int(effect.payments_saved)),
        ("reducedPayment", money_to_value(&effect.reduced_payment)),
        ("reducedPaymentInterest", money_to_value(&effect.reduced_payment_interest)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    overpayment_effect_to_value(&overpayment_effect(
        &money_from_value(&args[0]),
        args[1].as_i64(),
        args[2].as_i64(),
        args[3].as_i64(),
        &money_from_value(&args[4]),
        &money_from_value(&args[5]),
        args[6].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 4 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 lending.overpayment-effect
Download for Rust lending.overpayment-effect-1.0.1-rust.fune · 22,336 bytes sha256 ebeba177f35433f2cc3ff83d8f93ba3b9d66642231e269dd13b0749cbf31fad1

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./lending.overpayment-effect-1.0.1-rust.fune, or fetch it from a terminal with fune pull lending.overpayment-effect@1.0.1:rust.

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.

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

PathBytes
README.md3,511
impl/python.py3,510
impl/rust.rs4,905
impl/typescript.ts3,591
vectors.json8,530