Functional Weave
Code in Rust

invest.pension-annual-allowance

UK pension annual allowance for a tax year with taper, MPAA and three-year carry-forward, and the excess charged.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

Pinned by 22 tests, run in TypeScript, Python and Rust.

Not professional advice. This capability calculates investment 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 pensions tax 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 tax professional before it is published.**

The UK pension annual allowance for a tax year, after the taper and the money purchase annual allowance (MPAA), the unused allowance carried forward from the three previous tax years, and the amount subject to the annual allowance charge. The charge itself is income tax at the member's marginal rate on that excess, which is left to an income tax calculation.

For example

  • pension_annual_allowance(2025-10-01, money purchase input £30,000.00, other input £0.00, threshold income £100,000.00, adjusted income £100,000.00, mpaa applies false, member true, previous years ×3) → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £30,000.00, basis standard, … 2025/26 inside the £60,000: nothing carried forward is touched
  • pension_annual_allowance(2025-10-01, money purchase input £20,000.00, other input £100,000.00, threshold income £100,000.00, adjusted income £180,000.00, mpaa applies false, member true, previous years ×3) → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £120,000.00, basis standard,… £120,000 uses £60,000 of carry-forward, the earliest year first
  • pension_annual_allowance(2025-10-01, money purchase input £200,000.00, other input £0.00, threshold income £150,000.00, adjusted income £250,000.00, mpaa applies false, member true, previous years ×3) → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £200,000.00, basis standard,… £200,000 exhausts all three years and leaves £10,000 excess

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 pension_annual_allowance(tax_year_date: &str, current: &PensionInputYear, previous_years: &[PensionInputYear]) -> AnnualAllowanceResult
tax_year_datedateany date in the tax year being tested
currentPensionInputYearthat tax year's pension inputs and income
previous_yearsPensionInputYear[]the tax years before it, oldest first, ending with the year just before; at least three
returnsAnnualAllowanceResult

The types it declares, generated into your project

/// One tax year's pension input amounts and the incomes the taper tests.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PensionInputYear {
    /// money purchase input tested against the MPAA: from the trigger on, or all of it
    pub money_purchase_input: Money,
    /// every other pension input amount: defined benefits, cash balance, money purchase before the trigger
    pub other_input: Money,
    pub threshold_income: Money,
    pub adjusted_income: Money,
    /// the money purchase annual allowance applied in the year
    pub mpaa_applies: bool,
    /// a member of a registered pension scheme at some time in the year
    pub member: bool,
}

// ChargeBasis is a string in Rust, one of: "standard", "alternative".
// Parameters take it as &str and results hold it as String.

/// Unused allowance from one earlier tax year.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CarryForwardYear {
    pub tax_year: String,
    /// the allowance tested: tapered, or the alternative allowance in an MPAA year
    pub allowance: Money,
    /// the input tested against it
    pub input: Money,
    /// 0 in a year without scheme membership
    pub unused: Money,
    /// taken by excesses in later earlier years, earliest first
    pub used_by_later_years: Money,
    /// taken by the year being tested
    pub used_this_year: Money,
}

/// The allowance, the carry-forward and the excess subject to the annual allowance charge.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnnualAllowanceResult {
    pub tax_year: String,
    pub standard_allowance: Money,
    /// after any taper
    pub allowance: Money,
    pub tapered: bool,
    /// null unless the MPAA applies
    pub money_purchase_allowance: Option<Money>,
    /// null unless money purchase inputs exceed the MPAA
    pub alternative_allowance: Option<Money>,
    pub total_input: Money,
    /// alternative when the MPAA computation gives the larger charge
    pub basis: String,
    /// the three tax years before, oldest first
    pub carry_forward: Vec<CarryForwardYear>,
    pub carry_forward_available: Money,
    pub carry_forward_used: Money,
    /// the amount subject to the annual allowance charge
    pub excess: Money,
}

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

