From 050adbbf023521a2e437d6c89f40bc05dd351903 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 11 May 2026 08:27:19 -0400 Subject: [PATCH] Fix abolish_council_tax to refund net council tax, not gross MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `gov.contrib.abolish_council_tax` switch removed gross council tax from `household_tax` and `gov_tax` but left `council_tax_benefit` untouched. CTR was not in any of the household benefit aggregates (`household_benefits`, `hbai_household_net_income`, `pre_budget_ change_household_benefits`) or in `gov_spending`, so CTR never flowed through net income or the government balance in the first place. Abolition therefore refunded the full gross amount to every household — overstating CTR-recipient households' real-world out- of-pocket saving by their CTR amount, ~£4bn aggregate. Add `council_tax_benefit` to the four aggregates. The existing `if abolish_council_tax: [b for b in benefits if b != "council_ tax_benefit"]` filters become meaningful (no-ops before). Applied the same filter pattern to `hbai_household_net_income.formula` and a fresh `gov_spending.formula`. Now abolition: household net income rises by net council tax, and government balance falls by net council tax revenue. Added regression test for a CTR-recipient household. --- .../fix-abolish-council-tax-net.fixed.md | 1 + .../test_abolish_council_tax_reform.py | 45 +++++++++++++++++++ .../pre_budget_change_household_benefits.py | 1 + policyengine_uk/variables/gov/gov_spending.py | 8 ++++ .../income/hbai_household_net_income.py | 17 ++++--- .../household/income/household_benefits.py | 1 + 6 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 changelog.d/fix-abolish-council-tax-net.fixed.md diff --git a/changelog.d/fix-abolish-council-tax-net.fixed.md b/changelog.d/fix-abolish-council-tax-net.fixed.md new file mode 100644 index 000000000..6d59903f2 --- /dev/null +++ b/changelog.d/fix-abolish-council-tax-net.fixed.md @@ -0,0 +1 @@ +- Include `council_tax_benefit` in `household_benefits`, `gov_spending`, `hbai_household_net_income`, and `pre_budget_change_household_benefits`. Previously CTR was absent from these aggregates, so abolishing council tax via `gov.contrib.abolish_council_tax` refunded the gross billed amount to households (and removed gross revenue from the government balance) rather than the net out-of-pocket amount, overstating household savings by about £4 billion in aggregate. diff --git a/policyengine_uk/tests/microsimulation/test_abolish_council_tax_reform.py b/policyengine_uk/tests/microsimulation/test_abolish_council_tax_reform.py index 81c14f70a..8f5a0bf67 100644 --- a/policyengine_uk/tests/microsimulation/test_abolish_council_tax_reform.py +++ b/policyengine_uk/tests/microsimulation/test_abolish_council_tax_reform.py @@ -37,3 +37,48 @@ def test_abolish_council_tax_removes_budget_impact(): assert reform_household_tax == baseline_household_tax - 2000 assert reform_gov_balance == baseline_gov_balance - 2000 assert reform_hbai == baseline_hbai + 2000 + + +CTR_SITUATION = { + "people": { + "person": { + "age": {YEAR: 40}, + "employment_income": {YEAR: 8000}, + "council_tax_benefit_reported": {YEAR: 800}, + } + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "council_tax": {YEAR: 2000}, + "council_tax_band": {YEAR: "C"}, + } + }, +} + + +@pytest.mark.microsimulation +def test_abolish_council_tax_nets_out_council_tax_benefit(): + """CTR recipient should see net-CT saving, not gross-CT saving. + + Pre-reform: household pays £2,000 gross council tax and receives £800 + CTR, so out-of-pocket is £1,200 (the net bill). Abolishing council + tax saves them the £1,200, not the £2,000 — because the £800 CTR + rebate disappears alongside the council tax it was rebating. + """ + baseline = Microsimulation(situation=CTR_SITUATION) + reform = Microsimulation( + situation=CTR_SITUATION, + reform={"gov.contrib.abolish_council_tax": True}, + ) + + baseline_hbai = baseline.calculate("hbai_household_net_income", YEAR).sum() + reform_hbai = reform.calculate("hbai_household_net_income", YEAR).sum() + baseline_gov_balance = baseline.calculate("gov_balance", YEAR).sum() + reform_gov_balance = reform.calculate("gov_balance", YEAR).sum() + + # Net council tax = £2,000 - £800 CTR = £1,200 saving. + assert reform_hbai == pytest.approx(baseline_hbai + 1200, abs=1) + # Government loses net council tax revenue (gross - CTR forgone). + assert reform_gov_balance == pytest.approx(baseline_gov_balance - 1200, abs=1) diff --git a/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py b/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py index ffb2577f6..2323a3e95 100644 --- a/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py +++ b/policyengine_uk/variables/contrib/policyengine/pre_budget_change_household_benefits.py @@ -10,6 +10,7 @@ class pre_budget_change_household_benefits(Variable): unit = GBP adds = [ "child_benefit", + "council_tax_benefit", "esa_income", "housing_benefit", "income_support", diff --git a/policyengine_uk/variables/gov/gov_spending.py b/policyengine_uk/variables/gov/gov_spending.py index 25ac15362..eb3cb1270 100644 --- a/policyengine_uk/variables/gov/gov_spending.py +++ b/policyengine_uk/variables/gov/gov_spending.py @@ -10,6 +10,7 @@ class gov_spending(Variable): unit = GBP adds = [ "child_benefit", + "council_tax_benefit", "esa_income", "esa_contrib", "housing_benefit", @@ -57,3 +58,10 @@ class gov_spending(Variable): "nhs_spending", "carer_support_payment", ] + + def formula(household, period, parameters): + variables = list(gov_spending.adds) + abolish_council_tax = parameters.gov.contrib.abolish_council_tax(period) + if abolish_council_tax: + variables = [v for v in variables if v != "council_tax_benefit"] + return add(household, period, variables) diff --git a/policyengine_uk/variables/household/income/hbai_household_net_income.py b/policyengine_uk/variables/household/income/hbai_household_net_income.py index 5c33f72c8..fe4e515df 100644 --- a/policyengine_uk/variables/household/income/hbai_household_net_income.py +++ b/policyengine_uk/variables/household/income/hbai_household_net_income.py @@ -26,6 +26,7 @@ class hbai_household_net_income(Variable): "free_school_milk", "free_tv_licence_value", "child_benefit", + "council_tax_benefit", "esa_income", "esa_contrib", "housing_benefit", @@ -74,15 +75,13 @@ class hbai_household_net_income(Variable): def formula(household, period, parameters): abolish_council_tax = parameters.gov.contrib.abolish_council_tax(period) if abolish_council_tax: - return add( - household, - period, - hbai_household_net_income.adds, - ) - add( - household, - period, - [s for s in hbai_household_net_income.subtracts if s != "council_tax"], - ) + adds = [ + a for a in hbai_household_net_income.adds if a != "council_tax_benefit" + ] + subtracts = [ + s for s in hbai_household_net_income.subtracts if s != "council_tax" + ] + return add(household, period, adds) - add(household, period, subtracts) return add(household, period, hbai_household_net_income.adds) - add( household, period, hbai_household_net_income.subtracts ) diff --git a/policyengine_uk/variables/household/income/household_benefits.py b/policyengine_uk/variables/household/income/household_benefits.py index d7cd2faa2..138186ebc 100644 --- a/policyengine_uk/variables/household/income/household_benefits.py +++ b/policyengine_uk/variables/household/income/household_benefits.py @@ -10,6 +10,7 @@ class household_benefits(Variable): unit = GBP adds = [ "child_benefit", + "council_tax_benefit", "esa_income", "esa_contrib", "housing_benefit",