Functional Weave
Code in TypeScript

energy.climate-change-levy

Climate Change Levy on a business supply of electricity, gas, LPG or solid fuel, at the main rate in force on the date.

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

Pinned by 20 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

The Climate Change Levy (CCL) a supplier adds to a business energy bill:

levy = quantity x main rate for the commodity on the date of supply

For example

  • climateChangeLevy(electricity, 10,000,000, 2026-05-01, false, half-up) → commodity electricity, unit kWh, rate 801, excluded false, levy £80.10, valid from 2026-04-01, valid to 2027-03-31 10,000 kWh of business electricity in May 2026 at £0.00801 is £80.10
  • climateChangeLevy(gas, 5,000,000, 2024-01-15, false, half-up) → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31 5,000 kWh of gas in January 2024, still at the 2023 gas rate of £0.00672
  • climateChangeLevy(gas, 5,000,000, 2024-03-31, false, half-up) → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31 gas on 31 March 2024, the last day of the lower rate

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 climateChangeLevy(commodity: CclCommodity, quantity: number, onDate: string, qualifyingUse: boolean, mode: RoundingMode): ClimateChangeLevy
commodityCclCommodityelectricity and gas are charged per kWh, lpg and other per kg
quantityintin thousandths of the unit: watt-hours, or grams
onDatedatethe date of supply the rate applies on
qualifyingUsebooltrue for domestic use, charity non-business use or a de minimis supply: the levy does not apply
modeRoundingModehow the levy rounds to a whole penny
returnsClimateChangeLevy

The types it declares, generated into your project

export type CclCommodity = "electricity" | "gas" | "lpg" | "other";

/** The levy, the rate it was charged at, and whether the supply was outside it. */
export interface ClimateChangeLevy {
  readonly commodity: string;
  /** kWh or kg */
  readonly unit: string;
  /** main rate in thousandths of a penny per unit: £0.00775/kWh is 775 */
  readonly rate: number;
  /** true when qualifying use kept the supply out of the levy */
  readonly excluded: boolean;
  /** GBP, zero when excluded */
  readonly levy: Money;
  /** when the rate came into force */
  readonly validFrom: string;
  /** when it stops; null while it is the latest rate published */
  readonly validTo: string | null;
}

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