fune!(invest.pension-annual-allowance@^1);  // then call pension_annual_allowance(…)
impl/rust.rs · 289 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::dates_uk_tax_year::uk_tax_year;  ← from dates.uk-tax-year ^1.0.0 · built alongside by fune
use super::invest_pension_annual_allowance_data::{
    AnnualAllowanceRule, ANNUAL_ALLOWANCE_RULES, ANNUAL_ALLOWANCE_RULES_HISTORY, ANNUAL_ALLOWANCE_RULES_HORIZON,
};
use super::money_amount::{money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

/// How one tax year comes out against its own allowance, given the carry-forward it can reach.
struct YearTest {
    label: String,
    standard: i64,
    allowance: i64,
    tapered: bool,
    mpaa: Option<i64>,
    alternative: Option<i64>,
    total: i64,
    basis: &'static str,
    carry_forward_used: i64,
    excess: i64,
    /// What this year leaves to carry forward, and what it was tested against.
    unused: i64,
    tested_allowance: i64,
    tested_input: i64,
}

struct Ledger {
    tax_year: String,
    allowance: i64,
    input: i64,
    unused: i64,
    used_by_later_years: i64,
    used_this_year: i64,
}

fn gbp(minor: i64) -> Money {
    Money {
        minor,
        currency: "GBP".to_string(),
    }
}

fn check_money(value: &Money, name: &str, label: &str) -> i64 {
    if value.currency != "GBP" {
        panic!("{} must be in GBP, received {} for {}", name, value.currency, label);
    }
    if value.minor < 0 {
        panic!(
            "{} must be a whole number of pence, not negative, received {} for {}",
            name, value.minor, label
        );
    }
    value.minor
}

fn rule_for(start_year: i64) -> (&'static AnnualAllowanceRule, String) {
    let start = format!("{:04}-04-06", start_year);
    let label = uk_tax_year(&start).label;
    for rule in ANNUAL_ALLOWANCE_RULES {
        if rule.valid_from <= start.as_str() && rule.valid_to.map(|to| start.as_str() <= to).unwrap_or(true) {
            return (rule, label);
        }
    }
    let why = match ANNUAL_ALLOWANCE_RULES_HORIZON {
        Some(h) if start.as_str() < h => format!(
            ": this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier years.",
            ANNUAL_ALLOWANCE_RULES_HISTORY, h
        ),
        _ => String::new(),
    };
    panic!("no annual allowance rules for the {} tax year{}", label, why);
}

fn test(start_year: i64, year: &PensionInputYear, carry_forward: i64) -> YearTest {
    let (rule, label) = rule_for(start_year);
    let mp = check_money(&year.money_purchase_input, "moneyPurchaseInput", &label);
    let other = check_money(&year.other_input, "otherInput", &label);
    let threshold = check_money(&year.threshold_income, "thresholdIncome", &label);
    let adjusted = check_money(&year.adjusted_income, "adjustedIncome", &label);
    let total = mp + other;
    if !year.member && total > 0 {
        panic!(
            "pension inputs in {} need membership of a registered pension scheme in that year",
            label
        );
    }

    // Taper: one pound off for every two of adjusted income over the limit,
    // rounded down to a whole pound, when threshold income is also over its limit.
    let mut allowance = rule.annual_allowance;
    let tapered = threshold > rule.threshold_income && adjusted > rule.adjusted_income;
    if tapered {
        let reduction = (adjusted - rule.adjusted_income) / 200 * 100;
        allowance = rule.minimum_tapered_allowance.max(allowance - reduction);
    }

    let default_used = carry_forward.min((total - allowance).max(0));
    let default_excess = (total - allowance - default_used).max(0);
    if year.mpaa_applies && mp > rule.money_purchase_allowance {
        // Carry-forward can top up the alternative allowance, never the MPAA.
        let alternative = (allowance - rule.money_purchase_allowance).max(0);
        let alt_used = carry_forward.min((other - alternative).max(0));
        let alt_excess = mp - rule.money_purchase_allowance + (other - alternative - alt_used).max(0);
        let use_alternative = alt_excess > default_excess;
        return YearTest {
            label,
            standard: rule.annual_allowance,
            allowance,
            tapered,
            mpaa: Some(rule.money_purchase_allowance),
            alternative: Some(alternative),
            total,
            basis: if use_alternative { "alternative" } else { "standard" },
            carry_forward_used: if use_alternative { alt_used } else { default_used },
            excess: if use_alternative { alt_excess } else { default_excess },
            unused: if year.member { (alternative - other).max(0) } else { 0 },
            tested_allowance: alternative,
            tested_input: other,
        };
    }
    YearTest {
        label,
        standard: rule.annual_allowance,
        allowance,
        tapered,
        mpaa: if year.mpaa_applies { Some(rule.money_purchase_allowance) } else { None },
        alternative: None,
        total,
        basis: "standard",
        carry_forward_used: default_used,
        excess: default_excess,
        unused: if year.member { (allowance - total).max(0) } else { 0 },
        tested_allowance: allowance,
        tested_input: total,
    }
}

fn available(entry: &Ledger) -> i64 {
    entry.unused - entry.used_by_later_years - entry.used_this_year
}

/// Take `amount` from the entries' unused allowance, earliest year first.
fn consume(entries: &mut [Ledger], amount: i64, this_year: bool) {
    let mut left = amount;
    for entry in entries.iter_mut() {
        let take = left.min(available(entry));
        if this_year {
            entry.used_this_year += take;
        } else {
            entry.used_by_later_years += take;
        }
        left -= take;
    }
}

/// The annual allowance for a UK tax year, and how much of the year's pension
/// input amounts is subject to the annual allowance charge.
///
/// The earlier years are replayed oldest first, because an excess in one of
/// them uses up unused allowance from the years before it (earliest first),
/// and that is allowance the year being tested can no longer carry forward.
/// A year's own allowance is used before any carried forward; carry-forward
/// is then taken from the earliest of the three previous years first.
///
/// # Panics
/// Panics on fewer than three previous years, a tax year the rules do not
/// cover, amounts not in GBP or negative, or inputs in a year without
/// scheme membership.
pub fn pension_annual_allowance(
    tax_year_date: &str,
    current: &PensionInputYear,
    previous_years: &[PensionInputYear],
) -> AnnualAllowanceResult {
    let year = uk_tax_year(tax_year_date);
    let start_year: i64 = year.start[..4].parse().unwrap();
    if previous_years.len() < 3 {
        panic!(
            "previousYears must hold at least the three tax years before this one, oldest first, received {}",
            previous_years.len()
        );
    }
    let count = previous_years.len() as i64;
    let mut ledger: Vec<Ledger> = Vec::new();
    for (i, previous) in previous_years.iter().enumerate() {
        let from = i.saturating_sub(3);
        let reach: i64 = ledger[from..i].iter().map(available).sum();
        let result = test(start_year - count + i as i64, previous, reach);
        consume(&mut ledger[from..i], result.carry_forward_used, false);
        ledger.push(Ledger {
            tax_year: result.label.clone(),
            allowance: result.tested_allowance,
            input: result.tested_input,
            unused: result.unused,
            used_by_later_years: 0,
            used_this_year: 0,
        });
    }
    let from = ledger.len() - 3;
    let carry_forward_available: i64 = ledger[from..].iter().map(available).sum();
    let result = test(start_year, current, carry_forward_available);
    consume(&mut ledger[from..], result.carry_forward_used, true);

    let carry_forward: Vec<CarryForwardYear> = ledger[from..]
        .iter()
        .map(|e| CarryForwardYear {
            tax_year: e.tax_year.clone(),
            allowance: gbp(e.allowance),
            input: gbp(e.input),
            unused: gbp(e.unused),
            used_by_later_years: gbp(e.used_by_later_years),
            used_this_year: gbp(e.used_this_year),
        })
        .collect();
    AnnualAllowanceResult {
        tax_year: result.label.clone(),
        standard_allowance: gbp(result.standard),
        allowance: gbp(result.allowance),
        tapered: result.tapered,
        money_purchase_allowance: result.mpaa.map(gbp),
        alternative_allowance: result.alternative.map(gbp),
        total_input: gbp(result.total),
        basis: result.basis.to_string(),
        carry_forward,
        carry_forward_available: gbp(carry_forward_available),
        carry_forward_used: gbp(result.carry_forward_used),
        excess: gbp(result.excess),
    }
}

fn optional_money(value: &Option<Money>) -> Value {
    match value {
        Some(m) => money_to_value(m),
        None => Value::Null,
    }
}

pub fn carry_forward_year_to_value(y: &CarryForwardYear) -> Value {
    Value::obj(vec![
        ("taxYear", Value::str(&y.tax_year)),
        ("allowance", money_to_value(&y.allowance)),
        ("input", money_to_value(&y.input)),
        ("unused", money_to_value(&y.unused)),
        ("usedByLaterYears", money_to_value(&y.used_by_later_years)),
        ("usedThisYear", money_to_value(&y.used_this_year)),
    ])
}

pub fn annual_allowance_result_to_value(r: &AnnualAllowanceResult) -> Value {
    Value::obj(vec![
        ("taxYear", Value::str(&r.tax_year)),
        ("standardAllowance", money_to_value(&r.standard_allowance)),
        ("allowance", money_to_value(&r.allowance)),
        ("tapered", Value::Bool(r.tapered)),
        ("moneyPurchaseAllowance", optional_money(&r.money_purchase_allowance)),
        ("alternativeAllowance", optional_money(&r.alternative_allowance)),
        ("totalInput", money_to_value(&r.total_input)),
        ("basis", Value::str(&r.basis)),
        ("carryForward", Value::Arr(r.carry_forward.iter().map(carry_forward_year_to_value).collect())),
        ("carryForwardAvailable", money_to_value(&r.carry_forward_available)),
        ("carryForwardUsed", money_to_value(&r.carry_forward_used)),
        ("excess", money_to_value(&r.excess)),
    ])
}

fn whole_money(v: &Value, name: &str) -> Money {
    // Refuse a fractional number of pence with the wording TypeScript and Python use.
    if let Value::Float(f) = v.get("minor") {
        if f.fract() != 0.0 {
            panic!("{} must be a whole number of pence, not negative, received {}", name, f);
        }
    }
    money_from_value(v)
}

pub fn pension_input_year_from_value(v: &Value) -> PensionInputYear {
    PensionInputYear {
        money_purchase_input: whole_money(v.get("moneyPurchaseInput"), "moneyPurchaseInput"),
        other_input: whole_money(v.get("otherInput"), "otherInput"),
        threshold_income: whole_money(v.get("thresholdIncome"), "thresholdIncome"),
        adjusted_income: whole_money(v.get("adjustedIncome"), "adjustedIncome"),
        mpaa_applies: v.get("mpaaApplies").as_bool(),
        member: v.get("member").as_bool(),
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    let current = pension_input_year_from_value(&args[1]);
    let previous: Vec<PensionInputYear> = args[2].as_arr().iter().map(pension_input_year_from_value).collect();
    annual_allowance_result_to_value(&pension_annual_allowance(args[0].as_str(), &current, &previous))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 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 invest.pension-annual-allowance
Download for Rust invest.pension-annual-allowance-1.0.0-rust.fune · 101,162 bytes sha256 112e5e827ded4451e2ba5d1b0d089b7f2501f441b4d0782c47c039b1e50dd827

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./invest.pension-annual-allowance-1.0.0-rust.fune, or fetch it from a terminal with fune pull invest.pension-annual-allowance@1.0.0:rust.

The whole function, every language, is one file too: invest.pension-annual-allowance-1.0.0.fune, 117,117 bytes, sha256 87d133c9704e1d8626a6ef6ba2c0cb8f64821c84cba418375fd12b70e0d5cded. 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 invest.pension-annual-allowance

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

// fune: after invest.pension-annual-allowance

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.uk-tax-year in invest.pension-annual-allowance
// fune: replace money.amount in invest.pension-annual-allowance

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 invest.pension-annual-allowance --steps.

// fune: step invest.pension-annual-allowance 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
2025/26 inside the £60,000: nothing carried forward is touched 2025-10-01, money purchase input £30,000.00, other input £0.00, threshold income £100,000.00, adjusted income £100,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £30,000.00, basis standard, …
£120,000 uses £60,000 of carry-forward, the earliest year first 2025-10-01, money purchase input £20,000.00, other input £100,000.00, threshold income £100,000.00, adjusted income £180,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £120,000.00, basis standard,…
£200,000 exhausts all three years and leaves £10,000 excess 2025-10-01, money purchase input £200,000.00, other input £0.00, threshold income £150,000.00, adjusted income £250,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £200,000.00, basis standard,…
taper: adjusted income £300,000 takes £20,000 off 2025-10-01, money purchase input £50,000.00, other input £0.00, threshold income £250,000.00, adjusted income £300,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £40,000.00, tapered true, money purchase allowance —, alternative allowance —, total input £50,000.00, basis standard, c…
taper stops at the £10,000 minimum 2025-10-01, money purchase input £10,000.00, other input £0.00, threshold income £300,000.00, adjusted income £400,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £10,000.00, tapered true, money purchase allowance —, alternative allowance —, total input £10,000.00, basis standard, c…
threshold income of exactly £200,000 is not over the limit: no taper 2025-10-01, money purchase input £60,000.00, other input £0.00, threshold income £200,000.00, adjusted income £300,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £60,000.00, basis standard, …
taper reduction is rounded down to a whole pound: £3 over gives £1 off 2025-10-01, money purchase input £0.00, other input £0.00, threshold income £210,000.00, adjusted income £260,003.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £59,999.00, tapered true, money purchase allowance —, alternative allowance —, total input £0.00, basis standard, carry …
2021/22 taper floor was £4,000 2021-10-01, money purchase input £4,000.00, other input £0.00, threshold income £300,000.00, adjusted income £400,000.00, mpaa applies false, member true, previous years ×3 → tax year 2021/22, standard allowance £40,000.00, allowance £4,000.00, tapered true, money purchase allowance —, alternative allowance —, total input £4,000.00, basis standard, car…
a tapered earlier year carries forward against its own reduced allowance 2025-10-01, money purchase input £90,000.00, other input £0.00, threshold income £150,000.00, adjusted income £150,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £90,000.00, basis standard, …
an excess in an earlier year has already used the oldest unused allowance 2025-10-01, money purchase input £90,000.00, other input £0.00, threshold income £150,000.00, adjusted income £150,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £90,000.00, basis standard, …
Show the other 12 tests
CaseArgumentsExpected
with five earlier years given, that excess takes the oldest year's allowance first 2025-10-01, money purchase input £90,000.00, other input £0.00, threshold income £150,000.00, adjusted income £150,000.00, mpaa applies false, member true, previous years ×5 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £90,000.00, basis standard, …
no carry-forward from a year without scheme membership 2025-10-01, money purchase input £100,000.00, other input £0.00, threshold income £150,000.00, adjusted income £150,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £100,000.00, basis standard,…
MPAA: £15,000 of money purchase input is £5,000 over, and carry-forward cannot cover it 2025-10-01, money purchase input £15,000.00, other input £0.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies true, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance £10,000.00, alternative allowance £50,000.00, total input £15,000.00…
MPAA applies but is not exceeded: the ordinary test 2025-10-01, money purchase input £8,000.00, other input £50,000.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies true, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance £10,000.00, alternative allowance —, total input £58,000.00, basis s…
MPAA exceeded and defined benefits over the alternative allowance: carry-forward tops up the alternative only 2025-10-01, money purchase input £20,000.00, other input £70,000.00, threshold income £100,000.00, adjusted income £190,000.00, mpaa applies true, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance £10,000.00, alternative allowance £50,000.00, total input £90,000.00…
an earlier MPAA year carries forward the alternative allowance less other inputs 2025-10-01, money purchase input £90,000.00, other input £0.00, threshold income £150,000.00, adjusted income £150,000.00, mpaa applies false, member true, previous years ×3 → tax year 2025/26, standard allowance £60,000.00, allowance £60,000.00, tapered false, money purchase allowance —, alternative allowance —, total input £90,000.00, basis standard, …
fewer than three earlier years is refused 2025-10-01, money purchase input £0.00, other input £0.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies false, member true, previous years ×2 → error: previousYears must hold at least the three tax years before this one
years before 2016/17 are not covered 2018-10-01, money purchase input £0.00, other input £0.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies false, member true, previous years ×3 → error: no annual allowance rules for the 2015/16 tax year
inputs in a year without membership are refused 2025-10-01, money purchase input £1,000.00, other input £0.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies false, member false, previous years ×3 → error: pension inputs in 2025/26 need membership of a registered pension scheme
amounts must be in sterling 2025-10-01, money purchase input £0.00, other input €1.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies false, member true, previous years ×3 → error: otherInput must be in GBP
negative inputs are refused 2025-10-01, money purchase input -£0.01, other input £0.00, threshold income £50,000.00, adjusted income £50,000.00, mpaa applies false, member true, previous years ×3 → error: moneyPurchaseInput must be a whole number of pence, not negative
fractional pence are refused 2025-10-01, money purchase input £0.00, other input £0.00, threshold income £50,000.00, adjusted income £100..5, mpaa applies false, member true, previous years ×3 → error: adjustedIncome must be a whole number of pence, not negative

More from the author

## How it is worked out

- **Allowance.** The standard annual allowance for the year. If threshold income is over its limit *and* adjusted income is over its limit, it is reduced by £1 for every £2 of adjusted income over the limit, the reduction rounded down to a whole pound, but not below the minimum tapered allowance (PTM057100). Both tests are "more than", so exactly £200,000 of threshold income is not tapered. - **Carry-forward.** Unused allowance of each of the three previous tax years can be carried forward, if the member belonged to a registered pension scheme at some time in that year. The current year's allowance is used first, then the earliest previous year's unused allowance (PTM055100). - **Earlier excesses.** An excess in one of the previous years will already have used unused allowance from the years before it, earliest first (PTM055200). So the previous years are replayed oldest first. Pass more than three years, oldest first, to replay further back; the oldest year you pass is assumed to have had nothing of its own to carry forward. - **MPAA.** When the MPAA applies and money purchase input (from the trigger on) is over it, the rest of the inputs are tested against the alternative annual allowance, the (tapered) allowance less the MPAA. Carry-forward can top up the alternative allowance but never the MPAA. The amount charged is the greater of the alternative chargeable amount (money purchase over the MPAA plus other inputs over the alternative allowance) and the default one (all inputs over the allowance) (PTM056510, PTM056540). A year like that carries forward the alternative allowance less its other inputs (PTM055100).

`moneyPurchaseInput` is the money purchase input tested against the MPAA: all of it in a year after the trigger, only the part after the trigger event in the year it happens. Everything else (defined benefit and cash balance inputs, and money purchase input before the trigger) is `otherInput`. When the MPAA does not apply the split does not matter.

## Rules data

`data/annual-allowance.json`, from 2016/17 (the first year of the taper), so the year tested must be 2019/20 or later to have three earlier years.

| Tax years | Allowance | Taper: threshold / adjusted income | Minimum | MPAA | |---|---|---|---|---| | 2016/17 | £40,000 | £110,000 / £150,000 | £10,000 | £10,000 | | 2017/18 to 2019/20 | £40,000 | £110,000 / £150,000 | £10,000 | £4,000 | | 2020/21 to 2022/23 | £40,000 | £200,000 / £240,000 | £4,000 | £4,000 | | 2023/24 on | £60,000 | £200,000 / £260,000 | £10,000 | £10,000 |

## Out of scope

Working out pension input amounts (defined benefit growth, the opening value revaluation, deferred members), threshold and adjusted income themselves, the 2015/16 transitional rules, scheme pays, the lifetime allowance, and individuals who are in more than one MPAA-related circumstance in a year beyond the split described above.

## Sources

Read on 2026-09-23:

- GOV.UK, "Tax on your private pension contributions: Annual allowance" (£60,000; taper over £200,000 threshold and £260,000 adjusted income; carry-forward from the previous 3 tax years): https://www.gov.uk/tax-on-your-private-pension/annual-allowance - HMRC Pensions Tax Manual PTM051100, "Annual allowance: essential principles" (£40,000 from 2014/15, £60,000 from 2023/24): https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm051100 - PTM057100, "Tapered annual allowance" (thresholds and minimums by year; £1 for every £2, rounded down to the nearest £1): https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm057100 - PTM056540, "Money purchase annual allowance: trigger event occurs during tax year 2016-17 or a later tax year" (MPAA £10,000 2016/17, £4,000 2017/18 to 2022/23, £10,000 from 2023/24; alternative and default chargeable amounts): https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm056540 - PTM056510, "Money purchase annual allowance" (carry-forward is added to the alternative annual allowance; taper reduces it): https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm056510 - PTM055100, "Carry forward" (order of use, membership, tapered and MPAA years): https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm055100 - PTM055200, "Carry forward: calculating unused annual allowance" (earlier excesses use earlier unused allowance first): https://www.gov.uk/hmrc-internal-manuals/pensions-tax-manual/ptm055200

## What a reviewer should check

- How much carry-forward an MPAA year consumes when the default chargeable amount is chosen (here: whatever the default computation used). - The tie rule: when the alternative and default chargeable amounts are equal, the default (standard) basis is reported. - Assuming nothing carried into the oldest year passed.

Files

PathBytes
README.md5,359
data/annual-allowance.json1,410
impl/python.py7,644
impl/rust.rs11,382
impl/typescript.ts7,746
vectors.json65,881