From 72927e484aa7c91349cea5f96dfed7f924166391 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Thu, 8 Oct 2026 12:17:18 +0100 Subject: [PATCH 1/6] Model the temporary GB zero rate of VAT on domestic electricity (#2166) SI 2026/987 zero-rates domestic electricity in England, Wales and Scotland from 1 October 2026 to 31 March 2027; Northern Ireland and gas stay at 5%. - Add domestic_energy_vat: VAT on the household's own electricity and gas bills, VAT-exclusive base = bill / 1.05, not grossed up by microdata_vat_coverage. Electricity uses the new nation-split gov.hmrc.vat.domestic_electricity_rate (fiscal_year_blend: FY 2026-27 has 183 days at 5% and 182 at 0%); gas uses the reduced rate. - Take the ONS COICOP 04.5 share of household spending (gov.hmrc.vat.domestic_fuel_and_power_share) out of the generic reduced-rate share, so energy is not counted twice. - baseline_domestic_energy_vat day-weights the dated baseline tree, so a simulation with no reform shows no vat_change. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2166.fixed.md | 1 + .../great_britain.yaml | 26 +++ .../vat/domestic_electricity_rate/index.yaml | 3 + .../northern_ireland.yaml | 14 ++ .../vat/domestic_fuel_and_power_share.yaml | 44 +++++ .../parameters/gov/hmrc/vat/reduced_rate.yaml | 2 +- .../gov/hmrc/vat/domestic_energy_vat.yaml | 55 ++++++ .../tests/test_domestic_electricity_vat.py | 157 ++++++++++++++++++ .../variables/gov/hmrc/baseline_vat.py | 8 +- .../variables/gov/hmrc/domestic_energy_vat.py | 76 +++++++++ policyengine_uk/variables/gov/hmrc/vat.py | 8 +- .../reduced_rate_vat_consumption.py | 9 +- 12 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 changelog.d/2166.fixed.md create mode 100644 policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/great_britain.yaml create mode 100644 policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/index.yaml create mode 100644 policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/northern_ireland.yaml create mode 100644 policyengine_uk/parameters/gov/hmrc/vat/domestic_fuel_and_power_share.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/vat/domestic_energy_vat.yaml create mode 100644 policyengine_uk/tests/test_domestic_electricity_vat.py create mode 100644 policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py diff --git a/changelog.d/2166.fixed.md b/changelog.d/2166.fixed.md new file mode 100644 index 0000000000..5fd8f33c3a --- /dev/null +++ b/changelog.d/2166.fixed.md @@ -0,0 +1 @@ +Modelled the temporary zero rate of VAT on domestic electricity in Great Britain from 1 October 2026 to 31 March 2027 (SI 2026/987), with Northern Ireland and gas staying at 5%. VAT on domestic electricity and gas is now charged on the households' own bills (`domestic_energy_vat`), at a new nation-split `gov.hmrc.vat.domestic_electricity_rate` and the reduced rate, instead of on a flat share of total consumption grossed up by `microdata_vat_coverage`. The ONS domestic fuel and power share of spending now comes out of the reduced-rate share, which leaves at most 0.01% of spending in the generic reduced-rate base in any year since 2006. diff --git a/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/great_britain.yaml b/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/great_britain.yaml new file mode 100644 index 0000000000..3a066700c4 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/great_britain.yaml @@ -0,0 +1,26 @@ +description: VAT rate on supplies of domestic electricity in England, Wales and Scotland. It is the reduced rate except while the temporary zero rate applies. +values: + 1994-04-01: 0.08 + 1997-09-01: 0.05 + 2026-10-01: + value: 0 + metadata: + reference: + - title: The Value Added Tax (Supplies of Domestic Electricity) Order 2026 (SI 2026/987), article 3 + href: https://www.legislation.gov.uk/uksi/2026/987/contents/made + - title: HMRC Revenue and Customs Brief 10 (2026) + href: https://www.gov.uk/government/publications/revenue-and-customs-brief-10-2026-temporary-zero-rate-of-vat-for-domestic-electricity-in-great-britain/temporary-zero-rate-of-vat-for-domestic-electricity-in-great-britain + 2027-04-01: 0.05 +metadata: + unit: /1 + period: year + label: VAT rate on domestic electricity, Great Britain + reference: + - title: Value Added Tax Act 1994, Schedule 7A, Group 1 + href: https://www.legislation.gov.uk/ukpga/1994/23/schedule/7A + - title: The Value Added Tax (Supplies of Domestic Electricity) Order 2026 (SI 2026/987) + href: https://www.legislation.gov.uk/uksi/2026/987/contents/made + # Liability follows the date the electricity is consumed (Revenue and + # Customs Brief 10 (2026)). Day-weighting the fiscal year assumes equal + # daily use: FY 2026-27 has 183 days at 5% and 182 at 0%. + fiscal_year_blend: true diff --git a/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/index.yaml b/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/index.yaml new file mode 100644 index 0000000000..ab54bb1738 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/index.yaml @@ -0,0 +1,3 @@ +description: VAT rates on supplies of domestic electricity, by nation. +metadata: + label: VAT rate on domestic electricity diff --git a/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/northern_ireland.yaml b/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/northern_ireland.yaml new file mode 100644 index 0000000000..cbb38988ad --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/domestic_electricity_rate/northern_ireland.yaml @@ -0,0 +1,14 @@ +description: VAT rate on supplies of domestic electricity in Northern Ireland. The temporary zero rate from 1 October 2026 does not apply there. +values: + 1994-04-01: 0.08 + 1997-09-01: 0.05 +metadata: + unit: /1 + period: year + label: VAT rate on domestic electricity, Northern Ireland + reference: + - title: Value Added Tax Act 1994, Schedule 7A, Group 1 + href: https://www.legislation.gov.uk/ukpga/1994/23/schedule/7A + - title: HMRC Revenue and Customs Brief 10 (2026) + href: https://www.gov.uk/government/publications/revenue-and-customs-brief-10-2026-temporary-zero-rate-of-vat-for-domestic-electricity-in-great-britain/temporary-zero-rate-of-vat-for-domestic-electricity-in-great-britain + fiscal_year_blend: true diff --git a/policyengine_uk/parameters/gov/hmrc/vat/domestic_fuel_and_power_share.yaml b/policyengine_uk/parameters/gov/hmrc/vat/domestic_fuel_and_power_share.yaml new file mode 100644 index 0000000000..dc3b70a4cb --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/domestic_fuel_and_power_share.yaml @@ -0,0 +1,44 @@ +description: Share of household final consumption expenditure that goes on domestic fuel and power (electricity, gas and other fuels). It is subtracted from the reduced-rate share of consumption, because domestic electricity and gas are now taxed from their own bills. +values: + 2000-01-01: 0.0213 + 2001-01-01: 0.0203 + 2002-01-01: 0.0197 + 2003-01-01: 0.0189 + 2004-01-01: 0.0215 + 2005-01-01: 0.0219 + 2006-01-01: 0.0263 + 2007-01-01: 0.0259 + 2008-01-01: 0.0299 + 2009-01-01: 0.0305 + 2010-01-01: 0.0309 + 2011-01-01: 0.0291 + 2012-01-01: 0.0319 + 2013-01-01: 0.0323 + 2014-01-01: 0.0275 + 2015-01-01: 0.0274 + 2016-01-01: 0.0264 + 2017-01-01: 0.0249 + 2018-01-01: 0.0258 + 2019-01-01: 0.0264 + 2020-01-01: 0.0286 + 2021-01-01: 0.0290 + 2022-01-01: 0.0351 + 2023-01-01: 0.0363 + 2024-01-01: 0.0283 + 2025-01-01: 0.0279 +metadata: + unit: /1 + period: year + label: Domestic fuel and power share of household spending + reference: + - title: ONS Consumer trends, 04.5 Electricity, gas and other fuels, current prices, not seasonally adjusted (ADFX), release of 30 September 2026 + href: https://www.ons.gov.uk/economy/nationalaccounts/satelliteaccounts/timeseries/adfx/ct + - title: ONS, Household final consumption expenditure, domestic concept, current prices (ABQI) + href: https://www.ons.gov.uk/economy/nationalaccounts/satelliteaccounts/timeseries/abqi/ukea + - title: OBR, VAT ("Around 2.5 per cent of expenditure is taxed at this reduced rate") + href: https://obr.uk/forecasts-in-depth/tax-by-tax-spend-by-spend/vat/ + # ADFX / ABQI by calendar year. From 2006 the share is 2.49% or more, + # against the OBR's 2.5% reduced-rate share, so domestic fuel and power + # accounts for all of it to within rounding and the non-energy remainder + # (children's car seats, smoking-cessation products and similar) is too + # small to separate from published totals. diff --git a/policyengine_uk/parameters/gov/hmrc/vat/reduced_rate.yaml b/policyengine_uk/parameters/gov/hmrc/vat/reduced_rate.yaml index d807a25ca5..6db505553b 100644 --- a/policyengine_uk/parameters/gov/hmrc/vat/reduced_rate.yaml +++ b/policyengine_uk/parameters/gov/hmrc/vat/reduced_rate.yaml @@ -1,4 +1,4 @@ -description: A reduced rate of VAT applicable to select goods and services (domestic fuel and power). +description: A reduced rate of VAT applicable to select goods and services, including domestic gas and other fuels. Domestic electricity has its own rate (gov.hmrc.vat.domestic_electricity_rate), so a change to this rate does not reach electricity. values: 1994-04-01: value: 0.08 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/vat/domestic_energy_vat.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/vat/domestic_energy_vat.yaml new file mode 100644 index 0000000000..ed9b9da39f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/vat/domestic_energy_vat.yaml @@ -0,0 +1,55 @@ +# Temporary GB zero rate on domestic electricity, 1 October 2026 to +# 31 March 2027 (SI 2026/987). £1,050 of bills carries £50 of VAT at 5%. +# FY 2026-27 has 183 days at 5% and 182 at 0%: 50 * 183 / 365 = 25.07. +- name: GB electricity in FY 2026-27 is zero-rated for 182 of 365 days + period: 2026 + absolute_error_margin: 0.01 + input: + region: WALES + electricity_consumption: 1_050 + gas_consumption: 0 + output: + domestic_energy_vat: 25.07 + +- name: Northern Ireland electricity stays at 5% + period: 2026 + absolute_error_margin: 0.01 + input: + region: NORTHERN_IRELAND + electricity_consumption: 1_050 + gas_consumption: 0 + output: + domestic_energy_vat: 50 + +- name: Gas is not affected by the electricity zero rate + period: 2026 + absolute_error_margin: 0.01 + input: + region: SCOTLAND + electricity_consumption: 0 + gas_consumption: 1_050 + output: + domestic_energy_vat: 50 + +- name: GB electricity is back at 5% in FY 2027-28 + period: 2027 + absolute_error_margin: 0.01 + input: + region: LONDON + electricity_consumption: 1_050 + gas_consumption: 0 + output: + domestic_energy_vat: 50 + +- name: Energy VAT adds to VAT without the coverage or household-share gross-up + period: 2025 + absolute_error_margin: 0.01 + input: + region: LONDON + consumption: 0 + electricity_consumption: 1_050 + gas_consumption: 1_050 + output: + reduced_rate_vat_consumption: 0 + vat: 100 + vat_change: 0 diff --git a/policyengine_uk/tests/test_domestic_electricity_vat.py b/policyengine_uk/tests/test_domestic_electricity_vat.py new file mode 100644 index 0000000000..e3217a7dda --- /dev/null +++ b/policyengine_uk/tests/test_domestic_electricity_vat.py @@ -0,0 +1,157 @@ +""" +Domestic electricity VAT: the temporary GB zero rate (SI 2026/987) and the +explicit energy VAT base (#2166). + +From 1 October 2026 to 31 March 2027 domestic electricity in England, Wales +and Scotland is zero-rated; Northern Ireland and gas stay at 5%. Model year +2026 runs 6 April 2026 to 5 April 2027, so with equal daily use it has 183 +days at 5% and 182 at 0%. +""" + +from pathlib import Path + +import pytest +from policyengine_core.parameters import ParameterNode +from policyengine_core.reforms import Reform + +import policyengine_uk +from policyengine_uk import Simulation +from policyengine_uk.model_api import Scenario +from policyengine_uk.system import system + +FY2026_SHARE_AT_5_PERCENT = 183 / 365 +GB_RATE = "gov.hmrc.vat.domestic_electricity_rate.great_britain" + + +def raw_parameters() -> ParameterNode: + """Parameters as written, before uprating and fiscal-year conversion.""" + directory = Path(policyengine_uk.__file__).parent / "parameters" + return ParameterNode(directory_path=str(directory)) + + +def household( + region="LONDON", + electricity=1_050, + gas=0, + consumption=20_000, + year=2026, +): + return { + "people": {"adult": {"age": {year: 45}}}, + "benunits": {"benunit": {"members": ["adult"]}}, + "households": { + "household": { + "members": ["adult"], + "region": {year: region}, + "consumption": {year: consumption}, + "electricity_consumption": {year: electricity}, + "gas_consumption": {year: gas}, + } + }, + } + + +def calc(variable, year=2026, scenario=None, **kwargs): + sim = Simulation(situation=household(year=year, **kwargs), scenario=scenario) + return float(sim.calculate(variable, year)[0]) + + +@pytest.mark.parametrize( + "date, rate", + [ + ("2026-09-30", 0.05), + ("2026-10-01", 0.0), + ("2027-03-31", 0.0), + ("2027-04-01", 0.05), + ], +) +def test_gb_rate_changes_on_both_statutory_dates(date, rate): + rates = raw_parameters().gov.hmrc.vat.domestic_electricity_rate + assert rates.great_britain(date) == pytest.approx(rate) + assert rates.northern_ireland(date) == pytest.approx(0.05) + + +def test_fy2026_gb_rate_is_day_weighted(): + rates = system.parameters.gov.hmrc.vat.domestic_electricity_rate + assert rates.great_britain("2025") == pytest.approx(0.05) + assert rates.great_britain("2026") == pytest.approx( + 0.05 * FY2026_SHARE_AT_5_PERCENT + ) + assert rates.great_britain("2027") == pytest.approx(0.05) + assert rates.northern_ireland("2026") == pytest.approx(0.05) + + +@pytest.mark.parametrize("region", ["LONDON", "WALES", "SCOTLAND"]) +def test_gb_electricity_vat_in_fy2026(region): + # £1,050 of bills includes £50 of VAT at 5%. + assert calc("domestic_energy_vat", region=region) == pytest.approx( + 50 * FY2026_SHARE_AT_5_PERCENT + ) + + +def test_northern_ireland_keeps_5_percent(): + assert calc("domestic_energy_vat", region="NORTHERN_IRELAND") == pytest.approx(50) + + +def test_gas_is_unchanged(): + assert calc("domestic_energy_vat", electricity=0, gas=1_050) == pytest.approx(50) + + +@pytest.mark.parametrize("year", [2025, 2027]) +def test_years_either_side_are_at_5_percent(year): + assert calc("domestic_energy_vat", year=year) == pytest.approx(50) + + +def test_vat_responds_to_electricity(): + low = calc("vat", electricity=1_200) + high = calc("vat", electricity=5_000) + expected = 3_800 / 1.05 * 0.05 * FY2026_SHARE_AT_5_PERCENT + assert high - low == pytest.approx(expected) + + +def test_energy_vat_is_not_grossed_up_by_coverage(): + with_energy = calc("vat", electricity=1_050, gas=1_050, year=2025) + without_energy = calc("vat", electricity=0, gas=0, year=2025) + assert with_energy - without_energy == pytest.approx(100) + + +def test_domestic_fuel_and_power_leaves_the_generic_reduced_rate_base(): + # The ONS domestic fuel and power share of spending (2.79% in 2025) exceeds + # the OBR's 2.5% reduced-rate share, so nothing is left in the generic base. + assert calc("reduced_rate_vat_consumption", year=2025) == 0 + + +def test_non_energy_reduced_rate_base_is_kept(): + # A reduced-rate share above the domestic fuel and power share keeps the + # remainder in the generic base, at the reduced rate. + scenario = Scenario( + parameter_changes={"gov.hmrc.vat.reduced_rate_share": {"2025": 0.0379}} + ) + base = calc("reduced_rate_vat_consumption", year=2025, scenario=scenario) + assert base == pytest.approx(20_000 * (0.0379 - 0.0279)) + + +def test_no_reform_means_no_vat_change(): + assert calc("vat_change") == pytest.approx(0) + assert calc("baseline_domestic_energy_vat") == pytest.approx( + calc("domestic_energy_vat") + ) + + +def test_extending_the_zero_rate_to_fy2027(): + # A whole-year reform. A reform dated part-way through a year is not + # day-weighted on blended parameters (#2167). + reform = Reform.from_dict( + {GB_RATE: {"2027-01-01.2027-12-31": 0.0}}, country_id="uk" + ) + sim = Simulation(situation=household(year=2027), reform=reform) + assert sim.calculate("domestic_energy_vat", 2027)[0] == 0 + assert sim.calculate("baseline_domestic_energy_vat", 2027)[0] == pytest.approx(50) + assert sim.calculate("vat_change", 2027)[0] == pytest.approx(-50) + + +def test_reduced_rate_reform_reaches_gas_not_electricity(): + scenario = Scenario(parameter_changes={"gov.hmrc.vat.reduced_rate": {"2025": 0.0}}) + assert calc( + "domestic_energy_vat", year=2025, scenario=scenario, gas=1_050 + ) == pytest.approx(50) diff --git a/policyengine_uk/variables/gov/hmrc/baseline_vat.py b/policyengine_uk/variables/gov/hmrc/baseline_vat.py index 4c6f4589b4..164f372021 100644 --- a/policyengine_uk/variables/gov/hmrc/baseline_vat.py +++ b/policyengine_uk/variables/gov/hmrc/baseline_vat.py @@ -18,6 +18,10 @@ def formula(household, period, parameters): + reduced_rate_consumption * p.hmrc.vat.reduced_rate ) # Survey consumption under-records spending, and households bear only - # part of VAT liabilities, so household VAT is grossed up to receipts. + # part of VAT liabilities, so household VAT on recorded consumption is + # grossed up to receipts. VAT on the household's own energy bills is + # not grossed up: the bills are calibrated to national energy use and + # households bear that VAT in full. coverage = vat_grossing_factor(p.simulation) - return raw_vat / coverage + energy_vat = household("baseline_domestic_energy_vat", period) + return raw_vat / coverage + energy_vat diff --git a/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py b/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py new file mode 100644 index 0000000000..9d66d05b07 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py @@ -0,0 +1,76 @@ +from types import SimpleNamespace + +from policyengine_uk.model_api import * +from policyengine_uk.utils.parameters import fiscal_year_average + + +def _domestic_energy_vat(household, period, electricity_rate, gas_rate, priced_rate): + # electricity_consumption and gas_consumption are priced at Ofgem cap unit + # rates, which include VAT at the reduced rate, so the VAT-exclusive base is + # the bill divided by one plus that rate (5/105 of the bill at 5%). The + # base is held fixed when the rate changes. The inputs are calibrated to + # NEED consumption, not grossed up to receipts, so neither the survey + # consumption coverage nor the household share of VAT receipts applies. + country = household("country", period) + northern_ireland = country == country.possible_values.NORTHERN_IRELAND + rate = where( + northern_ireland, + electricity_rate.northern_ireland, + electricity_rate.great_britain, + ) + electricity = household("electricity_consumption", period) / (1 + priced_rate) + gas = household("gas_consumption", period) / (1 + priced_rate) + return electricity * rate + gas * gas_rate + + +class domestic_energy_vat(Variable): + label = "VAT on domestic electricity and gas" + documentation = ( + "VAT on the household's electricity and gas bills. Electricity is " + "charged at its own rate, which is zero in Great Britain from 1 October " + "2026 to 31 March 2027; gas is charged at the reduced rate." + ) + entity = Household + definition_period = YEAR + value_type = float + unit = GBP + reference = [ + "https://www.legislation.gov.uk/ukpga/1994/23/schedule/7A", + "https://www.legislation.gov.uk/uksi/2026/987/contents/made", + ] + + def formula(household, period, parameters): + p = parameters(period) + vat = p.gov.hmrc.vat + return _domestic_energy_vat( + household, + period, + vat.domestic_electricity_rate, + vat.reduced_rate, + p.baseline.gov.hmrc.vat.reduced_rate, + ) + + +class baseline_domestic_energy_vat(Variable): + label = "baseline VAT on domestic electricity and gas" + entity = Household + definition_period = YEAR + value_type = float + unit = GBP + + def formula(household, period, parameters): + # The baseline parameter tree keeps its dated values (it is not + # converted to fiscal years), so the electricity rates are + # day-weighted here as the reform tree's are, or a simulation with no + # reform would show a VAT change in 2026-27. + rates = household.simulation.tax_benefit_system.parameters.baseline.gov + rates = rates.hmrc.vat.domestic_electricity_rate + year = period.start.year + electricity_rate = SimpleNamespace( + great_britain=fiscal_year_average(rates.great_britain, year), + northern_ireland=fiscal_year_average(rates.northern_ireland, year), + ) + reduced_rate = parameters(period).baseline.gov.hmrc.vat.reduced_rate + return _domestic_energy_vat( + household, period, electricity_rate, reduced_rate, reduced_rate + ) diff --git a/policyengine_uk/variables/gov/hmrc/vat.py b/policyengine_uk/variables/gov/hmrc/vat.py index 70012df9c5..8a913749dd 100644 --- a/policyengine_uk/variables/gov/hmrc/vat.py +++ b/policyengine_uk/variables/gov/hmrc/vat.py @@ -18,6 +18,10 @@ def formula(household, period, parameters): + reduced_rate_consumption * p.hmrc.vat.reduced_rate ) # Survey consumption under-records spending, and households bear only - # part of VAT liabilities, so household VAT is grossed up to receipts. + # part of VAT liabilities, so household VAT on recorded consumption is + # grossed up to receipts. VAT on the household's own energy bills is + # not grossed up: the bills are calibrated to national energy use and + # households bear that VAT in full. coverage = vat_grossing_factor(p.simulation) - return raw_vat / coverage + energy_vat = household("domestic_energy_vat", period) + return raw_vat / coverage + energy_vat diff --git a/policyengine_uk/variables/household/consumption/reduced_rate_vat_consumption.py b/policyengine_uk/variables/household/consumption/reduced_rate_vat_consumption.py index a4e7b6b953..9719becd71 100644 --- a/policyengine_uk/variables/household/consumption/reduced_rate_vat_consumption.py +++ b/policyengine_uk/variables/household/consumption/reduced_rate_vat_consumption.py @@ -2,13 +2,16 @@ class reduced_rate_vat_consumption(Variable): - label = "consumption of VAT reduced-rated goods and services" + label = "consumption of VAT reduced-rated goods and services other than domestic fuel and power" entity = Household definition_period = YEAR value_type = float unit = "currency-GBP" def formula(household, period, parameters): + # Domestic electricity and gas are taxed from their own bills in + # domestic_energy_vat, so the domestic fuel and power share of spending + # comes out of the reduced-rate share here. vat = parameters(period).gov.hmrc.vat - consumption = household("consumption", period) - return consumption * vat.reduced_rate_share + share = max_(vat.reduced_rate_share - vat.domestic_fuel_and_power_share, 0) + return household("consumption", period) * share From f60f78d5ffb5faef37825115aadebe5ed9eb2b6d Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Thu, 8 Oct 2026 13:28:05 +0100 Subject: [PATCH 2/6] Derive VAT coverage net of energy-bill VAT on top of #2187 Energy-bill VAT sits outside both grossing factors from #2187, since the bills are calibrated to NEED and households bear that VAT in full. Re-derived the survey coverage against receipts net of energy VAT on the certified Microcosm UK 2024-25 release: 79.2 / (0.7 x (171.0 - 2.0)) = 0.670, which rounds to the existing 0.67. Records the derivation in the parameter and in docs/engineering/vat-coverage.md, and loosens a float32 difference check to 1p. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2166.fixed.md | 2 +- docs/engineering/vat-coverage.md | 41 +++++++++++++++---- .../vat/survey_consumption_coverage.yaml | 29 +++++++------ .../tests/test_domestic_electricity_vat.py | 3 +- 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/changelog.d/2166.fixed.md b/changelog.d/2166.fixed.md index 5fd8f33c3a..0421a42f9c 100644 --- a/changelog.d/2166.fixed.md +++ b/changelog.d/2166.fixed.md @@ -1 +1 @@ -Modelled the temporary zero rate of VAT on domestic electricity in Great Britain from 1 October 2026 to 31 March 2027 (SI 2026/987), with Northern Ireland and gas staying at 5%. VAT on domestic electricity and gas is now charged on the households' own bills (`domestic_energy_vat`), at a new nation-split `gov.hmrc.vat.domestic_electricity_rate` and the reduced rate, instead of on a flat share of total consumption grossed up by `microdata_vat_coverage`. The ONS domestic fuel and power share of spending now comes out of the reduced-rate share, which leaves at most 0.01% of spending in the generic reduced-rate base in any year since 2006. +Modelled the temporary zero rate of VAT on domestic electricity in Great Britain from 1 October 2026 to 31 March 2027 (SI 2026/987), with Northern Ireland and gas staying at 5%. VAT on domestic electricity and gas is now charged on the households' own bills (`domestic_energy_vat`), at a new nation-split `gov.hmrc.vat.domestic_electricity_rate` and the reduced rate, instead of on a flat share of total consumption grossed up to receipts. Energy-bill VAT is not grossed up by the survey coverage or the household share of VAT receipts. The ONS domestic fuel and power share of spending now comes out of the reduced-rate share, which leaves at most 0.01% of spending in the generic reduced-rate base in any year since 2006. diff --git a/docs/engineering/vat-coverage.md b/docs/engineering/vat-coverage.md index 49c467898a..ca89ffb6ce 100644 --- a/docs/engineering/vat-coverage.md +++ b/docs/engineering/vat-coverage.md @@ -146,11 +146,36 @@ For Enhanced FRS, download `enhanced_frs_2024_25.h5` from the model repository `policyengine/policyengine-uk-data-private` at the release tag (for example `revision="1.58.0"`). -## Interaction with an explicit energy VAT base - -A domestic fuel and power VAT base built from `electricity_consumption` and -`gas_consumption` (PolicyEngine/policyengine-uk#2166) changes what the survey -coverage applies to. If those energy inputs are already calibrated to an -aggregate, they should not be divided by the survey coverage again, and the -reconciled energy part of `reduced_rate_vat_consumption` should come out of the -base used to derive it. +## Energy bills are not grossed up + +VAT on domestic electricity and gas is charged on each household's own bills +(`domestic_energy_vat`, PolicyEngine/policyengine-uk#2166), and the ONS domestic +fuel and power share of spending comes out of the generic reduced-rate share. +Those bills are calibrated to national energy use (NEED), and households bear +VAT on them in full, so neither factor applies to them: + +``` +vat = (standard rate x full_rate_vat_consumption + + reduced rate x reduced_rate_vat_consumption) + / (survey_consumption_coverage x household_share_of_receipts) + + domestic_energy_vat +``` + +Dividing energy VAT by the household share as well would put part of +government, charity and exempt-business VAT on electricity bills, and a zero +rate on domestic electricity would then cost 1/0.7 times the VAT households +actually pay on it. + +With energy split out, the coverage is derived against receipts net of energy +VAT: coverage = pre-scaling non-energy VAT / (0.7 x (receipts - energy VAT)). + +| Year | Pre-scaling non-energy VAT | Energy-bill VAT | OBR VAT receipts | Survey coverage | +|---|---|---|---|---| +| 2024-25 | £79.2bn | £2.0bn | £171.0bn (outturn) | 0.670 | +| 2025-26 | £82.5bn | £2.0bn | £180.2bn | 0.662 | +| 2026-27 | £84.7bn | £1.4bn | £187.7bn | 0.650 | + +The 2024-25 value rounds to the same 0.67 as the derivation above, so the +parameter is unchanged. To reproduce, subtract +`sim.calculate("domestic_energy_vat", year).sum()` from the receipts in the +snippet above before dividing. diff --git a/policyengine_uk/parameters/gov/simulation/vat/survey_consumption_coverage.yaml b/policyengine_uk/parameters/gov/simulation/vat/survey_consumption_coverage.yaml index e5a16038db..0cad4f8287 100644 --- a/policyengine_uk/parameters/gov/simulation/vat/survey_consumption_coverage.yaml +++ b/policyengine_uk/parameters/gov/simulation/vat/survey_consumption_coverage.yaml @@ -7,19 +7,22 @@ metadata: Calibrated to the certified Microcosm UK 2024-25 national release only (policyengine/populace-uk-private, tag microcosm-uk-2024-25-national-20261002T230158Z-5c6b3f68, revision - f9d1922c). Pre-scaling VAT (standard rate times full_rate_vat_consumption - plus reduced rate times reduced_rate_vat_consumption) on that release is - 0.20 x £396.15bn + 0.05 x £25.02bn = £80.48bn in 2024. OBR VAT receipts - for 2024-25 (outturn, March 2026 EFO table 3.8, cash basis) are £170.994bn. - 80.48 / 170.994 = 0.471 for the product of the two factors; divided by the - household share of 0.7 gives 0.67. Across 2024-25 to 2026-27 the product is - 0.459 to 0.471, and the HMRC ready reckoner (£8.8bn for one point on the - standard rate in 2026-27) implies 0.481, so the coverage lies between 0.655 - and 0.688. On Enhanced FRS 2024-25 the same calculation gives a coverage of - 0.927 (policyengine-uk-data 1.56.16) or 0.967 (1.58.0), so this value puts - VAT on that dataset 38% or 44% above 2024-25 receipts. Re-derive when the - dataset it should match changes, with docs/engineering/vat-coverage.md, - which lists the inputs, file hashes and per-dataset results. + f9d1922c). VAT on household energy bills (domestic_energy_vat) is not + grossed up, so the coverage applies only to VAT on other recorded + consumption. On that release, pre-scaling VAT on that consumption is 0.20 x + £396.15bn = £79.23bn in 2024 (the generic reduced-rate base is empty once + domestic fuel and power is taken out), and VAT on energy bills is £1.99bn. + OBR VAT receipts for 2024-25 (outturn, March 2026 EFO table 3.8, cash + basis) are £170.994bn. Coverage = 79.23 / (0.7 x (170.994 - 1.99)) = 0.670. + Before energy bills were split out, the same calculation gave 0.672, so + the rounded value is unchanged. Across 2024-25 to 2026-27 the coverage is + 0.650 to 0.670, and the HMRC ready reckoner (£8.8bn for one point on the + standard rate in 2026-27) implies 0.688. On Enhanced FRS 2024-25 the same + calculation gives 0.927 (policyengine-uk-data 1.56.16) or 0.967 (1.58.0), + so this value puts total VAT on that dataset 38% or 44% above 2024-25 + receipts. Re-derive when the dataset it should match changes, with + docs/engineering/vat-coverage.md, which lists the inputs, file hashes and + per-dataset results. reference: - title: OBR, Economic and fiscal outlook March 2026, detailed forecast tables, receipts, table 3.8 href: https://obr.uk/efo/economic-and-fiscal-outlook-march-2026/ diff --git a/policyengine_uk/tests/test_domestic_electricity_vat.py b/policyengine_uk/tests/test_domestic_electricity_vat.py index e3217a7dda..88b7166b98 100644 --- a/policyengine_uk/tests/test_domestic_electricity_vat.py +++ b/policyengine_uk/tests/test_domestic_electricity_vat.py @@ -106,7 +106,8 @@ def test_vat_responds_to_electricity(): low = calc("vat", electricity=1_200) high = calc("vat", electricity=5_000) expected = 3_800 / 1.05 * 0.05 * FY2026_SHARE_AT_5_PERCENT - assert high - low == pytest.approx(expected) + # vat is float32, so the difference of two totals is good to about 1p. + assert high - low == pytest.approx(expected, abs=0.01) def test_energy_vat_is_not_grossed_up_by_coverage(): From bcde58d3fdc3c23d109359dff26cfc55b764eff8 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Fri, 9 Oct 2026 18:23:39 +0100 Subject: [PATCH 3/6] Price energy-bill VAT on a fixed 5% input basis (review C1, A1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses juaristi22's review of #2189. C1: domestic_energy_vat divided the electricity and gas inputs by the baseline-tree reduced rate. Scenario(parameter_changes=...) clones that tree after applying the reform (#2188), so a 20% gas rate treated 5%-priced bills as if they included 20% (£218.75 instead of £250 on £1,050 + £1,050). The divisor is now a fixed price-basis parameter, gov.simulation.vat.energy_input_price_basis_rate = 0.05 (Ofgem Q2 2026 unit rates include 5% VAT), in both domestic_energy_vat and baseline_domestic_energy_vat. Scenario and Reform.from_dict now agree. Tests: her £250 case through both routes, a scalar zero gas rate leaving electricity VAT at £50, and the FY2026 GB window/NI/gas cases with gas. A1 (standing charges, other fuels) is not changed here: ONS electricity plus gas alone is 2.71% of spending in 2025 (2.74% in 2024), above the 2.5% reduced-rate share, so a narrower subtraction is a no-op; complete bills belong in the input data. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2166.fixed.md | 2 +- docs/engineering/vat-coverage.md | 6 ++ .../vat/energy_input_price_basis_rate.yaml | 20 +++++ .../tests/test_domestic_electricity_vat.py | 75 +++++++++++++++++++ .../variables/gov/hmrc/domestic_energy_vat.py | 19 +++-- 5 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml diff --git a/changelog.d/2166.fixed.md b/changelog.d/2166.fixed.md index 0421a42f9c..f7872663f5 100644 --- a/changelog.d/2166.fixed.md +++ b/changelog.d/2166.fixed.md @@ -1 +1 @@ -Modelled the temporary zero rate of VAT on domestic electricity in Great Britain from 1 October 2026 to 31 March 2027 (SI 2026/987), with Northern Ireland and gas staying at 5%. VAT on domestic electricity and gas is now charged on the households' own bills (`domestic_energy_vat`), at a new nation-split `gov.hmrc.vat.domestic_electricity_rate` and the reduced rate, instead of on a flat share of total consumption grossed up to receipts. Energy-bill VAT is not grossed up by the survey coverage or the household share of VAT receipts. The ONS domestic fuel and power share of spending now comes out of the reduced-rate share, which leaves at most 0.01% of spending in the generic reduced-rate base in any year since 2006. +Modelled the temporary zero rate of VAT on domestic electricity in Great Britain from 1 October 2026 to 31 March 2027 (SI 2026/987), with Northern Ireland and gas staying at 5%. VAT on domestic electricity and gas is now charged on the households' own bills (`domestic_energy_vat`), at a new nation-split `gov.hmrc.vat.domestic_electricity_rate` and the reduced rate, instead of on a flat share of total consumption grossed up to receipts. Energy-bill VAT is not grossed up by the survey coverage or the household share of VAT receipts. The ONS domestic fuel and power share of spending now comes out of the reduced-rate share, which leaves at most 0.01% of spending in the generic reduced-rate base in any year since 2006. The bills are taken net of the 5% VAT they were priced at (`gov.simulation.vat.energy_input_price_basis_rate`), so a reform to the reduced rate no longer changes the VAT-exclusive energy base. diff --git a/docs/engineering/vat-coverage.md b/docs/engineering/vat-coverage.md index ca89ffb6ce..41bee4fbbd 100644 --- a/docs/engineering/vat-coverage.md +++ b/docs/engineering/vat-coverage.md @@ -161,6 +161,12 @@ vat = (standard rate x full_rate_vat_consumption + domestic_energy_vat ``` +The electricity and gas inputs are priced at Ofgem cap unit rates that +include VAT at 5%, so `domestic_energy_vat` charges the statutory rates on the +input divided by 1.05. The divisor is the fixed price-basis parameter +`gov.simulation.vat.energy_input_price_basis_rate`, not the statutory reduced +rate, so a reform to a VAT rate leaves the VAT-exclusive base unchanged. + Dividing energy VAT by the household share as well would put part of government, charity and exempt-business VAT on electricity bills, and a zero rate on domestic electricity would then cost 1/0.7 times the VAT households diff --git a/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml b/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml new file mode 100644 index 0000000000..8eea9a8a00 --- /dev/null +++ b/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml @@ -0,0 +1,20 @@ +description: VAT rate included in the prices used to value the electricity_consumption and gas_consumption inputs. Energy-bill VAT is charged on those inputs net of this rate, so the VAT-exclusive base stays fixed when a reform changes a statutory VAT rate. This describes the input data, not policy; change it only if the inputs are repriced on a different VAT basis. +metadata: + unit: /1 + period: year + economy: false + label: VAT rate included in energy input prices + derivation: >- + The default electricity and gas inputs are NEED kWh consumption valued at + Ofgem price cap unit rates for 1 April to 30 June 2026, which Ofgem quotes + including VAT at 5%. The VAT-exclusive base is therefore the input divided + by 1.05. Reading the statutory reduced rate here instead would make a + reform to that rate reprice the inputs (PolicyEngine/policyengine-uk#2189, + review finding C1). + reference: + - title: Ofgem, Changes to energy price cap between 1 April and 30 June 2026 (unit rates and standing charges include VAT) + href: https://www.ofgem.gov.uk/news/changes-energy-price-cap-between-1-april-and-30-june-2026 + - title: VAT Act 1994, Schedule 7A, Group 1 (reduced rate on domestic fuel and power) + href: https://www.legislation.gov.uk/ukpga/1994/23/schedule/7A +values: + 2000-01-01: 0.05 diff --git a/policyengine_uk/tests/test_domestic_electricity_vat.py b/policyengine_uk/tests/test_domestic_electricity_vat.py index 88b7166b98..1eb8519574 100644 --- a/policyengine_uk/tests/test_domestic_electricity_vat.py +++ b/policyengine_uk/tests/test_domestic_electricity_vat.py @@ -156,3 +156,78 @@ def test_reduced_rate_reform_reaches_gas_not_electricity(): assert calc( "domestic_energy_vat", year=2025, scenario=scenario, gas=1_050 ) == pytest.approx(50) + + +# Price basis of the energy inputs (#2189 review, finding C1). The inputs are +# priced at Ofgem cap unit rates including 5% VAT, so the VAT-exclusive base +# is the bill / 1.05 whatever rate a reform sets. Previously the divisor was +# the baseline-tree reduced rate, which a Scenario reform also changes (#2188), +# so a 20% gas rate treated 5%-priced bills as if they included 20%. +HER_CASE = dict(region="LONDON", electricity=1_050, gas=1_050, year=2025) +GAS_RATE = "gov.hmrc.vat.reduced_rate" + + +def test_price_basis_rate_is_5_percent(): + p = system.parameters.gov.simulation.vat + for year in ("2024", "2025", "2026", "2027"): + assert p.energy_input_price_basis_rate(year) == pytest.approx(0.05) + + +def test_gas_rate_reform_scenario_and_reform_agree_at_250(): + # Model year 2025, London, £1,050 of electricity and £1,050 of gas, both + # priced including 5% VAT. VAT-exclusive base: 1,050 / 1.05 = £1,000 each. + # electricity: 1,000 x 5% = £50 + # gas: 1,000 x 20% = £200 + # total = £250 + # Before the fix the Scenario route divided by 1.20 instead: + # 1,050 / 1.20 x 5% + 1,050 / 1.20 x 20% = 43.75 + 175 = £218.75. + scenario = Scenario(parameter_changes={GAS_RATE: 0.20}) + reform = Reform.from_dict( + {GAS_RATE: {"2025-01-01.2100-12-31": 0.20}}, country_id="uk" + ) + via_scenario = Simulation(situation=household(**HER_CASE), scenario=scenario) + via_reform = Simulation(situation=household(**HER_CASE), reform=reform) + scenario_vat = float(via_scenario.calculate("domestic_energy_vat", 2025)[0]) + reform_vat = float(via_reform.calculate("domestic_energy_vat", 2025)[0]) + assert scenario_vat == pytest.approx(250, abs=0.01) + assert reform_vat == pytest.approx(250, abs=0.01) + assert scenario_vat == pytest.approx(reform_vat, abs=0.01) + # Total vat differs only through energy, so it matches across routes too. + assert float(via_scenario.calculate("vat", 2025)[0]) == pytest.approx( + float(via_reform.calculate("vat", 2025)[0]), abs=0.01 + ) + # Reform.from_dict leaves the baseline tree alone: 1,000 x 5% x 2 = £100. + # (Under Scenario the baseline tree is still contaminated, #2188.) + assert float( + via_reform.calculate("baseline_domestic_energy_vat", 2025)[0] + ) == pytest.approx(100, abs=0.01) + + +def test_scalar_zero_gas_rate_leaves_electricity_vat_alone(): + # £1,050 of electricity only: 1,050 / 1.05 x 5% = £50. A scalar Scenario + # setting the reduced rate to 0 used to give 1,050 / 1.00 x 5% = £52.50. + scenario = Scenario(parameter_changes={GAS_RATE: 0.0}) + assert calc( + "domestic_energy_vat", year=2025, gas=0, scenario=scenario + ) == pytest.approx(50, abs=0.01) + + +@pytest.mark.parametrize( + "region, electricity_rate", + [ + # GB, model year 2026: 183 of 365 days at 5%, so 0.05 x 183 / 365. + ("LONDON", 0.05 * FY2026_SHARE_AT_5_PERCENT), + ("SCOTLAND", 0.05 * FY2026_SHARE_AT_5_PERCENT), + # Northern Ireland is outside the zero rate: 5% all year. + ("NORTHERN_IRELAND", 0.05), + ], +) +def test_baseline_fy2026_window_by_nation_with_gas(region, electricity_rate): + # £1,050 of electricity and £2,100 of gas, each VAT-exclusive at / 1.05: + # electricity: 1,000 x rate (GB 25.07, NI 50) + # gas: 2,000 x 5% = £100 in every nation + # so GB = 125.07 and NI = 150. + vat = calc("domestic_energy_vat", region=region, electricity=1_050, gas=2_100) + assert vat == pytest.approx(1_000 * electricity_rate + 100, abs=0.01) + gas_only = calc("domestic_energy_vat", region=region, electricity=0, gas=2_100) + assert gas_only == pytest.approx(100, abs=0.01) diff --git a/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py b/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py index 9d66d05b07..47a0587fbf 100644 --- a/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py +++ b/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py @@ -4,13 +4,16 @@ from policyengine_uk.utils.parameters import fiscal_year_average -def _domestic_energy_vat(household, period, electricity_rate, gas_rate, priced_rate): +def _domestic_energy_vat(household, period, parameters, electricity_rate, gas_rate): # electricity_consumption and gas_consumption are priced at Ofgem cap unit - # rates, which include VAT at the reduced rate, so the VAT-exclusive base is - # the bill divided by one plus that rate (5/105 of the bill at 5%). The - # base is held fixed when the rate changes. The inputs are calibrated to - # NEED consumption, not grossed up to receipts, so neither the survey - # consumption coverage nor the household share of VAT receipts applies. + # rates, which include VAT at 5%, so the VAT-exclusive base is the bill + # divided by 1.05 (5/105 of the bill is VAT). The divisor is the rate the + # inputs were priced at (gov.simulation.vat.energy_input_price_basis_rate), + # never the statutory rate being modelled, so the base stays fixed when a + # reform changes a VAT rate. The inputs are calibrated to NEED consumption, + # not grossed up to receipts, so neither the survey consumption coverage + # nor the household share of VAT receipts applies. + priced_rate = parameters(period).gov.simulation.vat.energy_input_price_basis_rate country = household("country", period) northern_ireland = country == country.possible_values.NORTHERN_IRELAND rate = where( @@ -45,9 +48,9 @@ def formula(household, period, parameters): return _domestic_energy_vat( household, period, + parameters, vat.domestic_electricity_rate, vat.reduced_rate, - p.baseline.gov.hmrc.vat.reduced_rate, ) @@ -72,5 +75,5 @@ def formula(household, period, parameters): ) reduced_rate = parameters(period).baseline.gov.hmrc.vat.reduced_rate return _domestic_energy_vat( - household, period, electricity_rate, reduced_rate, reduced_rate + household, period, parameters, electricity_rate, reduced_rate ) From c26853f03487f4648a919c84bf1073859f19890f Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 12 Oct 2026 08:24:48 +0200 Subject: [PATCH 4/6] Cite DESNZ Quarterly Energy Prices as the energy input price basis Microcosm prices electricity_consumption and gas_consumption at DESNZ QEP average domestic prices paid (tables 2.2.4 and 2.3.4, all payment methods, 2024-25, including VAT), not Ofgem cap unit rates. The 5% price-basis rate is unchanged. Co-Authored-By: Claude Opus 5.5 --- docs/engineering/vat-coverage.md | 4 ++-- .../simulation/vat/energy_input_price_basis_rate.yaml | 9 +++++---- policyengine_uk/tests/test_domestic_electricity_vat.py | 2 +- .../variables/gov/hmrc/domestic_energy_vat.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/engineering/vat-coverage.md b/docs/engineering/vat-coverage.md index 41bee4fbbd..94384c460d 100644 --- a/docs/engineering/vat-coverage.md +++ b/docs/engineering/vat-coverage.md @@ -161,8 +161,8 @@ vat = (standard rate x full_rate_vat_consumption + domestic_energy_vat ``` -The electricity and gas inputs are priced at Ofgem cap unit rates that -include VAT at 5%, so `domestic_energy_vat` charges the statutory rates on the +The electricity and gas inputs are priced at DESNZ Quarterly Energy Prices +average domestic prices paid, which include VAT at 5%, so `domestic_energy_vat` charges the statutory rates on the input divided by 1.05. The divisor is the fixed price-basis parameter `gov.simulation.vat.energy_input_price_basis_rate`, not the statutory reduced rate, so a reform to a VAT rate leaves the VAT-exclusive base unchanged. diff --git a/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml b/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml index 8eea9a8a00..a8dd1ce790 100644 --- a/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml +++ b/policyengine_uk/parameters/gov/simulation/vat/energy_input_price_basis_rate.yaml @@ -6,14 +6,15 @@ metadata: label: VAT rate included in energy input prices derivation: >- The default electricity and gas inputs are NEED kWh consumption valued at - Ofgem price cap unit rates for 1 April to 30 June 2026, which Ofgem quotes - including VAT at 5%. The VAT-exclusive base is therefore the input divided + DESNZ Quarterly Energy Prices average domestic prices paid (standard + electricity table 2.2.4 and gas table 2.3.4, all payment methods, 2024-25), + which include VAT at 5%. The VAT-exclusive base is therefore the input divided by 1.05. Reading the statutory reduced rate here instead would make a reform to that rate reprice the inputs (PolicyEngine/policyengine-uk#2189, review finding C1). reference: - - title: Ofgem, Changes to energy price cap between 1 April and 30 June 2026 (unit rates and standing charges include VAT) - href: https://www.ofgem.gov.uk/news/changes-energy-price-cap-between-1-april-and-30-june-2026 + - title: DESNZ, Annual domestic energy price statistics (Quarterly Energy Prices tables 2.2.4 and 2.3.4, prices include VAT) + href: https://www.gov.uk/government/statistical-data-sets/annual-domestic-energy-price-statistics - title: VAT Act 1994, Schedule 7A, Group 1 (reduced rate on domestic fuel and power) href: https://www.legislation.gov.uk/ukpga/1994/23/schedule/7A values: diff --git a/policyengine_uk/tests/test_domestic_electricity_vat.py b/policyengine_uk/tests/test_domestic_electricity_vat.py index 1eb8519574..b7d5a1a83a 100644 --- a/policyengine_uk/tests/test_domestic_electricity_vat.py +++ b/policyengine_uk/tests/test_domestic_electricity_vat.py @@ -159,7 +159,7 @@ def test_reduced_rate_reform_reaches_gas_not_electricity(): # Price basis of the energy inputs (#2189 review, finding C1). The inputs are -# priced at Ofgem cap unit rates including 5% VAT, so the VAT-exclusive base +# priced at DESNZ QEP average prices paid including 5% VAT, so the VAT-exclusive base # is the bill / 1.05 whatever rate a reform sets. Previously the divisor was # the baseline-tree reduced rate, which a Scenario reform also changes (#2188), # so a 20% gas rate treated 5%-priced bills as if they included 20%. diff --git a/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py b/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py index 47a0587fbf..663b1ca3e6 100644 --- a/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py +++ b/policyengine_uk/variables/gov/hmrc/domestic_energy_vat.py @@ -5,8 +5,8 @@ def _domestic_energy_vat(household, period, parameters, electricity_rate, gas_rate): - # electricity_consumption and gas_consumption are priced at Ofgem cap unit - # rates, which include VAT at 5%, so the VAT-exclusive base is the bill + # electricity_consumption and gas_consumption are priced at DESNZ Quarterly + # Energy Prices average prices paid, which include VAT at 5%, so the VAT-exclusive base is the bill # divided by 1.05 (5/105 of the bill is VAT). The divisor is the rate the # inputs were priced at (gov.simulation.vat.energy_input_price_basis_rate), # never the statutory rate being modelled, so the base stays fixed when a From 968c122b5554075074ce95ae367776e618f39aa9 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 12 Oct 2026 11:10:12 +0200 Subject: [PATCH 5/6] Date the FY2027 zero-rate test over the model year now that main day-weights reforms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main (3edfb4b99, fixing #2167 and #2188) applies dated reforms on the statutory timeline. A calendar-2027 zero rate now leaves 1 January to 5 April 2028 at 5%, so the test dates the reform 6 April 2027 to 5 April 2028. The Scenario route also keeps the baseline tree now, so the price basis test asserts the £100 baseline on both routes. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_domestic_electricity_vat.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/policyengine_uk/tests/test_domestic_electricity_vat.py b/policyengine_uk/tests/test_domestic_electricity_vat.py index b7d5a1a83a..8d91bf08e4 100644 --- a/policyengine_uk/tests/test_domestic_electricity_vat.py +++ b/policyengine_uk/tests/test_domestic_electricity_vat.py @@ -140,10 +140,12 @@ def test_no_reform_means_no_vat_change(): def test_extending_the_zero_rate_to_fy2027(): - # A whole-year reform. A reform dated part-way through a year is not - # day-weighted on blended parameters (#2167). + # Dated reforms are applied on the statutory timeline and day-weighted + # into the model year (#2167), so the zero rate must cover all of model + # year 2027: 6 April 2027 to 5 April 2028. A calendar-2027 window would + # leave 96 of 366 days at 5%. reform = Reform.from_dict( - {GB_RATE: {"2027-01-01.2027-12-31": 0.0}}, country_id="uk" + {GB_RATE: {"2027-04-06.2028-04-05": 0.0}}, country_id="uk" ) sim = Simulation(situation=household(year=2027), reform=reform) assert sim.calculate("domestic_energy_vat", 2027)[0] == 0 @@ -161,7 +163,7 @@ def test_reduced_rate_reform_reaches_gas_not_electricity(): # Price basis of the energy inputs (#2189 review, finding C1). The inputs are # priced at DESNZ QEP average prices paid including 5% VAT, so the VAT-exclusive base # is the bill / 1.05 whatever rate a reform sets. Previously the divisor was -# the baseline-tree reduced rate, which a Scenario reform also changes (#2188), +# the baseline-tree reduced rate, which a Scenario reform also changed (#2188), # so a 20% gas rate treated 5%-priced bills as if they included 20%. HER_CASE = dict(region="LONDON", electricity=1_050, gas=1_050, year=2025) GAS_RATE = "gov.hmrc.vat.reduced_rate" @@ -196,11 +198,11 @@ def test_gas_rate_reform_scenario_and_reform_agree_at_250(): assert float(via_scenario.calculate("vat", 2025)[0]) == pytest.approx( float(via_reform.calculate("vat", 2025)[0]), abs=0.01 ) - # Reform.from_dict leaves the baseline tree alone: 1,000 x 5% x 2 = £100. - # (Under Scenario the baseline tree is still contaminated, #2188.) - assert float( - via_reform.calculate("baseline_domestic_energy_vat", 2025)[0] - ) == pytest.approx(100, abs=0.01) + # Both routes leave the baseline tree alone (#2188): 1,000 x 5% x 2 = £100. + for sim in (via_scenario, via_reform): + assert float( + sim.calculate("baseline_domestic_energy_vat", 2025)[0] + ) == pytest.approx(100, abs=0.01) def test_scalar_zero_gas_rate_leaves_electricity_vat_alone(): From ebddf75f5c77e16e4041258c49e2a76d74691993 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 12 Oct 2026 14:53:00 +0200 Subject: [PATCH 6/6] Give the energy-net coverage derivation per dataset and release Add the Enhanced FRS rows (policyengine-uk-data 1.56.16 and 1.58.0) to the energy-net coverage table, state that the bound from #2187 still holds, and say which version the #2187 derivation table was computed on. Co-Authored-By: Claude Opus 5.5 --- docs/engineering/vat-coverage.md | 35 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/engineering/vat-coverage.md b/docs/engineering/vat-coverage.md index 94384c460d..946d877607 100644 --- a/docs/engineering/vat-coverage.md +++ b/docs/engineering/vat-coverage.md @@ -56,8 +56,8 @@ the calculation below, whenever the dataset it should match changes. | Statutory rates | 20% standard, 5% reduced | `gov.hmrc.vat` | Model year 2024 is the 2024-25 fiscal year. All dataset totals are weighted -sums from `Microsimulation`, computed with policyengine-uk at this branch and -policyengine-core 3.33.2. +sums from `Microsimulation`, computed with policyengine-uk as of #2187 (before +energy bills were split out, see below) and policyengine-core 3.33.2. ### Formula @@ -175,13 +175,24 @@ actually pay on it. With energy split out, the coverage is derived against receipts net of energy VAT: coverage = pre-scaling non-energy VAT / (0.7 x (receipts - energy VAT)). -| Year | Pre-scaling non-energy VAT | Energy-bill VAT | OBR VAT receipts | Survey coverage | -|---|---|---|---|---| -| 2024-25 | £79.2bn | £2.0bn | £171.0bn (outturn) | 0.670 | -| 2025-26 | £82.5bn | £2.0bn | £180.2bn | 0.662 | -| 2026-27 | £84.7bn | £1.4bn | £187.7bn | 0.650 | - -The 2024-25 value rounds to the same 0.67 as the derivation above, so the -parameter is unchanged. To reproduce, subtract -`sim.calculate("domestic_energy_vat", year).sum()` from the receipts in the -snippet above before dividing. +On the same inputs as the derivation above: + +| Dataset | Year | Pre-scaling non-energy VAT | Energy-bill VAT | OBR VAT receipts | Survey coverage | +|---|---|---|---|---|---| +| Microcosm UK 2024-25 (`f9d1922c`) | 2024-25 | £79.23bn | £1.99bn | £171.0bn (outturn) | 0.670 | +| Microcosm UK 2024-25 (`f9d1922c`) | 2025-26 | £82.51bn | £2.01bn | £180.2bn | 0.662 | +| Microcosm UK 2024-25 (`f9d1922c`) | 2026-27 | £84.73bn | £1.40bn | £187.7bn | 0.650 | +| Enhanced FRS 2024-25, uk-data 1.56.16 | 2024-25 | £109.39bn | £2.35bn | £171.0bn (outturn) | 0.927 | +| Enhanced FRS 2024-25, uk-data 1.58.0 | 2024-25 | £114.06bn | £2.44bn | £171.0bn (outturn) | 0.967 | + +The pre-scaling non-energy VAT is the standard-rated part alone, since the +generic reduced-rate base is empty once domestic fuel and power is taken out. +The Microcosm 2024-25 value rounds to the same 0.67 as the derivation above, +so the parameter is unchanged, and the bound above still holds (0.650 to 0.670 +by receipts, 0.688 by the ready reckoner). With it, total `vat` on Microcosm +is within 0.1% of 2024-25 receipts and 3.0% below in 2026-27; on Enhanced FRS +it is 38% (1.56.16) or 44% (1.58.0) above 2024-25 receipts. + +To reproduce, run the snippet above on this version and subtract +`sim.calculate("domestic_energy_vat", year).sum()` from the receipts before +dividing.