Functional Weave
Code in Rust

charity.gift-aid-eligibility

Whether a UK donation qualifies for Gift Aid: declaration, enough tax paid, and the donor benefit limits.

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

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

Not professional advice. This capability calculates tax figures from published rules. It is a software component for developers, not tax 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 charity tax specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Whether one donation qualifies for Gift Aid, and every reason it does not. It checks the three things a charity can check at the point of a gift:

1. **A Gift Aid declaration** covers the gift (ITA 2007 s416(1)(b)). 2. **The donor pays enough tax.** A donor who pays less income tax and capital gains tax in the year than their Gift Aid gifts reclaim is liable to pay the difference (s424). The charity cannot work this out, so it is a flag the caller supplies, usually from what the donor confirmed on the declaration. 3. **The donor benefit limits** (s418): the benefits the donor, or a person connected with them, receives because of the gift.

For example

  • gift_aid_eligibility(£100.00, £25.00, £0.00, true, true, 2026-05-01) → eligible true, reasons , benefit limit £25.00, annual benefit limit £2,500.00 £100 with a £25 benefit is exactly at 25% and qualifies
  • gift_aid_eligibility(£100.00, £25.01, £0.00, true, true, 2026-05-01) → eligible false, reasons benefit-over-limit, benefit limit £25.00, annual benefit limit £2,500.00 £100 with a £25.01 benefit exceeds 25%
  • gift_aid_eligibility(£1.01, £0.25, £0.00, true, true, 2026-05-01) → eligible true, reasons , benefit limit £0.25, annual benefit limit £2,500.00 £1.01: a 25p benefit is under the unrounded limit of 25.25p

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 gift_aid_eligibility(donation: &Money, benefit_value: &Money, prior_benefits_this_year: &Money, has_declaration: bool, donor_pays_enough_tax: bool, on_date: &str) -> GiftAidEligibility
donationMoneythe gift, in GBP
benefit_valueMoneyvalue of benefits the donor or a connected person gets because of this gift; 0 if none
prior_benefits_this_yearMoneybenefits from earlier qualifying gifts by this donor to this charity in the same tax year
has_declarationboola Gift Aid declaration covers this gift
donor_pays_enough_taxboolthe donor pays at least as much income or capital gains tax as all their Gift Aid gifts reclaim
on_datedatethe date of the gift; decides which limits apply
returnsGiftAidEligibility

The type it declares, generated into your project

/// Whether the gift qualifies, every reason it does not, and the limits it was tested against.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GiftAidEligibility {
    pub eligible: bool,
    /// in this order, when they apply: no-declaration, insufficient-tax, benefit-over-limit, annual-benefits-over-limit
    pub reasons: Vec<String>,
    /// most this gift's benefits may be worth, rounded down to the penny
    pub benefit_limit: Money,
    /// most all benefits from this donor's gifts to the charity in the tax year may be worth
    pub annual_benefit_limit: Money,
}

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

fune!(charity.gift-aid-eligibility@^1);  // then call gift_aid_eligibility(…)
impl/rust.rs · 119 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::charity_gift_aid_eligibility_data::{BenefitLimit, BENEFIT_LIMITS, BENEFIT_LIMITS_HISTORY};  ← this capability’s own data, compiled from data/benefit-limits.json into the same file by fune build
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn is_iso_date(s: &str) -> bool {
    let b = s.as_bytes();
    b.len() == 10
        && b.iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() })
}

fn limits_on(on_date: &str) -> &'static BenefitLimit {
    for row in BENEFIT_LIMITS {
        if row.valid_from <= on_date && row.valid_to.map_or(true, |to| on_date <= to) {
            return row;
        }
    }
    if BENEFIT_LIMITS_HISTORY != "full" {
        if let Some(earliest) = BENEFIT_LIMITS.iter().map(|r| r.valid_from).min() {
            if on_date < earliest {
                panic!(
                    "no Gift Aid benefit limits on {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full for earlier tax years.",
                    on_date, BENEFIT_LIMITS_HISTORY, earliest
                );
            }
        }
    }
    panic!("no Gift Aid benefit limits on {}", on_date);
}