import { climateChangeLevy } from "#fune/energy.climate-change-levy@^1";
impl/typescript.ts · 71 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 { type RoundingMode, roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { CCL_RATES, CCL_RATES_HISTORY } from "./energy_climate_change_levy_data.ts";  ← this capability’s own data, compiled from data/ccl-rates.json into the same file by fune build
import { type ClimateChangeLevy, type CclCommodity } from "./energy_climate_change_levy_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
// quantity (thousandths of a unit) x rate (thousandths of a penny) is in
// millionths of a penny.
const DIVISOR = 1_000_000;

/**
 * The Climate Change Levy on one supply, at the main rate in force on
 * `onDate`. A qualifying-use supply (domestic, charity non-business or
 * de minimis) is outside the levy and comes back excluded, with a zero levy.
 */
export function climateChangeLevy(
  commodity: CclCommodity,
  quantity: number,
  onDate: string,
  qualifyingUse: boolean,
  mode: RoundingMode
): ClimateChangeLevy {
  if (!ISO_DATE.test(onDate)) {
    throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  }
  if (!Number.isInteger(quantity) || quantity < 0) {
    throw new RangeError(`quantity must be a whole number of thousandths of a unit, 0 or more, received ${quantity}`);
  }
  if (!CCL_RATES.some((r) => r.commodity === commodity)) {
    throw new RangeError(`unknown commodity "${commodity}": expected electricity, gas, lpg or other`);
  }
  let best: (typeof CCL_RATES)[number] | null = null;
  for (const r of CCL_RATES) {
    if (r.commodity !== commodity || onDate < r.validFrom) continue;
    if (r.validTo !== null && onDate > r.validTo) continue;
    if (best === null || r.validFrom > best.validFrom) best = r;
  }
  if (best === null) {
    // A pruned build must not answer an old date with a newer rate.
    if (CCL_RATES_HISTORY !== "full") {
      let earliest: string | null = null;
      for (const r of CCL_RATES) {
        if (r.commodity === commodity && (earliest === null || r.validFrom < earliest)) earliest = r.validFrom;
      }
      if (earliest !== null && onDate < earliest) {
        throw new RangeError(
          `no Climate Change Levy rate for ${commodity} on ${onDate}: this build was installed with history=${CCL_RATES_HISTORY}, ` +
            `so it only carries rates from ${earliest}. Reinstall with history=full for older supplies.`
        );
      }
    }
    throw new RangeError(`no Climate Change Levy rate for ${commodity} on ${onDate}`);
  }
  let pence = 0;
  if (!qualifyingUse) {
    const product = quantity * best.rate;
    if (product > Number.MAX_SAFE_INTEGER) {
      throw new RangeError("levy too large to calculate exactly");
    }
    pence = roundDiv(product, DIVISOR, mode);
  }
  return {
    commodity,
    unit: best.unit,
    rate: best.rate,
    excluded: qualifyingUse,
    levy: money(pence, "GBP"),
    validFrom: best.validFrom,
    validTo: best.validTo,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 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 energy.climate-change-levy
Download for TypeScript energy.climate-change-levy-1.0.2-typescript.fune · 20,897 bytes sha256 817611581d638b413d4be0ba6e50c6413e332a60c004a2d01abc56a50b5f7471

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./energy.climate-change-levy-1.0.2-typescript.fune, or fetch it from a terminal with fune pull energy.climate-change-levy@1.0.2:typescript.

The whole function, every language, is one file too: energy.climate-change-levy-1.0.2.fune, 28,839 bytes, sha256 18139232039628d0676a0d19756af8beb5c03b60f1811a68eccf403a14f406fc. 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 energy.climate-change-levy

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

// fune: after energy.climate-change-levy

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 math.round-div in energy.climate-change-levy
// fune: replace money.amount in energy.climate-change-levy

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 energy.climate-change-levy --steps.

// fune: step energy.climate-change-levy 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 kWh of business electricity in May 2026 at £0.00801 is £80.10 electricity, 10,000,000, 2026-05-01, false, half-up → commodity electricity, unit kWh, rate 801, excluded false, levy £80.10, valid from 2026-04-01, valid to 2027-03-31
5,000 kWh of gas in January 2024, still at the 2023 gas rate of £0.00672 gas, 5,000,000, 2024-01-15, false, half-up → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31
gas on 31 March 2024, the last day of the lower rate gas, 5,000,000, 2024-03-31, false, half-up → commodity gas, unit kWh, rate 672, excluded false, levy £33.60, valid from 2023-04-01, valid to 2024-03-31
gas on 1 April 2024 is charged at the equalised £0.00775 gas, 5,000,000, 2024-04-01, false, half-up → commodity gas, unit kWh, rate 775, excluded false, levy £38.75, valid from 2024-04-01, valid to 2025-03-31
a tonne of LPG at £0.02175 per kg lpg, 1,000,000, 2025-06-30, false, half-up → commodity lpg, unit kg, rate 2,175, excluded false, levy £21.75, valid from 2025-04-01, valid to 2026-03-31
a tonne of coal as any other taxable commodity in 2026 other, 1,000,000, 2026-04-01, false, half-up → commodity other, unit kg, rate 6,264, excluded false, levy £62.64, valid from 2026-04-01, valid to 2027-03-31
the rate already announced for April 2027 is the latest, open-ended electricity, 1,000,000, 2027-04-01, false, half-up → commodity electricity, unit kWh, rate 827, excluded false, levy £8.27, valid from 2027-04-01, valid to —
a domestic supply is outside the levy but still reports the rate electricity, 10,000,000, 2026-05-01, true, half-up → commodity electricity, unit kWh, rate 801, excluded true, levy £0.00, valid from 2026-04-01, valid to 2027-03-31
one kWh is 0.775p, rounded half-up to 1p electricity, 1,000, 2025-06-01, false, half-up → commodity electricity, unit kWh, rate 775, excluded false, levy £0.01, valid from 2025-04-01, valid to 2026-03-31
one kWh is 0.775p, rounded down to nothing electricity, 1,000, 2025-06-01, false, down → commodity electricity, unit kWh, rate 775, excluded false, levy £0.00, valid from 2025-04-01, valid to 2026-03-31
Show the other 10 tests
CaseArgumentsExpected
a fraction of a kWh in watt-hours: 1,234.567 kWh of gas at 0.775p gas, 1,234,567, 2025-06-01, false, half-up → commodity gas, unit kWh, rate 775, excluded false, levy £9.57, valid from 2025-04-01, valid to 2026-03-31
60 kg of LPG is exactly 130.5p: half-even keeps the even 130p lpg, 60,000, 2025-06-01, false, half-even → commodity lpg, unit kg, rate 2,175, excluded false, levy £1.30, valid from 2025-04-01, valid to 2026-03-31
no energy is no levy gas, 0, 2026-06-01, false, half-up → commodity gas, unit kWh, rate 801, excluded false, levy £0.00, valid from 2026-04-01, valid to 2027-03-31
a date before the earliest rate carried is an error electricity, 1,000, 2023-03-31, false, half-up → error: no Climate Change Levy rate for electricity on 2023-03-31
an unknown commodity is refused diesel, 1,000, 2025-06-01, false, half-up → error: unknown commodity "diesel"
a negative quantity is refused gas, -1, 2025-06-01, false, half-up → error: quantity must be a whole number of thousandths of a unit
a malformed date is refused gas, 1,000, 01/06/2025, false, half-up → error: onDate must be an ISO date
an unknown rounding mode is refused gas, 1,000, 2025-06-01, false, nearest → error: unknown rounding mode "nearest"
a onDate with a trailing newline is refused gas, 1,000, 2026-09-16 , false, half-up → error: onDate must be an ISO date (YYYY-MM-DD), received "2026-09-16 "
a onDate in Arabic-Indic digits is refused gas, 1,000, ٢٠٢٦-٠٩-١٦, false, half-up → error: onDate must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-١٦"

More from the author

Electricity and gas are charged per kWh; LPG and "any other taxable commodity" (coal, coke, petroleum coke) per kg. The rates are dated data from HMRC, from 1 April 2023 to the rates already announced for 1 April 2027.

## Qualifying use: the domestic and de minimis exclusion

CCL does not apply to supplies for **domestic use** or **charity non-business use**, and supplies small enough to be **de minimis** are excluded too. Those are the same supplies that get VAT at the reduced 5% rate as "qualifying use" (VAT Notice 701/19): for a business, up to an average of 33 kWh a day (1,000 kWh a month) of electricity, or 145 kWh a day (4,397 kWh a month) of piped gas. Pass `qualifyingUse: true` for any of these and the result has `excluded: true` and a zero levy; the rate is still returned, so a bill can show which rate it did not charge. Deciding whether a supply qualifies (including the 60% mixed-use rule) is the caller's, because it depends on the customer, not the kWh alone.

## Units and rounding

- Quantities are integers in **thousandths** of the unit (watt-hours or grams), so part-units are exact. - Rates are integers in **thousandths of a penny** per unit, exactly the five decimal places of pounds HMRC publishes (£0.00775 per kWh is `775`). - The levy is rounded once to a whole penny by `mode` (suppliers usually round half-up per bill). Quantity times rate must stay within 2^53.

## Not covered

- **Reduced rates for climate change agreement holders** (a percentage discount off the main rate: 92% for electricity, 89% for gas from April 2024) — apply the discount to the result if the site holds a CCA. - **Carbon Price Support rates** paid by generators. - Supplies spanning a rate change on 1 April: split the quantity by date and call once per part.

## Dates and pruning

Each rate applies from `validFrom` to `validTo` inclusive; the latest has a null `validTo`. A build installed with `history=current` keeps only rates still in force, and a date before the earliest surviving rate is refused with an explanation rather than charged at a newer rate.

## Sources

- HMRC, "Climate Change Levy rates", https://www.gov.uk/guidance/climate-change-levy-rates (main rates from 1 April 2023, 2024, 2025, 2026 and 2027; last updated 27 November 2025). - HMRC, "VAT on fuel and power (VAT Notice 701/19)", https://www.gov.uk/guidance/vat-on-fuel-and-power-notice-70119 (paragraph 2.8: the levy does not apply to domestic or charity non-business use, and de minimis supplies are excluded; paragraphs 4.2 and 5.2: the de minimis limits).

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/).

1.0.2 adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE189
README.md3,117
data/ccl-rates.json4,291
impl/python.py3,043
impl/rust.rs4,614
impl/typescript.ts2,824
vectors.json5,231