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 qualifiesgift_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
| donation | Money | the gift, in GBP |
| benefit_value | Money | value of benefits the donor or a connected person gets because of this gift; 0 if none |
| prior_benefits_this_year | Money | benefits from earlier qualifying gifts by this donor to this charity in the same tax year |
| has_declaration | bool | a Gift Aid declaration covers this gift |
| donor_pays_enough_tax | bool | the donor pays at least as much income or capital gains tax as all their Gift Aid gifts reclaim |
| on_date | date | the date of the gift; decides which limits apply |
| returns | GiftAidEligibility |
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(…)
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| £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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| NOTICE | 241 |
| README.md | 3,360 |
| data/benefit-limits.json | 1,222 |
| impl/python.py | 3,575 |
| impl/rust.rs | 4,547 |
| impl/typescript.ts | 3,509 |
| vectors.json | 8,636 |