fn check_gbp(name: &str, amount: &Money) {
    if amount.currency != "GBP" {
        panic!("Gift Aid amounts must be in GBP, received {} for {}", amount.currency, name);
    }
}

/// Whether a donation qualifies for Gift Aid (ITA 2007 ss416-418). The benefit
/// test is done on the exact limit, scaled by 10000, so 25% or 5% of an odd
/// number of pence is never rounded before the comparison.
///
/// # Panics
/// Panics on a non-GBP amount, a donation that is not positive, a negative
/// benefit, a malformed date, or a date with no limits on file.
pub fn gift_aid_eligibility(
    donation: &Money,
    benefit_value: &Money,
    prior_benefits_this_year: &Money,
    has_declaration: bool,
    donor_pays_enough_tax: bool,
    on_date: &str,
) -> GiftAidEligibility {
    check_gbp("donation", donation);
    check_gbp("benefitValue", benefit_value);
    check_gbp("priorBenefitsThisYear", prior_benefits_this_year);
    if donation.minor <= 0 {
        panic!("donation must be positive, received {}", donation.minor);
    }
    if benefit_value.minor < 0 {
        panic!("benefitValue must not be negative, received {}", benefit_value.minor);
    }
    if prior_benefits_this_year.minor < 0 {
        panic!("priorBenefitsThisYear must not be negative, received {}", prior_benefits_this_year.minor);
    }
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    let rule = limits_on(on_date);

    // The variable limit, in pence x 10000.
    let scaled_limit: i64 = if donation.minor <= rule.small_gift_up_to {
        donation.minor * rule.small_gift_basis_points
    } else {
        match (rule.middle_gift_up_to, rule.middle_gift_fixed) {
            (Some(up_to), Some(fixed)) if donation.minor <= up_to => fixed * 10000,
            _ => rule.excess_fixed * 10000 + (donation.minor - rule.excess_from) * rule.excess_basis_points,
        }
    };

    let mut reasons: Vec<String> = Vec::new();
    if !has_declaration {
        reasons.push("no-declaration".to_string());
    }
    if !donor_pays_enough_tax {
        reasons.push("insufficient-tax".to_string());
    }
    // "exceeds" and "is more than": a benefit exactly at a limit is allowed.
    if benefit_value.minor * 10000 > scaled_limit {
        reasons.push("benefit-over-limit".to_string());
    }
    if benefit_value.minor + prior_benefits_this_year.minor > rule.annual_limit {
        reasons.push("annual-benefits-over-limit".to_string());
    }

    GiftAidEligibility {
        eligible: reasons.is_empty(),
        reasons,
        benefit_limit: money(scaled_limit.div_euclid(10000), "GBP"),
        annual_benefit_limit: money(rule.annual_limit, "GBP"),
    }
}

