Functional Weave
Code in TypeScript

charity.higher-rate-relief

A Gift Aid donor's extra tax relief at higher, intermediate, additional and top rates, by extending the tax bands.

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

Pinned by 16 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 tax adviser review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

What a donor's Gift Aid donations save the donor in their own tax, on top of the basic rate tax the charity reclaims. Give it the donor's taxable income for the year, the total Gift Aid donations they made in it, and where they pay income tax.

ITA 2007 s414(2)(b) treats the basic rate limit and the higher rate limit as raised by the grossed up donations. For a Scottish taxpayer the upper limit of the Scottish basic rate and the limits of every Scottish rate above it are raised too, but **not the starter rate limit**. Income that the wider bands move from a higher rate into a lower one is the relief:

For example

  • higherRateRelief(£60,000.00, £800.00, rest-of-uk, 2026-06-01) → gross donations £1,000.00, basic rate relief £200.00, tax before £16,460.00, tax after £16,260.00, extra relief £200.00, net cost £600.00 higher rate taxpayer, £800 given: £1,000 gross, the donor saves another £200
  • higherRateRelief(£38,000.00, £800.00, rest-of-uk, 2026-06-01) → gross donations £1,000.00, basic rate relief £200.00, tax before £7,660.00, tax after £7,600.00, extra relief £60.00, net cost £740.00 income just £300 into the higher band: only £300 moves, relief £60 not £200
  • higherRateRelief(£150,000.00, £800.00, rest-of-uk, 2026-06-01) → gross donations £1,000.00, basic rate relief £200.00, tax before £53,703.00, tax after £53,453.00, extra relief £250.00, net cost £550.00 additional rate taxpayer: the higher rate limit moves too, relief 25% of gross

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.

export function higherRateRelief(taxableIncome: Money, donations: Money, region: TaxRegion, onDate: string): HigherRateRelief
taxableIncomeMoneyincome after allowances and reliefs for the year, in GBP, before any Gift Aid adjustment
donationsMoneytotal Gift Aid donations the donor made in the tax year: the amounts actually paid
regionTaxRegionwhere the donor pays income tax
onDatedateany date in the tax year of the gifts
returnsHigherRateRelief

The types it declares, generated into your project

export type TaxRegion = "rest-of-uk" | "scotland" | "wales";

/** What the donor's Gift Aid is worth to the donor, beyond the charity's claim. */
export interface HigherRateRelief {
  /** donations grossed up at the UK basic rate: the band extension */
  readonly grossDonations: Money;
  /** the tax the charity reclaims: grossDonations less donations */
  readonly basicRateRelief: Money;
  /** tax on taxableIncome with the ordinary bands, rounded down to the penny */
  readonly taxBefore: Money;
  /** taxBefore less extraRelief */
  readonly taxAfter: Money;
  /** the donor's own saving from the extended bands, rounded down to the penny */
  readonly extraRelief: Money;
  /** donations less extraRelief: what the gifts cost the donor */
  readonly netCost: Money;
}

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

import { higherRateRelief } from "#fune/charity.higher-rate-relief@^1";
impl/typescript.ts · 90 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.

import { giftAid } from "./charity_gift_aid.ts";  ← from charity.gift-aid ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type HigherRateRelief, type TaxRegion } from "./charity_higher_rate_relief_types.ts";
import { type IncomeTaxBand, INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY } from "./payroll_income_tax_data.ts";  ← payroll.income-tax’s rule data (^1.0.0) · built alongside by fune

const REGIONS = ["rest-of-uk", "scotland", "wales"];

function bandsFor(region: string, onDate: string): IncomeTaxBand[] {
  const rows = INCOME_TAX_BANDS.filter(
    (b) => b.region === region && b.validFrom <= onDate && (b.validTo === null || onDate <= b.validTo)
  );
  if (rows.length === 0) {
    // A history=current build only carries this year's bands; an older year
    // would otherwise be answered with the wrong bands.
    const froms = INCOME_TAX_BANDS.filter((b) => b.region === region).map((b) => b.validFrom);
    if (INCOME_TAX_BANDS_HISTORY !== "full" && froms.length > 0) {
      const earliest = froms.reduce((a, b) => (b < a ? b : a));
      if (onDate < earliest) {
        throw new RangeError(
          `no income tax bands for ${region} on ${onDate}: this build was installed with history=${INCOME_TAX_BANDS_HISTORY}, so it only carries rules from ${earliest}. Reinstall with history=full for earlier tax years.`
        );
      }
    }
    throw new RangeError(`no income tax bands for ${region} on ${onDate}`);
  }
  return [...rows].sort((a, b) => a.band - b.band);
}

