Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/corporate-sdlt-once.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Count corporate SDLT once in `household_tax`, `gov_tax` and `pre_budget_change_household_tax`. `expected_sdlt` no longer adds `corporate_sdlt`, which those totals already list. `gov.hmrc.stamp_duty.abolish` now zeroes `corporate_sdlt` and the `stamp_duty_land_tax` liability (no household is `sdlt_liable` once the tax is abolished) as well as `expected_sdlt`, so abolishing SDLT removes all of it. `expected_sdlt` also no longer adds `property_sale_rate` (a 0.045 share) to every household as £0.045 of SDLT.
1 change: 1 addition & 0 deletions changelog.d/fiscal-partition.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Give corporate SDLT its own head in `policyengine_uk/data/fiscal_partition.yaml` now that the totals count it once, and empty `model_double_counts`. Each tax head now names its row of the OBR current receipts table (EFO November 2025, Table A.5) and, where the March 2026 EFO's Table 3.1 footnotes place it, that table's line. The file also lists the receipt rows the model does not cover, more exclusions (tax assessments, wage-change measures, income tax reductions) and more overlap guards. New tests check on simulated households that each tax total is the sum of its heads and counts each head once.
259 changes: 202 additions & 57 deletions policyengine_uk/data/fiscal_partition.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -205,9 +205,9 @@ def numeric_lists(node):


def test_geographic_guards_hold_once_declared_double_counts_are_netted(measured):
"""Council tax and domestic rates never meet; nor do SDLT, LTT and LBTT
once corporate SDLT, which expected_sdlt carries while #2159 is open, is
netted out of it as a declared double count."""
"""Council tax and domestic rates never meet; nor do SDLT, LTT and LBTT.
Since #2159 expected_sdlt carries no corporate SDLT, so nothing needs
netting here; a declared double count would be netted out of its head."""
result, _, _, _ = measured
status = {c["id"]: c["status"] for c in result["checks"]}
assert status["overlap_guard:local_tax_geography"] == "pass"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ def calculate(self, variable, period, map_to=None, unweighted=True):
return np.asarray(self.values.get(variable, np.zeros(self.n)), dtype=float)