pub fn gift_aid_eligibility_to_value(e: &GiftAidEligibility) -> Value {
    Value::obj(vec![
        ("eligible", Value::Bool(e.eligible)),
        ("reasons", Value::Arr(e.reasons.iter().map(|r| Value::str(r)).collect())),
        ("benefitLimit", money_to_value(&e.benefit_limit)),
        ("annualBenefitLimit", money_to_value(&e.annual_benefit_limit)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    gift_aid_eligibility_to_value(&gift_aid_eligibility(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        &money_from_value(&args[2]),
        args[3].as_bool(),
        args[4].as_bool(),
        args[5].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 1 dependency, 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 charity.gift-aid-eligibility
Download for Rust charity.gift-aid-eligibility-1.0.2-rust.fune · 23,216 bytes sha256 a01e06a0f0f0c55dc5406d2727c7a4e51ec32f2686000521ed681c0fcb3ba73d

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./charity.gift-aid-eligibility-1.0.2-rust.fune, or fetch it from a terminal with fune pull charity.gift-aid-eligibility@1.0.2:rust.

The whole function, every language, is one file too: charity.gift-aid-eligibility-1.0.2.fune, 30,575 bytes, sha256 064eea4e8c0671a71b467d4fbea583f7c475ff0a1e460244fcb03e1823366dbe. 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 charity.gift-aid-eligibility

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

// fune: after charity.gift-aid-eligibility

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 money.amount in charity.gift-aid-eligibility

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 charity.gift-aid-eligibility --steps.

// fune: step charity.gift-aid-eligibility 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
£100 with a £25 benefit is exactly at 25% and qualifies £100.00, £25.00, £0.00, true, true, 2026-05-01 → eligible true, reasons , benefit limit £25.00, annual benefit limit £2,500.00
£100 with a £25.01 benefit exceeds 25% £100.00, £25.01, £0.00, true, true, 2026-05-01 → eligible false, reasons benefit-over-limit, benefit limit £25.00, annual benefit limit £2,500.00
£1.01: a 25p benefit is under the unrounded limit of 25.25p £1.01, £0.25, £0.00, true, true, 2026-05-01 → eligible true, reasons , benefit limit £0.25, annual benefit limit £2,500.00
£1.01: a 26p benefit is over 25.25p £1.01, £0.26, £0.00, true, true, 2026-05-01 → eligible false, reasons benefit-over-limit, benefit limit £0.25, annual benefit limit £2,500.00
£2,000 since 2019: £25 plus 5% of £1,900 is £120, and £120 qualifies £2,000.00, £120.00, £0.00, true, true, 2026-05-01 → eligible true, reasons , benefit limit £120.00, annual benefit limit £2,500.00
£2,000 since 2019: £120.01 is over the limit £2,000.00, £120.01, £0.00, true, true, 2026-05-01 → eligible false, reasons benefit-over-limit, benefit limit £120.00, annual benefit limit £2,500.00
£50,000: the per-gift limit is £2,520 but a £2,500.01 benefit breaks the £2,500 annual cap £50,000.00, £2,500.01, £0.00, true, true, 2026-05-01 → eligible false, reasons annual-benefits-over-limit, benefit limit £2,520.00, annual benefit limit £2,500.00
£50,000 with a £2,500 benefit is exactly at the annual cap and qualifies £50,000.00, £2,500.00, £0.00, true, true, 2026-05-01 → eligible true, reasons , benefit limit £2,520.00, annual benefit limit £2,500.00
earlier benefits this tax year count towards the £2,500 £2,000.00, £100.01, £2,400.00, true, true, 2026-05-01 → eligible false, reasons annual-benefits-over-limit, benefit limit £120.00, annual benefit limit £2,500.00
no declaration and not enough tax: both reasons, in order £10.00, £0.00, £0.00, false, false, 2026-05-01 → eligible false, reasons no-declaration, insufficient-tax, benefit limit £2.50, annual benefit limit £2,500.00
Show the other 15 tests
CaseArgumentsExpected
every reason at once £100.00, £2,500.01, £0.00, false, false, 2026-05-01 → eligible false, reasons no-declaration, insufficient-tax, benefit-over-limit, annual-benefits-over-limit, benefit limit £25.00, annual benefit limit £2,500.00
before 6 April 2019 a £500 gift had a flat £25 limit, so a £40 benefit failed £500.00, £40.00, £0.00, true, true, 2019-04-05 → eligible false, reasons benefit-over-limit, benefit limit £25.00, annual benefit limit £2,500.00
from 6 April 2019 the same £500 gift allows £25 plus 5% of £400, £45 £500.00, £40.00, £0.00, true, true, 2019-04-06 → eligible true, reasons , benefit limit £45.00, annual benefit limit £2,500.00
before 2019 a gift over £1,000 allowed 5% of the whole gift: £100 on £2,000 £2,000.00, £100.00, £0.00, true, true, 2018-06-01 → eligible true, reasons , benefit limit £100.00, annual benefit limit £2,500.00
before 2019 exactly £1,000 was still in the £25 band £1,000.00, £25.01, £0.00, true, true, 2018-06-01 → eligible false, reasons benefit-over-limit, benefit limit £25.00, annual benefit limit £2,500.00
2010-11: the annual limit was £500 £2,000.00, £50.01, £450.00, true, true, 2011-04-05 → eligible false, reasons annual-benefits-over-limit, benefit limit £100.00, annual benefit limit £500.00
6 April 2011: the annual limit became £2,500 £2,000.00, £50.01, £450.00, true, true, 2011-04-06 → eligible true, reasons , benefit limit £100.00, annual benefit limit £2,500.00
a zero donation is an error £0.00, £0.00, £0.00, true, true, 2026-05-01 → error: donation must be positive
a euro donation is an error €10.00, £0.00, £0.00, true, true, 2026-05-01 → error: Gift Aid amounts must be in GBP, received EUR for donation
a negative benefit is an error £10.00, -£0.01, £0.00, true, true, 2026-05-01 → error: benefitValue must not be negative
negative prior benefits are an error £10.00, £0.00, -£0.01, true, true, 2026-05-01 → error: priorBenefitsThisYear must not be negative
a date before the rules on file is an error £10.00, £0.00, £0.00, true, true, 2007-04-05 → error: no Gift Aid benefit limits on 2007-04-05
a malformed date is an error £10.00, £0.00, £0.00, true, true, 2026-5-1 → error: onDate must be an ISO date
a trailing newline after onDate is not an ISO date £10.00, £0.00, £0.00, true, true, 2026-09-16 → error: onDate must be an ISO date
Arabic-Indic digits in onDate are not an ISO date £10.00, £0.00, £0.00, true, true, ٢٠٢٦-09-16 → error: onDate must be an ISO date

More from the author

The benefit limits, for gifts on or after 6 April 2019:

| gift | the benefits may be worth up to | |---|---| | £100 or less | 25% of the gift | | more than £100 | £25 plus 5% of the amount over £100 | | all gifts to this charity in the tax year | £2,500 in total |

Earlier rules are dated data: from 6 April 2007, 25% up to £100, a flat £25 on gifts up to £1,000 and 5% of the whole gift above that, with a £500 annual total, which became £2,500 from 6 April 2011. A £500 gift in March 2019 could carry a £25 benefit; the same gift in April 2019 can carry £45.

## Decisions

- **The comparison is exact.** The statute says a benefit breaches the limit if it "exceeds" it, and the annual total if it "is more than" £2,500, so a benefit exactly at a limit qualifies. The limit is compared unrounded: 25% of £1.01 is 25.25p, so a 25p benefit passes and 26p fails. `benefitLimit` is reported rounded down to the penny, for display. - **Every reason is returned**, in a fixed order (`no-declaration`, `insufficient-tax`, `benefit-over-limit`, `annual-benefits-over-limit`), so a donor record can be fixed in one pass. - `priorBenefitsThisYear` is the benefits from this donor's earlier *qualifying* gifts to this charity in the same tax year (s418(4)); the caller keeps that running total. - Out of scope: valuing a benefit, the special cases of ss419-421 (right of admission to heritage property, publications, and so on), and the other qualifying conditions of s416 (a payment of money, not repayable, not payroll giving, not deductible from income).

## Sources

- Income Tax Act 2007 ss416-418 and s424, https://www.legislation.gov.uk/ukpga/2007/3/part/8/chapter/2 - Finance Act 2007 s60 (5% above £1,000 and £500 annual limit, gifts from 6 April 2007), https://www.legislation.gov.uk/ukpga/2007/11/section/60 - Finance Act 2011 s41 (annual limit £2,500, gifts from 6 April 2011), https://www.legislation.gov.uk/ukpga/2011/11/section/41 - Finance Act 2019 s40 (£25 plus 5% of the excess, gifts from 6 April 2019), https://www.legislation.gov.uk/ukpga/2019/1/section/40 - GOV.UK, "Gift Aid: what donations charities and CASCs can claim on", https://www.gov.uk/guidance/gift-aid-what-donations-charities-and-cascs-can-claim-on

1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.

## 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.2 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE241
README.md3,360
data/benefit-limits.json1,222
impl/python.py3,575
impl/rust.rs4,547
impl/typescript.ts3,509
vectors.json8,636