function basicBandIndex(bands: IncomeTaxBand[], region: string, onDate: string): number {
  const index = bands.findIndex((b) => b.basicRate);
  if (index < 0) throw new RangeError(`no basic rate band for ${region} on ${onDate}`);
  return index;
}

/** Tax on income in pence, times 10000 (pence x basis points), unrounded. */
function scaledTax(income: number, bands: IncomeTaxBand[], extendFrom: number, extension: number): number {
  let total = 0;
  let lower = 0;
  for (let i = 0; i < bands.length && income > lower; i++) {
    const band = bands[i];
    const upper = band.upTo === null ? null : band.upTo * 100 + (i >= extendFrom ? extension : 0);
    const top = upper === null || income < upper ? income : upper;
    if (top > lower) total += (top - lower) * band.basisPoints;
    if (upper === null) break;
    lower = upper;
  }
  return total;
}

/**
 * The extra relief a Gift Aid donor gets through their own tax: ITA 2007
 * s414(2)(b) raises the basic rate limit and the higher rate limit (for a
 * Scottish taxpayer, the Scottish basic rate limit and every limit above it,
 * but not the starter rate limit) by the grossed up donations. Income moved
 * from a higher band into a lower one is the relief.
 */
export function higherRateRelief(taxableIncome: Money, donations: Money, region: TaxRegion, onDate: string): HigherRateRelief {
  if (taxableIncome.currency !== "GBP") {
    throw new RangeError(`taxableIncome must be in GBP, received ${taxableIncome.currency}`);
  }
  if (taxableIncome.minor < 0) {
    throw new RangeError(`taxableIncome must not be negative, received ${taxableIncome.minor}`);
  }
  if (!REGIONS.includes(region)) {
    throw new RangeError(`unknown tax region "${region}": expected rest-of-uk, scotland or wales`);
  }
  const grossed = giftAid(donations, onDate);
  const bands = bandsFor(region, onDate);
  const basic = basicBandIndex(bands, region, onDate);
  const ukBands = bandsFor("rest-of-uk", onDate);
  const ukBasic = ukBands[basicBandIndex(ukBands, "rest-of-uk", onDate)];
  if (bands[basic].basisPoints !== ukBasic.basisPoints) {
    // s414A then adjusts the donor's tax by the rate difference; not modelled.
    throw new RangeError(
      `the ${region} basic rate differs from the UK basic rate on ${onDate}; the section 414A adjustment is not supported`
    );
  }
  const before = scaledTax(taxableIncome.minor, bands, basic, 0);
  const after = scaledTax(taxableIncome.minor, bands, basic, grossed.gross.minor);
  const taxBefore = Math.floor(before / 10000);
  const extra = Math.floor((before - after) / 10000);
  return {
    grossDonations: grossed.gross,
    basicRateRelief: grossed.taxReclaimable,
    taxBefore: money(taxBefore, "GBP"),
    taxAfter: money(taxBefore - extra, "GBP"),
    extraRelief: money(extra, "GBP"),
    netCost: money(donations.minor - extra, "GBP"),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the TypeScript 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 charity.higher-rate-relief
Download for TypeScript charity.higher-rate-relief-1.0.0-typescript.fune · 18,239 bytes sha256 23e3c2db5c02d17b43247ee255a59a0187a32d7f25d3c3f4d724927455bd5ead

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./charity.higher-rate-relief-1.0.0-typescript.fune, or fetch it from a terminal with fune pull charity.higher-rate-relief@1.0.0:typescript.

The whole function, every language, is one file too: charity.higher-rate-relief-1.0.0.fune, 28,062 bytes, sha256 e2ac4b8686231fef1cf45c66ac8d43723d57152a6aeef90a157fc4660e0cc9cf. 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.higher-rate-relief

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

// fune: after charity.higher-rate-relief

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 charity.gift-aid in charity.higher-rate-relief
// fune: replace money.amount in charity.higher-rate-relief
// fune: replace payroll.income-tax in charity.higher-rate-relief

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.higher-rate-relief --steps.

// fune: step charity.higher-rate-relief 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
higher rate taxpayer, £800 given: £1,000 gross, the donor saves another £200 £60,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £16,460.00, tax after £16,260.00, extra relief £200.00, net cost £600.00
income just £300 into the higher band: only £300 moves, relief £60 not £200 £38,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £7,660.00, tax after £7,600.00, extra relief £60.00, net cost £740.00
additional rate taxpayer: the higher rate limit moves too, relief 25% of gross £150,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £53,703.00, tax after £53,453.00, extra relief £250.00, net cost £550.00
basic rate taxpayer: no extra relief £20,000.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £4,000.00, tax after £4,000.00, extra relief £0.00, net cost £800.00
no taxable income: no tax, no extra relief £0.00, £800.00, rest-of-uk, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £0.00, tax after £0.00, extra relief £0.00, net cost £800.00
£10.01 given: 20% of £12.51 is 250.2p, rounded down to 250p £60,000.00, £10.01, rest-of-uk, 2026-06-01 → gross donations £12.51, basic rate relief £2.50, tax before £16,460.00, tax after £16,457.50, extra relief £2.50, net cost £7.51
Scottish higher rate taxpayer 2026-27: 42% less 20%, relief £220 on £1,000 gross £40,000.00, £800.00, scotland, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £10,061.45, tax after £9,841.45, extra relief £220.00, net cost £580.00
Scottish intermediate rate taxpayer: relief 1%, the starter band is not extended £20,000.00, £800.00, scotland, 2026-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £3,990.77, tax after £3,980.77, extra relief £10.00, net cost £790.00
Scottish top rate taxpayer 2023-24: the top rate threshold moves as well, 27% relief £130,000.00, £800.00, scotland, 2023-12-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £48,160.88, tax after £47,890.88, extra relief £270.00, net cost £530.00
a Welsh taxpayer uses the UK limits £60,000.00, £800.00, wales, 2024-06-01 → gross donations £1,000.00, basic rate relief £200.00, tax before £16,460.00, tax after £16,260.00, extra relief £200.00, net cost £600.00
Show the other 6 tests
CaseArgumentsExpected
no donations: nothing changes £60,000.00, £0.00, rest-of-uk, 2026-06-01 → gross donations £0.00, basic rate relief £0.00, tax before £16,460.00, tax after £16,460.00, extra relief £0.00, net cost £0.00
a tax year before the bands on file is an error £60,000.00, £800.00, rest-of-uk, 2022-06-01 → error: no income tax bands for rest-of-uk on 2022-06-01
an unknown region is an error £60,000.00, £800.00, england, 2026-06-01 → error: unknown tax region "england"
negative taxable income is an error -£0.01, £800.00, rest-of-uk, 2026-06-01 → error: taxableIncome must not be negative
taxable income in euros is an error €60,000.00, £800.00, rest-of-uk, 2026-06-01 → error: taxableIncome must be in GBP
negative donations are an error £60,000.00, -£1.00, rest-of-uk, 2026-06-01 → error: donation must not be negative

More from the author

- a higher rate taxpayer giving £800 (£1,000 gross) saves another £200; - an additional rate taxpayer saves £250, because the additional rate threshold moves as well; - a Scottish intermediate rate taxpayer saves 1% of the gross, a Scottish higher rate taxpayer 22%; - someone only £300 into the higher band saves 20% of £300, not of £1,000.

The relief is worked out as the difference between the tax on the same income with the ordinary and the extended bands, in exact pence x basis points, and rounded down to the penny once. `taxAfter` is `taxBefore` less that relief.

## Where the bands come from

The bands are **payroll.income-tax's own dated band data** (region, rate, upper limit, which band is the basic rate), imported from its data module rather than copied, so a new tax year added there reaches this capability too. That data starts in 2023-24, so earlier tax years are an error. Its `upTo` figures are the statutory limits on taxable income (basic rate limit £37,700, higher rate limit £125,140; the Scottish limits by year). Grossing up uses the UK basic rate from charity.gift-aid, as s415 requires, for Scottish and Welsh taxpayers too.

## What it does not do

- **The personal allowance taper.** Gift Aid also reduces adjusted net income (ITA 2007 s58), which can restore personal allowance lost above £100,000 and is worth far more there. `taxableIncome` is taken as given, after allowances; work the allowance out first with the donations deducted from adjusted net income, then call this. - Savings and dividend income, which have their own rates. - s414A, the adjustment when a devolved basic rate differs from the UK basic rate. The Scottish and Welsh basic rates are 20% in every year on file, so it never applies; if the data ever says otherwise this is an error rather than a wrong answer. - Carry-back of gifts to the previous tax year (s426).

## Sources

- Income Tax Act 2007 s414 (band extension, including the Scottish limits) and s415 (grossed up at the basic rate), https://www.legislation.gov.uk/ukpga/2007/3/section/414 - ITA 2007 s58 (adjusted net income), https://www.legislation.gov.uk/ukpga/2007/3/section/58 - Band figures: see payroll.income-tax's README and data.

Files

PathBytes
README.md2,880
impl/python.py4,195
impl/rust.rs5,291
impl/typescript.ts4,270
vectors.json6,723