Functional Weave
Code in TypeScript

construction.cis-deduction

Construction Industry Scheme deduction (0, 20 or 30%) on the labour part of a subcontractor payment.

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 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 deduction a contractor withholds under the Construction Industry Scheme from a payment to a subcontractor, and what is left to pay.

## What the rate applies to

For example

  • cisDeduction(£1,000.00, £400.00, standard, 2026-09-23, down) → payment £1,000.00, materials £400.00, labour £600.00, rate 20%, deduction £120.00, net payment £880.00 1,000.00 with 400.00 of materials at the standard rate: 20% of the 600.00 labour only
  • cisDeduction(£1,000.00, £400.00, higher, 2026-09-23, down) → payment £1,000.00, materials £400.00, labour £600.00, rate 30%, deduction £180.00, net payment £820.00 the same payment to an unmatched subcontractor: 30% of the labour
  • cisDeduction(£1,000.00, £400.00, gross, 2026-09-23, down) → payment £1,000.00, materials £400.00, labour £600.00, rate 0%, deduction £0.00, net payment £1,000.00 gross payment status: nothing withheld

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 cisDeduction(payment: Money, materials: Money, status: CisStatus, onDate: string, mode: RoundingMode): CisDeduction
paymentMoneythe payment excluding VAT
materialsMoneymaterials, consumable stores, non-travel fuel, plant hire and prefabricated materials within the payment, excluding VAT
statusCisStatusgross, standard (registered and matched) or higher (unregistered or unmatched)
onDatedatethe date of payment, which decides the rate
modeRoundingModehow to round the deduction to a penny; HMRC publishes no rule, down never over-deducts
returnsCisDeduction

The types it declares, generated into your project

export type CisStatus = "gross" | "standard" | "higher";

/** The payment split into what the deduction is taken from and what is paid. */
export interface CisDeduction {
  /** the payment excluding VAT, as passed */
  readonly payment: Money;
  /** the part the deduction does not apply to */
  readonly materials: Money;
  /** payment less materials: what the rate is applied to */
  readonly labour: Money;
  /** the rate applied, 2000 = 20% */
  readonly basisPoints: number;
  /** withheld and paid to HMRC */
  readonly deduction: Money;
  /** payment less deduction, before any VAT is added */
  readonly netPayment: Money;
}

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

import { cisDeduction } from "#fune/construction.cis-deduction@^1";
impl/typescript.ts · 67 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 } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { subtractMoney } from "./money_add.ts";  ← from money.add ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { applyRate } from "./money_apply_rate.ts";  ← from money.apply-rate ^1.0.0 · built alongside by fune
import { CIS_RATES, CIS_RATES_HISTORY } from "./construction_cis_deduction_data.ts";  ← this capability’s own data, compiled from data/cis-rates.json into the same file by fune build
import { type CisDeduction, type CisStatus } from "./construction_cis_deduction_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;

function rateOn(status: string, onDate: string): number {
  let best: { basisPoints: number; validFrom: string } | null = null;
  let earliest: string | null = null;
  for (const rule of CIS_RATES) {
    if (rule.status !== status) continue;
    if (earliest === null || rule.validFrom < earliest) earliest = rule.validFrom;
    if (onDate < rule.validFrom) continue;
    if (rule.validTo !== null && onDate > rule.validTo) continue;
    if (best === null || rule.validFrom > best.validFrom) best = rule;
  }
  if (best !== null) return best.basisPoints;
  // A history=current build has dropped the old rows; say so rather than
  // answering an old payment at today's rate.
  if (CIS_RATES_HISTORY !== "full" && earliest !== null && onDate < earliest) {
    throw new RangeError(
      `no CIS rate for ${status} on ${onDate}: this build was installed with history=${CIS_RATES_HISTORY}, ` +
        `so it only carries rates from ${earliest}. Reinstall with history=full for older payments.`
    );
  }
  throw new RangeError(`no CIS rate for ${status} on ${onDate}`);
}

/**
 * The CIS deduction a contractor withholds from a subcontractor's payment.
 *
 * The rate applies only to the labour: VAT is left out by taking the payment
 * net of VAT, and materials (with consumables, non-travel fuel, plant hire and
 * prefabricated materials) are taken off before the rate is applied. Applying
 * 20% to the whole invoice is the classic over-deduction.
 */
