construction.vat-reverse-charge
Whether the UK VAT domestic reverse charge applies to a construction supply, and the invoice wording.
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 VAT specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
Decides whether the UK VAT domestic reverse charge for building and construction services (VAT Act 1994 s.55A, in force from 1 March 2021) applies to one supply. It also gives the note the invoice must carry when it does. Under the reverse charge the supplier invoices without VAT and the customer accounts for the VAT to HMRC.
## The conditions, in the order they are checked
For example
domestic_reverse_charge(supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa…)→ applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. a VAT and CIS registered subcontractor billing a VAT and CIS registered contractordomestic_reverse_charge(supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category reduced, employment business false, end user notified fal…)→ applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. reduced-rated work is reverse charged toodomestic_reverse_charge(supply date 2021-03-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa…)→ applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. the first day of the reverse charge
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 domestic_reverse_charge(supply: &ReverseChargeSupply) -> ReverseChargeDecision
| supply | ReverseChargeSupply | |
| returns | ReverseChargeDecision |
The types it declares, generated into your project
// VatCategory is a string in Rust, one of: "standard", "reduced", "zero", "exempt".
// Parameters take it as &str and results hold it as String.
// ReverseChargeReason is a string in Rust, one of: "reverse-charge", "before-start", "supplier-not-vat-registered", "customer-not-vat-registered", "not-reported-under-cis", "not-standard-or-reduced", "employment-business", "end-user-notified", "five-percent-disregard".
// Parameters take it as &str and results hold it as String.
/// The facts about one supply of construction services that decide its VAT treatment.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReverseChargeSupply {
/// tax point of the supply
pub supply_date: String,
/// the supplier is VAT registered in the UK
pub supplier_vat_registered: bool,
/// the customer is VAT registered in the UK
pub customer_vat_registered: bool,
/// payment for the supply is reported within the Construction Industry Scheme
pub reported_under_cis: bool,
/// the rate the supply would bear under normal rules
pub vat_category: String,
/// the supplier is an employment business supplying staff or workers
pub employment_business: bool,
/// the customer has told the supplier in writing it is an end user or intermediary supplier
pub end_user_notified: bool,
/// contracted value of the reverse charge services as a share of the whole supply; 10000 when all of it
pub reverse_charge_share_basis_points: i64,
}
/// The treatment, why, and what the invoice must say.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReverseChargeDecision {
pub applies: bool,
pub reason: String,
/// the note the invoice must carry when the reverse charge applies; null otherwise
pub invoice_wording: Option<String>,
}
Your code names it in one line, in the file that uses it
fune!(construction.vat-reverse-charge@^1); // then call domestic_reverse_charge(…)
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::construction_vat_reverse_charge_data::{
ReverseChargeRule, REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON,
};
fn is_iso_date(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes
.iter()
.enumerate()
.all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}
fn not_applied(reason: &str) -> ReverseChargeDecision {
ReverseChargeDecision {
applies: false,
reason: reason.to_string(),
invoice_wording: None,
}
}
/// Whether the UK VAT domestic reverse charge for building and construction
/// services (VATA 1994 s.55A) applies to one supply.
///
/// The conditions are checked in a fixed order and the first that fails is the
/// reason, so the answer is the same in every language and explains itself.
///
/// # Panics
/// Panics on a malformed date, a share outside 0..=10000 or an unknown category.
pub fn domestic_reverse_charge(supply: &ReverseChargeSupply) -> ReverseChargeDecision {
if !is_iso_date(&supply.supply_date) {
panic!(
"supplyDate must be an ISO date (YYYY-MM-DD), received \"{}\"",
supply.supply_date
);
}
let share = supply.reverse_charge_share_basis_points;
if !(0..=10000).contains(&share) {
panic!(
"reverseChargeShareBasisPoints must be between 0 and 10000, received {}",
share
);
}
let category = supply.vat_category.as_str();
if !["standard", "reduced", "zero", "exempt"].contains(&category) {
panic!(
"unknown vatCategory \"{}\": expected standard, reduced, zero or exempt",
category
);
}
let date = supply.supply_date.as_str();
let mut rule: Option<&ReverseChargeRule> = None;
for row in REVERSE_CHARGE_RULES.iter() {
if row.jurisdiction != "GB" || date < row.valid_from {
continue;
}
if let Some(valid_to) = row.valid_to {
if date > valid_to {
continue;
}
}
if rule.map_or(true, |r| row.valid_from > r.valid_from) {
rule = Some(row);
}
}
let rule = match rule {
Some(rule) => rule,
None => {
// A pruned build must not answer "before the reverse charge started"
// for a date it simply no longer carries rules for.
if let Some(horizon) = REVERSE_CHARGE_RULES_HORIZON {
if date < horizon {
panic!(
"no reverse charge rule for {}: this build was installed with history={}, so it only carries rules from {}. Reinstall with history=full.",
date, REVERSE_CHARGE_RULES_HISTORY, horizon
);
}
}
return not_applied("before-start");
}
};
if !supply.supplier_vat_registered {
return not_applied("supplier-not-vat-registered");
}
if !supply.customer_vat_registered {
return not_applied("customer-not-vat-registered");
}
if !supply.reported_under_cis {
return not_applied("not-reported-under-cis");
}
if category != "standard" && category != "reduced" {
return not_applied("not-standard-or-reduced");
}
if supply.employment_business {
return not_applied("employment-business");
}
if supply.end_user_notified {
return not_applied("end-user-notified");
}
if share <= rule.disregard_basis_points {
return not_applied("five-percent-disregard");
}
ReverseChargeDecision {
applies: true,
reason: "reverse-charge".to_string(),
invoice_wording: Some(rule.invoice_wording.to_string()),
}
}
pub fn reverse_charge_supply_from_value(v: &Value) -> ReverseChargeSupply {
let share = v.get("reverseChargeShareBasisPoints");
if let Value::Float(f) = share {
panic!("reverseChargeShareBasisPoints must be an integer, received {}", f);
}
let date = v.get("supplyDate");
if !matches!(date, Value::Str(_)) {
panic!("supplyDate must be an ISO date (YYYY-MM-DD), received \"{:?}\"", date);
}
ReverseChargeSupply {
supply_date: date.as_str().to_string(),
supplier_vat_registered: v.get("supplierVatRegistered").as_bool(),
customer_vat_registered: v.get("customerVatRegistered").as_bool(),
reported_under_cis: v.get("reportedUnderCis").as_bool(),
vat_category: v.get("vatCategory").as_str().to_string(),
employment_business: v.get("employmentBusiness").as_bool(),
end_user_notified: v.get("endUserNotified").as_bool(),
reverse_charge_share_basis_points: share.as_i64(),
}
}
pub fn reverse_charge_decision_to_value(decision: &ReverseChargeDecision) -> Value {
Value::obj(vec![
("applies", Value::Bool(decision.applies)),
("reason", Value::str(&decision.reason)),
(
"invoiceWording",
match &decision.invoice_wording {
Some(text) => Value::str(text),
None => Value::Null,
},
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
reverse_charge_decision_to_value(&domestic_reverse_charge(&reverse_charge_supply_from_value(&args[0])))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and nothing else, 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 construction.vat-reverse-charge
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./construction.vat-reverse-charge-1.0.0-rust.fune, or fetch it from a terminal with fune pull construction.vat-reverse-charge@1.0.0:rust.
The whole function, every language, is one file too: construction.vat-reverse-charge-1.0.0.fune, 34,072 bytes, sha256 3065508965a28cad36fd688f2bfaba11a1b14d62718e966897162b89c4165221. 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.vat-reverse-charge
after — your function gets the result and the arguments, and returns the final result.
// fune: after construction.vat-reverse-charge
replace — it requires no other capability, so there is no dependency to replace.
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.vat-reverse-charge --steps.
// fune: step construction.vat-reverse-charge 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 | |
|---|---|---|---|
| a VAT and CIS registered subcontractor billing a VAT and CIS registered contractor | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| reduced-rated work is reverse charged too | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category reduced, employment business false, end user notified fal… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| the first day of the reverse charge | supply date 2021-03-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| the day before it started | supply date 2021-02-28, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies false, reason before-start, invoice wording — |
| before March 2021 nothing else matters | supply date 2020-06-01, supplier vat registered true, customer vat registered false, reported under cis true, vat category standard, employment business false, end user notified f… | → | applies false, reason before-start, invoice wording — |
| a supplier that is not VAT registered charges no VAT at all | supply date 2026-09-01, supplier vat registered false, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified f… | → | applies false, reason supplier-not-vat-registered, invoice wording — |
| a householder is not VAT registered, so normal VAT rules apply | supply date 2026-09-01, supplier vat registered true, customer vat registered false, reported under cis false, vat category standard, employment business false, end user notified … | → | applies false, reason customer-not-vat-registered, invoice wording — |
| a payment not reported under CIS | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis false, vat category standard, employment business false, end user notified f… | → | applies false, reason not-reported-under-cis, invoice wording — |
| zero-rated new build housing is not reverse charged | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category zero, employment business false, end user notified false,… | → | applies false, reason not-standard-or-reduced, invoice wording — |
| an exempt supply is not reverse charged | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category exempt, employment business false, end user notified fals… | → | applies false, reason not-standard-or-reduced, invoice wording — |
Show the other 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| an employment business supplies staff, not construction services | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business true, end user notified fal… | → | applies false, reason employment-business, invoice wording — |
| a customer that has notified end user status in writing | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified tr… | → | applies false, reason end-user-notified, invoice wording — |
| an intermediary supplier (a landlord re-supplying to its tenant) notifies the same way | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified tr… | → | applies false, reason end-user-notified, invoice wording — |
| zero-rated is checked before end user status | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category zero, employment business false, end user notified true, … | → | applies false, reason not-standard-or-reduced, invoice wording — |
| exactly 5 percent is disregarded: the rule is 5% or less | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies false, reason five-percent-disregard, invoice wording — |
| a supply and fix contract whose fitting is 2 percent | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies false, reason five-percent-disregard, invoice wording — |
| 5.01 percent is not disregarded | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | applies true, reason reverse-charge, invoice wording Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies. |
| a share above 100 percent | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: reverseChargeShareBasisPoints must be between 0 and 10000 |
| a negative share | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: reverseChargeShareBasisPoints must be between 0 and 10000 |
| a fractional share | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: reverseChargeShareBasisPoints must be an integer |
| a malformed date | supply date 01/09/2026, supplier vat registered true, customer vat registered true, reported under cis true, vat category standard, employment business false, end user notified fa… | → | error: supplyDate must be an ISO date |
| an unknown VAT category | supply date 2026-09-01, supplier vat registered true, customer vat registered true, reported under cis true, vat category STANDARD, employment business false, end user notified fa… | → | error: unknown vatCategory |
More from the author
The first condition that fails becomes the `reason`, so every answer explains itself and the three languages agree on which reason wins:
| reason | the reverse charge does not apply because | |---|---| | `before-start` | the tax point is before 1 March 2021 | | `supplier-not-vat-registered` | the supplier is not UK VAT registered (and charges no VAT at all) | | `customer-not-vat-registered` | the customer is not UK VAT registered, e.g. a householder | | `not-reported-under-cis` | the payment is not reported within the Construction Industry Scheme | | `not-standard-or-reduced` | the supply is zero-rated or exempt | | `employment-business` | the supplier is an employment business supplying staff or workers | | `end-user-notified` | the customer has said in writing that it is an end user or intermediary supplier | | `five-percent-disregard` | the reverse charge services are 5% or less of the whole supply | | `reverse-charge` | none of the above: it applies (`applies` is true) |
**End users and intermediary suppliers.** Being one only changes anything once the customer has told the supplier **in writing**. Until then HMRC says the supplier must still apply the reverse charge. An intermediary supplier is connected or linked to the end user, either with a relevant interest in the same land (landlord and tenant) or as part of the same corporate group (Companies Act 2006 s.1161). It gives the same kind of written notice, so both cases are the one `endUserNotified` flag rather than a separate connected-party input.
**The 5% disregard** is judged on the contracted values agreed at the start of the contract, not invoice by invoice. Pass that share as `reverseChargeShareBasisPoints` (10000 when the whole supply is construction services). The guide says "5% or less", so exactly 500 is disregarded, and normal VAT rules then apply to the whole supply. When the predominant element is zero-rated, the supply is not standard or reduced-rated in the first place, so `not-standard-or-reduced` is reported.
## Invoice wording
When the reverse charge applies, `invoiceWording` is "Reverse charge: customer to pay the VAT to HMRC. S55A VATA 94 applies." The VAT Regulations 1995 require the reference "reverse charge", and the other two phrases are HMRC's own examples ("Customer to pay the VAT to HMRC", "S55A VATA 94 applies"). The invoice must also show the VAT due under the reverse charge (or the rate, if the amount cannot be shown) without adding it to the amount charged. That is for the invoice itself; `finance.tax.add-vat` gives the amount.
The start date, the 5% threshold and the wording are dated data in `data/reverse-charge.json`. A `history=current` build refuses a date before its horizon rather than calling it `before-start`.
## Not covered
Credit notes spanning the March 2021 change, the transitional scaffolding rules, and whether a service is within CIS at all (that is the caller's `reportedUnderCis`). The CIS deduction itself is `construction.cis-deduction`.
## Sources
- HMRC, "VAT reverse charge technical guide": https://www.gov.uk/guidance/vat-reverse-charge-technical-guide (start date 1 March 2021, end users and intermediary suppliers, employment businesses, the 5% disregard, invoice wording). - HMRC, "How to use the VAT reverse charge if you supply building and construction services": https://www.gov.uk/guidance/how-to-use-the-vat-reverse-charge-if-you-supply-building-and-construction-services (the list of conditions). - HMRC, "VAT domestic reverse charge for building and construction services": https://www.gov.uk/guidance/vat-domestic-reverse-charge-for-building-and-construction-services
Files
| Path | Bytes |
|---|---|
| README.md | 4,081 |
| data/reverse-charge.json | 312 |
| impl/python.py | 3,348 |
| impl/rust.rs | 5,431 |
| impl/typescript.ts | 3,178 |
| vectors.json | 11,444 |