def _measured_year(values):
def _measured_year(values, partition=None):
from policyengine_uk.utils.aggregate_impact.measure import measure_year
from policyengine_uk.utils.aggregate_impact.partition import (
default_partition_path,
Expand All @@ -557,7 +557,7 @@ def _measured_year(values):
rec.scope = "2026"
measure_year(
StubSim(values),
load_partition(default_partition_path()),
partition or load_partition(default_partition_path()),
load_spec(),
2026,
rec,
Expand All @@ -566,6 +566,45 @@ def _measured_year(values):
return rec, households


def _corporate_sdlt_double_counted():
"""The shipped partition in its shape before the #2159 fix: expected_sdlt
carries corporate SDLT, declared as a double count rather than a head.
The fix left the model with no double counts, so the tests of how a
declared one is measured use this."""
import yaml

from policyengine_uk.utils.aggregate_impact.partition import (
default_partition_path,
parse_partition,
)

with open(default_partition_path()) as f:
raw = yaml.safe_load(f)
totals = next(h for h in raw["tax_heads"] if h["id"] == "sdlt_corporate")["totals"]
raw["tax_heads"] = [h for h in raw["tax_heads"] if h["id"] != "sdlt_corporate"]

def repoint(ids):
return list(
dict.fromkeys("sdlt_household" if i == "sdlt_corporate" else i for i in ids)
)

for exclusion in raw["exclusions"]:
if exclusion.get("spans"):
exclusion["spans"] = repoint(exclusion["spans"])
if exclusion.get("head") == "sdlt_corporate":
exclusion["head"] = "sdlt_household"
for guard in raw["overlap_guards"]:
guard["heads"] = repoint(guard.get("heads") or [])
raw["model_double_counts"] = [
{
"variable": "corporate_sdlt",
"contained_in": "expected_sdlt",
"totals": totals,
}
]
return parse_partition(raw)


def _first(n, value):
out = np.zeros(N)
out[:n] = value
Expand All @@ -589,7 +628,8 @@ def test_a_double_count_from_few_records_is_suppressed_and_left_out():
"household_tax": model_tax,
"pre_budget_change_household_tax": model_tax,
"gov_balance": model_tax,
}
},
_corporate_sdlt_double_counted(),
)
published = _published(rec)
assert published["double_counts.corporate_sdlt"] is None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
main_residential_property_purchased: 0
council_tax: 0
household_owns_tv: false
gov.hmrc.stamp_duty.property_sale_rate: 0
output:
high_value_council_tax_surcharge: 0
- name: A 2028 value just above the first band on the annual 2026 HPI basis is charged
Expand All @@ -30,7 +29,6 @@
main_residential_property_purchased: 0
council_tax: 0
household_owns_tv: false
gov.hmrc.stamp_duty.property_sale_rate: 0
output:
high_value_council_tax_surcharge: 2_500
household_tax: 2_500
Expand All @@ -45,7 +43,6 @@
main_residential_property_purchased: 0
council_tax: 0
household_owns_tv: false
gov.hmrc.stamp_duty.property_sale_rate: 0
output:
high_value_council_tax_surcharge: 2_550
household_tax: 2_550
Expand All @@ -59,7 +56,6 @@
main_residential_property_purchased: 0
council_tax: 0
household_owns_tv: false
gov.hmrc.stamp_duty.property_sale_rate: 0
output:
high_value_council_tax_surcharge: 0
household_tax: 0
Expand All @@ -74,7 +70,6 @@
main_residential_property_purchased: 0
council_tax: 1_500
household_owns_tv: false
gov.hmrc.stamp_duty.property_sale_rate: 0
output:
high_value_council_tax_surcharge: 2_500
household_tax: 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Corporate SDLT is shareholding x (residential + non-residential corporate
# SDLT revenue) = shareholding x (£705m + £2,425m) = shareholding x £3,130m
# (gov.hmrc.stamp_duty.statistics, 2020 values held flat). A shareholding of
# 1e-6 gives £3,130 and 3e-6 gives £9,390. business_rates is set to 0 so the
# totals isolate SDLT. A £500,000 purchase that is not a first home pays
# £15,000 of SDLT (see expected_sdlt.yaml).
- name: Corporate SDLT enters household and government taxes once
period: 2026
absolute_error_margin: 0.01
input:
region: LONDON
shareholding: 0.000001
business_rates: 0
council_tax: 0
household_owns_tv: false
output:
corporate_sdlt: 3_130
expected_sdlt: 0
corporate_tax_incidence: 3_130
household_tax: 3_130
gov_tax: 3_130
pre_budget_change_household_tax: 3_130
- name: Household and corporate SDLT each enter the totals once
period: 2026
absolute_error_margin: 0.01
input:
region: LONDON
main_residential_property_purchased: 500_000
main_residential_property_purchased_is_first_home: false
shareholding: 0.000001
business_rates: 0
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: 15_000
corporate_sdlt: 3_130
household_tax: 18_130
gov_tax: 18_130
pre_budget_change_household_tax: 18_130
- name: Abolishing SDLT removes household and corporate SDLT from the totals
period: 2026
absolute_error_margin: 0.01
input:
gov.hmrc.stamp_duty.abolish: true
region: LONDON
main_residential_property_purchased: 500_000
main_residential_property_purchased_is_first_home: false
shareholding: 0.000001
business_rates: 0
council_tax: 0
household_owns_tv: false
output:
stamp_duty_land_tax: 0
expected_sdlt: 0
corporate_sdlt: 0
corporate_tax_incidence: 0
household_tax: 0
gov_tax: 0
pre_budget_change_household_tax: 0
- name: Two households, baseline - each household's SDLT counted once
period: 2026
absolute_error_margin: 0.01
input:
people:
buyer:
age: 40
shareholder:
age: 60
benunits:
benunit_1:
members: [buyer]
benunit_2:
members: [shareholder]
households:
household_1:
members: [buyer]
region: LONDON
main_residential_property_purchased: 500_000
main_residential_property_purchased_is_first_home: false
shareholding: 0.000001
business_rates: 0
council_tax: 0
household_owns_tv: false
household_2:
members: [shareholder]
region: SCOTLAND
shareholding: 0.000003
business_rates: 0
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: [15_000, 0]
corporate_sdlt: [3_130, 9_390]
household_tax: [18_130, 9_390]
gov_tax: [18_130, 9_390]
- name: Two households, SDLT abolished - no SDLT left in either household's totals
period: 2026
absolute_error_margin: 0.01
input:
gov.hmrc.stamp_duty.abolish: true
people:
buyer:
age: 40
shareholder:
age: 60
benunits:
benunit_1:
members: [buyer]
benunit_2:
members: [shareholder]
households:
household_1:
members: [buyer]
region: LONDON
main_residential_property_purchased: 500_000
main_residential_property_purchased_is_first_home: false
shareholding: 0.000001
business_rates: 0
council_tax: 0
household_owns_tv: false
household_2:
members: [shareholder]
region: SCOTLAND
shareholding: 0.000003
business_rates: 0
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: [0, 0]
corporate_sdlt: [0, 0]
household_tax: [0, 0]
gov_tax: [0, 0]
# Abolition removes SDLT and nothing else. Corporate SDLT reaches shareholders
# wherever they live; business rates, the other corporate tax allocated by
# shareholding, stays.
- name: A Welsh shareholder bears corporate SDLT and business rates, each once
period: 2026
absolute_error_margin: 0.01
input:
region: WALES
shareholding: 0.000001
business_rates: 500
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: 0
corporate_sdlt: 3_130
corporate_tax_incidence: 3_630
household_tax: 3_630
gov_tax: 3_630
pre_budget_change_household_tax: 3_630
- name: Abolishing SDLT removes the Welsh shareholder's corporate SDLT and keeps business rates
period: 2026
absolute_error_margin: 0.01
input:
gov.hmrc.stamp_duty.abolish: true
region: WALES
shareholding: 0.000001
business_rates: 500
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: 0
corporate_sdlt: 0
corporate_tax_incidence: 500
household_tax: 500
gov_tax: 500
pre_budget_change_household_tax: 500
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# expected_sdlt is the household's own SDLT, with nothing added to it.
# Residential rates from 1 April 2025 (gov.uk/stamp-duty-land-tax/residential-property-rates):
# 0% to £125,000; 2% to £250,000; 5% to £925,000. A £500,000 purchase that is
# not a first home pays 2% x £125,000 + 5% x £250,000 = £15,000.
- name: No purchase and no corporate wealth means no SDLT in the tax totals (England)
period: 2026
absolute_error_margin: 0
input:
region: LONDON
council_tax: 0
household_owns_tv: false
output:
stamp_duty_land_tax: 0
expected_sdlt: 0
corporate_sdlt: 0
household_tax: 0
gov_tax: 0
- name: No SDLT in the tax totals of a household outside England and Northern Ireland (Scotland)
period: 2026
absolute_error_margin: 0
input:
region: SCOTLAND
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: 0
household_tax: 0
gov_tax: 0
- name: No SDLT in the tax totals of a household outside England and Northern Ireland (Wales)
period: 2026
absolute_error_margin: 0
input:
region: WALES
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: 0
household_tax: 0
gov_tax: 0
- name: Expected SDLT on a purchase equals the SDLT liability
period: 2026
absolute_error_margin: 0
input:
region: LONDON
main_residential_property_purchased: 500_000
main_residential_property_purchased_is_first_home: false
council_tax: 0
household_owns_tv: false
output:
stamp_duty_land_tax: 15_000
expected_sdlt: 15_000
household_tax: 15_000
gov_tax: 15_000
- name: Abolishing SDLT zeroes the liability and expected SDLT
period: 2026
absolute_error_margin: 0
input:
gov.hmrc.stamp_duty.abolish: true
region: LONDON
main_residential_property_purchased: 500_000
main_residential_property_purchased_is_first_home: false
council_tax: 0
household_owns_tv: false
output:
sdlt_liable: false
stamp_duty_land_tax: 0
expected_sdlt: 0
household_tax: 0
gov_tax: 0
- name: Abolishing SDLT zeroes expected SDLT even when the liability is entered directly
period: 2026
absolute_error_margin: 0
input:
gov.hmrc.stamp_duty.abolish: true
region: LONDON
stamp_duty_land_tax: 15_000
council_tax: 0
household_owns_tv: false
output:
expected_sdlt: 0
household_tax: 0
gov_tax: 0
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@
country: [ENGLAND, WALES, SCOTLAND, NORTHERN_IRELAND]
output:
sdlt_liable: [true, false, false, true]
- name: No household is liable once Stamp Duty is abolished
period: 2026
input:
gov.hmrc.stamp_duty.abolish: true
country: [ENGLAND, WALES, SCOTLAND, NORTHERN_IRELAND]
output:
sdlt_liable: [false, false, false, false]
Loading
Loading