export function cisDeduction(payment: Money, materials: Money, status: CisStatus, onDate: string, mode: RoundingMode): CisDeduction {
  if (status !== "gross" && status !== "standard" && status !== "higher") {
    throw new RangeError(`unknown CIS status "${status}"`);
  }
  if (!ISO_DATE.test(onDate)) {
    throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  }
  if (payment.minor < 0) {
    throw new RangeError(`payment must not be negative, received ${payment.minor}`);
  }
  if (materials.minor < 0) {
    throw new RangeError(`materials must not be negative, received ${materials.minor}`);
  }
  const labour = subtractMoney(payment, materials);
  if (labour.minor < 0) {
    throw new RangeError(`materials (${materials.minor}) cannot exceed the payment (${payment.minor})`);
  }
  const basisPoints = rateOn(status, onDate);
  const deduction = applyRate(labour, basisPoints, mode);
  return {
    payment,
    materials,
    labour,
    basisPoints,
    deduction,
    netPayment: subtractMoney(payment, deduction),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 4 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 construction.cis-deduction
Download for TypeScript construction.cis-deduction-1.0.0-typescript.fune · 24,428 bytes sha256 e13faa3f86e28f55b4ae5d864df7da3c28d11cbd98637e50a42346ed625c2a0d

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

The whole function, every language, is one file too: construction.cis-deduction-1.0.0.fune, 32,006 bytes, sha256 dd6fa7c2bb71beabab099d76fc5b21a9dcc123675e9ee6d1d06b66e2676003c0. 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 construction.cis-deduction

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

// fune: after construction.cis-deduction

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 construction.cis-deduction
// fune: replace money.add in construction.cis-deduction
// fune: replace money.amount in construction.cis-deduction
// fune: replace money.apply-rate in construction.cis-deduction

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 construction.cis-deduction --steps.

// fune: step construction.cis-deduction 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
1,000.00 with 400.00 of materials at the standard rate: 20% of the 600.00 labour only £1,000.00, £400.00, standard, 2026-09-23, down → payment £1,000.00, materials £400.00, labour £600.00, rate 20%, deduction £120.00, net payment £880.00
the same payment to an unmatched subcontractor: 30% of the labour £1,000.00, £400.00, higher, 2026-09-23, down → payment £1,000.00, materials £400.00, labour £600.00, rate 30%, deduction £180.00, net payment £820.00
gross payment status: nothing withheld £1,000.00, £400.00, gross, 2026-09-23, down → payment £1,000.00, materials £400.00, labour £600.00, rate 0%, deduction £0.00, net payment £1,000.00
labour only: 20% of 2,500.00 £2,500.00, £0.00, standard, 2026-09-23, down → payment £2,500.00, materials £0.00, labour £2,500.00, rate 20%, deduction £500.00, net payment £2,000.00
not 20% of the whole invoice: 1,500.00 with 900.00 materials withholds 120.00, not 300.00 £1,500.00, £900.00, standard, 2026-09-23, half-up → payment £1,500.00, materials £900.00, labour £600.00, rate 20%, deduction £120.00, net payment £1,380.00
123.48 at 20% is 24.696: down gives 24.69 £123.48, £0.00, standard, 2026-09-23, down → payment £123.48, materials £0.00, labour £123.48, rate 20%, deduction £24.69, net payment £98.79
123.48 at 20% is 24.696: half-up gives 24.70 £123.48, £0.00, standard, 2026-09-23, half-up → payment £123.48, materials £0.00, labour £123.48, rate 20%, deduction £24.70, net payment £98.78
100.01 at 30% is 30.003: up gives 30.01 £100.01, £0.00, higher, 2026-09-23, up → payment £100.01, materials £0.00, labour £100.01, rate 30%, deduction £30.01, net payment £70.00
all materials: no labour, no deduction £500.00, £500.00, standard, 2026-09-23, down → payment £500.00, materials £500.00, labour £0.00, rate 20%, deduction £0.00, net payment £500.00
zero payment £0.00, £0.00, higher, 2026-09-23, down → payment £0.00, materials £0.00, labour £0.00, rate 30%, deduction £0.00, net payment £0.00
Show the other 12 tests
CaseArgumentsExpected
the first day of the 2007 scheme: 20% £1,000.00, £0.00, standard, 2007-04-06, down → payment £1,000.00, materials £0.00, labour £1,000.00, rate 20%, deduction £200.00, net payment £800.00
the last day of the old scheme: 18% for a CIS4 card holder £1,000.00, £0.00, standard, 2007-04-05, down → payment £1,000.00, materials £0.00, labour £1,000.00, rate 18%, deduction £180.00, net payment £820.00
a 2003 payment to a CIS6 certificate holder: gross £1,000.00, £0.00, gross, 2003-01-15, down → payment £1,000.00, materials £0.00, labour £1,000.00, rate 0%, deduction £0.00, net payment £1,000.00
there was no higher rate before 6 April 2007 £1,000.00, £0.00, higher, 2006-06-01, down → error: no CIS rate for higher on 2006-06-01
before the 2000 rates this registry carries £1,000.00, £0.00, standard, 1999-12-31, down → error: no CIS rate for standard on 1999-12-31
materials above the payment £100.00, £100.01, standard, 2026-09-23, down → error: materials (10001) cannot exceed the payment (10000)
a negative payment -£1.00, £0.00, standard, 2026-09-23, down → error: payment must not be negative
negative materials £1.00, -£0.01, standard, 2026-09-23, down → error: materials must not be negative
payment and materials in different currencies £1.00, €0.10, standard, 2026-09-23, down → error: currency mismatch
a date that is not ISO £1.00, £0.00, standard, 23/09/2026, down → error: onDate must be an ISO date
an unknown status £1.00, £0.00, registered, 2026-09-23, down → error: unknown CIS status "registered"
an unknown rounding mode £123.48, £0.00, standard, 2026-09-23, nearest → error: unknown rounding mode "nearest"

More from the author

Only the labour. HMRC's steps are: start from the invoice, take off VAT, take off what the subcontractor paid for materials, consumable stores, fuel (except for travelling), plant hired for the job and manufacturing or prefabricating materials, and apply the rate to what is left. So `payment` is the payment **excluding VAT**, `materials` is all of those costs together (excluding VAT where the subcontractor is VAT registered), and the rate is applied to `payment - materials`. Applying 20% to the whole invoice is the usual mistake: 1,500.00 with 900.00 of materials withholds 120.00, not 300.00.

VAT is paid on top of `netPayment` in full (or reverse charged, see `construction.vat-reverse-charge`); CIS never touches it.

## Rates

Dated rows in `data/cis-rates.json`, looked up on the payment date:

| status | from | rate | |----------|------------|------| | gross | 2007-04-06 | 0% | | standard | 2007-04-06 | 20% (registered and matched) | | higher | 2007-04-06 | 30% (not registered, or not matched on verification) | | standard | 2000-04-06 to 2007-04-05 | 18% (CIS4 card holders) | | gross | 2000-04-06 to 2007-04-05 | 0% (CIS5/CIS6 certificate holders) |

There was no 30% rate before the 2007 scheme, so `higher` before 6 April 2007 is an error, as is any date before 6 April 2000 (earlier rates, 25% to 23%, are not carried). The table declares its effective columns, so a `history=current` build refuses dates before its horizon instead of answering them at today's rate.

## Rounding

HMRC does not publish a rounding rule for the deduction (the monthly return takes it in pounds and pence), so the rounding mode is an argument. `down` never withholds more than the rate; `half-up` is the ordinary commercial choice. Whichever you use, use the same one every month.

## Edges

Materials equal to the payment leave no labour and no deduction. Materials above the payment, negative amounts and mixed currencies are errors.

## Sources

- HMRC, "What you must do as a Construction Industry Scheme (CIS) contractor: Make deductions and pay subcontractors" (rates 20%, 30%, 0% and the items taken off before the rate): https://www.gov.uk/what-you-must-do-as-a-cis-contractor/make-deductions-and-pay-subcontractors - HMRC, "Construction Industry Scheme: a guide for contractors and subcontractors (CIS 340)": https://www.gov.uk/government/publications/construction-industry-scheme-cis-340 - HMRC internal manual CISR71020, "Deductions: overview: the rate of deduction under the Construction Industry Scheme" (the rate history): https://www.gov.uk/hmrc-internal-manuals/construction-industry-scheme-reform/cisr71020

Files

PathBytes
README.md2,855
data/cis-rates.json753
impl/python.py3,003
impl/rust.rs4,288
impl/typescript.ts2,950
vectors.json12,098