From bbb12c043a60165ade0d41f7f2cd9de19e32e5dc Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 15:58:05 -0400 Subject: [PATCH 1/5] Count IIDB, SDA, incapacity benefit and maternity allowance in Pension Credit income SPC Regs 2002 reg 15(1) prescribes all social security benefits as income except a closed list that does not name these four. Append them to gov.dwp.pension_credit.guarantee_credit.income. The reported IIDB amount cannot separate the constant attendance and exceptionally severe disablement increases that reg 15(1)(c) and (e) except. Complete the reg 9 Savings Credit non-qualifying list with working tax credit, incapacity benefit, SDA and maternity allowance, and let savings_credit_income read benefit-unit sources such as working tax credit. Fix the list's unit metadata and add a label. Housing Benefit income counts IIDB, SDA and incapacity benefit under both HB (SPC) Regs 2006 reg 29(1)(j) and HB Regs 2006 reg 40 with Sch 5. Remove the unused duplicate parameter gov.dwp.pension_credit.income.savings_credit_excluded_sources. Refs PolicyEngine/policyengine-uk#1954. Co-Authored-By: Claude Opus 5.5 --- ...ension-credit-prescribed-benefits.fixed.md | 1 + ...sion-credit-prescribed-benefits.removed.md | 1 + .../guarantee_credit/income.yaml | 7 + .../savings_credit_excluded_sources.yaml | 12 - .../savings_credit/excluded_income.yaml | 10 +- ...ion_credit_prescribed_benefits_income.yaml | 476 ++++++++++++++++++ .../housing_benefit_applicable_income.py | 9 + .../savings_credit/savings_credit_income.py | 14 +- 8 files changed, 513 insertions(+), 17 deletions(-) create mode 100644 changelog.d/pension-credit-prescribed-benefits.fixed.md create mode 100644 changelog.d/pension-credit-prescribed-benefits.removed.md delete mode 100644 policyengine_uk/parameters/gov/dwp/pension_credit/income/savings_credit_excluded_sources.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml diff --git a/changelog.d/pension-credit-prescribed-benefits.fixed.md b/changelog.d/pension-credit-prescribed-benefits.fixed.md new file mode 100644 index 0000000000..1717f0b768 --- /dev/null +++ b/changelog.d/pension-credit-prescribed-benefits.fixed.md @@ -0,0 +1 @@ +- Pension Credit income now counts industrial injuries disablement benefit, severe disablement allowance, incapacity benefit and maternity allowance (SPC Regs 2002 reg 15(1)); Savings Credit treats working tax credit, incapacity benefit, severe disablement allowance and maternity allowance as non-qualifying income (reg 9); Housing Benefit income now counts industrial injuries disablement benefit, severe disablement allowance and incapacity benefit. diff --git a/changelog.d/pension-credit-prescribed-benefits.removed.md b/changelog.d/pension-credit-prescribed-benefits.removed.md new file mode 100644 index 0000000000..569194bb61 --- /dev/null +++ b/changelog.d/pension-credit-prescribed-benefits.removed.md @@ -0,0 +1 @@ +- Removed the unused duplicate parameter `gov.dwp.pension_credit.income.savings_credit_excluded_sources`; `gov.dwp.pension_credit.savings_credit.excluded_income` is the list the model reads. diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml index 3da4354bd1..ec4fa71901 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml @@ -26,3 +26,10 @@ values: # Supplement, which is a separate variable. - carers_allowance - carer_support_payment + # Reg. 15(1) does not list these either. Reg. 15(1)(c) and (e) except the + # constant attendance and exceptionally severe disablement increases of + # IIDB, but iidb is the whole reported payment and cannot separate them. + - iidb + - sda + - incapacity_benefit + - maternity_allowance diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/income/savings_credit_excluded_sources.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/income/savings_credit_excluded_sources.yaml deleted file mode 100644 index bedb5dd94b..0000000000 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/income/savings_credit_excluded_sources.yaml +++ /dev/null @@ -1,12 +0,0 @@ -description: Income sources which are deducted from income before Savings Credit calculations. -metadata: - economy: false - propagate_metadata_to_children: true - reference: - - href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/9 - title: The State Pension Credit Regulations 2002 reg. 9 - unit: currency-GBP -values: - 2002-01-01: - - jsa_contrib - - esa_contrib diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/excluded_income.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/excluded_income.yaml index e6fe669bbd..145e714047 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/excluded_income.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/excluded_income.yaml @@ -1,12 +1,20 @@ description: Income sources which are deducted from Guarantee Credit income before Savings Credit calculations. metadata: + label: Savings Credit non-qualifying income sources propagate_metadata_to_children: true reference: - href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/9 title: The State Pension Credit Regulations 2002 reg. 9 - unit: currency-GBP + unit: list values: 2002-01-01: + # Reg. 9(c) and (g). - jsa_contrib - esa_contrib + # Reg. 9(a), (b), (d) and (e). Reg. 9(f) maintenance is not modelled: + # maintenance_income does not separate spousal from child maintenance. + - working_tax_credit + - incapacity_benefit + - sda + - maternity_allowance diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml new file mode 100644 index 0000000000..082c1d1318 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml @@ -0,0 +1,476 @@ +# Industrial injuries disablement benefit (IIDB), severe disablement allowance +# (SDA), incapacity benefit (IB) and maternity allowance (MA) in Pension Credit +# and Housing Benefit income. +# +# SPC Regs 2002 reg 15(1): "all social security benefits are prescribed except" +# a closed list. The list does not name the four benefits; for IIDB it excepts +# only the increases under SSCBA 1992 ss.104 and 105 (constant attendance and +# exceptionally severe disablement) and Sch 8 paras 4 and 7(2)(b) (reg +# 15(1)(c)-(e)). Reg 9 makes WTC, IB, SDA and MA (with contribution-based JSA, +# contributory ESA and reg 15(5)(d) maintenance) non-qualifying income for +# Savings Credit; IIDB stays qualifying income. HB (SPC) Regs 2006 reg 29(1)(j) +# counts the same social security benefits for pension-age Housing Benefit, and +# HB Regs 2006 reg 40 counts working-age gross income less Sch 5. +# +# The awards are ordinary awards with no excepted increase, entered as reported +# amounts (SDA as a reported award, which the model pays at gov.dwp.sda.maximum). +# Weekly rates, x 52: +# SI 2026/148 Sch 1: IIDB 100% 233.90, 20% 46.78 (Part V); SDA 103.85 plus +# the higher age related addition 15.50 (Part III); long-term IB 146.60 +# (Part I). Art 9: SMP reg 6 rate 194.32, which is the MA standard rate under +# SSCBA s.35A(1)(b) from 6 April 2026 (art 1). Art 29: standard minimum +# guarantee 238.00 single and 363.25 couple; Savings Credit threshold 208.07 +# single. +# SI 2025/295 Sch 1 Part V: IIDB 20% 45.06; art 29: single standard minimum +# guarantee 227.10. +# MA and IB are rarely paid above State Pension age (the Enhanced FRS has no +# pension-age recipients); the cases test classification. None of these +# claimants pays income tax, so tax does not enter any expected value. + +- name: Case 1, single pensioner with IIDB at 20 percent. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 1_000 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # 1,000 + 2,432.56 = 3,432.56. + pension_credit_income: 1_000 + 46.78 * 52 + # 12,376 - 3,432.56 = 8,943.44. + guarantee_credit: 238 * 52 - (1_000 + 46.78 * 52) + +- name: Case 2, pensioner couple whose partner has IIDB at 100 percent. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 5_000 + person2: + age: 70 + state_pension: 0 + iidb_reported: 233.90 * 52 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + country: ENGLAND + output: + # 5,000 + 12,162.80 = 17,162.80. + pension_credit_income: 5_000 + 233.90 * 52 + # 18,889 - 17,162.80 = 1,726.20. + guarantee_credit: 363.25 * 52 - (5_000 + 233.90 * 52) + +- name: Case 3, single pensioner with SDA and the higher age related addition. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 1_000 + sda_reported: 1 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # The model pays 119.35 a week, the statutory 103.85 + 15.50. + sda: (103.85 + 15.50) * 52 + # 1,000 + 6,206.20 = 7,206.20. + pension_credit_income: 1_000 + (103.85 + 15.50) * 52 + # 12,376 - 7,206.20 = 5,169.80. + guarantee_credit: 238 * 52 - (1_000 + (103.85 + 15.50) * 52) + +- name: Case 4, pensioner couple whose partner has SDA. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 5_000 + person2: + age: 70 + state_pension: 0 + sda_reported: 1 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + country: ENGLAND + output: + # 5,000 + 6,206.20 = 11,206.20. + pension_credit_income: 5_000 + (103.85 + 15.50) * 52 + # 18,889 - 11,206.20 = 7,682.80. + guarantee_credit: 363.25 * 52 - (5_000 + (103.85 + 15.50) * 52) + +- name: Case 5, single pensioner with long-term incapacity benefit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 1_000 + incapacity_benefit_reported: 146.60 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # IB is taxable, but 8,623.20 of income bears no tax in the model. + income_tax: 0 + # 1,000 + 7,623.20 = 8,623.20. + pension_credit_income: 1_000 + 146.60 * 52 + # 12,376 - 8,623.20 = 3,752.80. + guarantee_credit: 238 * 52 - (1_000 + 146.60 * 52) + +- name: Case 6, pensioner couple whose partner has long-term incapacity benefit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 5_000 + person2: + age: 70 + state_pension: 0 + incapacity_benefit_reported: 146.60 * 52 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + country: ENGLAND + output: + income_tax: [0, 0] + # 5,000 + 7,623.20 = 12,623.20. + pension_credit_income: 5_000 + 146.60 * 52 + # 18,889 - 12,623.20 = 6,265.80. + guarantee_credit: 363.25 * 52 - (5_000 + 146.60 * 52) + +- name: Case 7, single pensioner with maternity allowance at the standard rate. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 1_000 + maternity_allowance_reported: 194.32 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # 1,000 + 10,104.64 = 11,104.64. + pension_credit_income: 1_000 + 194.32 * 52 + # 12,376 - 11,104.64 = 1,271.36. + guarantee_credit: 238 * 52 - (1_000 + 194.32 * 52) + +- name: Case 8, pensioner couple whose partner has maternity allowance. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 5_000 + person2: + age: 70 + state_pension: 0 + maternity_allowance_reported: 194.32 * 52 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + country: ENGLAND + output: + # 5,000 + 10,104.64 = 15,104.64. + pension_credit_income: 5_000 + 194.32 * 52 + # 18,889 - 15,104.64 = 3,784.36. + guarantee_credit: 363.25 * 52 - (5_000 + 194.32 * 52) + +- name: Case 9, single pensioner with IIDB at 20 percent in 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 1_000 + iidb_reported: 45.06 * 52 + benunits: + benunit: + members: [person1] + # SI 2025/295 art 29 sets the single standard minimum guarantee at + # 227.10; the model's 2025 parameter holds 221.85, so the statutory + # amount is supplied to isolate the income rule. + minimum_guarantee: 227.10 * 52 + households: + household: + members: [person1] + country: ENGLAND + output: + # 1,000 + 2,343.12 = 3,343.12. + pension_credit_income: 1_000 + 45.06 * 52 + # 11,809.20 - 3,343.12 = 8,466.08. + guarantee_credit: 227.10 * 52 - (1_000 + 45.06 * 52) + +- name: Case 10, IIDB is qualifying income for Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + # Aged 80, so State Pension age was reached before the 2016 cutoff. + age: 80 + state_pension: 0 + private_pension_income: 175.04 * 52 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # 175.04 + 46.78 = 221.82 a week, 11,534.64 a year. + pension_credit_income: (175.04 + 46.78) * 52 + savings_credit_income: (175.04 + 46.78) * 52 + # 0.6 x (221.82 - 208.07) = 8.25 a week, 429.00 a year: below the + # maximum 0.6 x (238 - 208.07), and income is below the minimum + # guarantee, so nothing is withdrawn. + savings_credit: 0.6 * (221.82 - 208.07) * 52 + # (238 - 221.82) x 52 = 841.36. + guarantee_credit: (238 - 221.82) * 52 + +- name: Case 11, an IIDB recipient who reached State Pension age after the cutoff gets no Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 175.04 * 52 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + meets_savings_credit_age_requirement: false + savings_credit_income: (175.04 + 46.78) * 52 + savings_credit: 0 + guarantee_credit: (238 - 221.82) * 52 + +- name: Case 12, SDA is not qualifying income for Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 6_000 + sda_reported: 1 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # 6,000 + 6,206.20 = 12,206.20. + pension_credit_income: 6_000 + (103.85 + 15.50) * 52 + # Reg 9(d): qualifying income is the 6,000 pension, below the 10,819.64 + # threshold, so no Savings Credit. Counting SDA as qualifying income + # would give 0.6 x (12,206.20 - 10,819.64) = 831.94. + savings_credit_income: 6_000 + is_savings_credit_eligible: false + savings_credit: 0 + # 12,376 - 12,206.20 = 169.80. + guarantee_credit: 238 * 52 - (6_000 + (103.85 + 15.50) * 52) + +- name: Case 13, incapacity benefit is not qualifying income for Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 4_500 + incapacity_benefit_reported: 146.60 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # 12,123.20 of income bears no tax in the model. + income_tax: 0 + # 4,500 + 7,623.20 = 12,123.20. + pension_credit_income: 4_500 + 146.60 * 52 + # Reg 9(b). + savings_credit_income: 4_500 + savings_credit: 0 + # 12,376 - 12,123.20 = 252.80. + guarantee_credit: 238 * 52 - (4_500 + 146.60 * 52) + +- name: Case 14, maternity allowance is not qualifying income for Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 2_000 + maternity_allowance_reported: 194.32 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + output: + # 2,000 + 10,104.64 = 12,104.64. + pension_credit_income: 2_000 + 194.32 * 52 + # Reg 9(e). + savings_credit_income: 2_000 + savings_credit: 0 + # 12,376 - 12,104.64 = 271.36. + guarantee_credit: 238 * 52 - (2_000 + 194.32 * 52) + +- name: Case 15, working tax credit is not qualifying income for Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 10_000 + benunits: + benunit: + members: [person1] + # An illustrative award: reg 9(a) applies to any amount, and working + # tax credit rates are not set by the up-rating orders used here. + working_tax_credit: 1_000 + households: + household: + members: [person1] + country: ENGLAND + output: + # SPCA s.15(1)(b): 10,000 + 1,000 = 11,000. + pension_credit_income: 11_000 + # Reg 9(a): qualifying income is 10,000, below the 10,819.64 threshold. + # Counting the tax credit would give 0.6 x (11,000 - 10,819.64) = 108.22. + savings_credit_income: 10_000 + savings_credit: 0 + # 12,376 - 11,000 = 1,376. + guarantee_credit: 1_376 + +- name: Case 16, pension-age Housing Benefit counts IIDB once Guarantee Credit ends. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension: 0 + private_pension_income: 1_000 + iidb_reported: 233.90 * 52 + benunits: + benunit: + members: [person1] + # The model applies the HB earnings disregard (HB (SPC) Regs Sch 4) + # whether or not there are earnings; there are none here, so it is + # set to zero to isolate the income rule. + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1] + country: ENGLAND + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + # 1,000 + 12,162.80 = 13,162.80, above the 12,376 minimum guarantee. + pension_credit_income: 1_000 + 233.90 * 52 + guarantee_credit: 0 + # HB (SPC) Regs reg 29(1)(j): the same 13,162.80. + housing_benefit_applicable_income: 1_000 + 233.90 * 52 + +- name: Case 17, working-age Housing Benefit counts incapacity benefit and SDA. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + incapacity_benefit_reported: 146.60 * 52 + person2: + age: 45 + sda_reported: 1 + benunits: + benunit: + members: [person1, person2] + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1, person2] + country: ENGLAND + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + income_tax: [0, 0] + # HB Regs 2006 reg 40(1): 7,623.20 + 6,206.20 = 13,829.40. + housing_benefit_applicable_income: (146.60 + 103.85 + 15.50) * 52 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index b97d9ac316..ad10cb9549 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -67,6 +67,15 @@ def formula(benunit, period, parameters): "statutory_sick_pay", "statutory_maternity_pay", "ssmg", + # Counted by both regimes: HB (SPC) Regs 2006 reg 29(1)(j), which + # excepts only the IIDB increases under SSCBA ss.104-105 and Sch 8 + # (heads (iii)-(v)), and HB Regs 2006 reg 40 with Sch 5, whose + # para 9 attendance allowance disregard covers the ss.104-105 + # increases (reg 2(1)). iidb is the whole reported payment and + # cannot separate those increases. + "iidb", + "sda", + "incapacity_benefit", ] INCOME_COMPONENTS = [ "employment_income", diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py index 856e7e99f1..9cc98ec6fa 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py @@ -21,8 +21,14 @@ def formula(benunit, period, parameters): pc = parameters(period).gov.dwp.pension_credit person = benunit.members is_claimant_or_partner = person("is_claimant_or_partner", period) - excluded_income = benunit.sum( - add(person, period, pc.savings_credit.excluded_income) - * is_claimant_or_partner - ) + excluded_income = 0 + for source in pc.savings_credit.excluded_income: + # Person-level sources count for the claimant and partner only; + # benefit-unit sources (working tax credit) count once. + if person.entity.get_variable(source).entity.is_person: + excluded_income += benunit.sum( + person(source, period) * is_claimant_or_partner + ) + else: + excluded_income += add(benunit, period, [source]) return max_(0, guarantee_credit_income - excluded_income) From 2b2d51ff3a8ddaf3ca30349762eb60aba0b761f8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 15:37:24 -0400 Subject: [PATCH 2/5] Finish builder edits after review (checkpoint) --- ...ion_credit_prescribed_benefits_income.yaml | 87 ++++- ...n_credit_prescribed_benefits_properties.py | 333 ++++++++++++++++++ .../housing_benefit_applicable_income.py | 13 +- 3 files changed, 416 insertions(+), 17 deletions(-) create mode 100644 policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml index 082c1d1318..5833bd54c2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml @@ -3,14 +3,15 @@ # and Housing Benefit income. # # SPC Regs 2002 reg 15(1): "all social security benefits are prescribed except" -# a closed list. The list does not name the four benefits; for IIDB it excepts -# only the increases under SSCBA 1992 ss.104 and 105 (constant attendance and +# a closed list. The list does not name the four benefits. For IIDB it excepts +# the increases under SSCBA 1992 ss.104 and 105 (constant attendance and # exceptionally severe disablement) and Sch 8 paras 4 and 7(2)(b) (reg -# 15(1)(c)-(e)). Reg 9 makes WTC, IB, SDA and MA (with contribution-based JSA, -# contributory ESA and reg 15(5)(d) maintenance) non-qualifying income for -# Savings Credit; IIDB stays qualifying income. HB (SPC) Regs 2006 reg 29(1)(j) -# counts the same social security benefits for pension-age Housing Benefit, and -# HB Regs 2006 reg 40 counts working-age gross income less Sch 5. +# 15(1)(c)-(e)); (h) excepts increases for a dependant other than a partner. +# Reg 9 makes WTC, IB, SDA and MA (with contribution-based JSA, contributory +# ESA and reg 15(5)(d) maintenance) non-qualifying income for Savings Credit; +# IIDB stays qualifying income. HB (SPC) Regs 2006 reg 29(1)(j) counts the same +# social security benefits for pension-age Housing Benefit, and HB Regs 2006 +# reg 40 counts working-age gross income less Sch 5. # # The awards are ordinary awards with no excepted increase, entered as reported # amounts (SDA as a reported award, which the model pays at gov.dwp.sda.maximum). @@ -20,12 +21,13 @@ # (Part I). Art 9: SMP reg 6 rate 194.32, which is the MA standard rate under # SSCBA s.35A(1)(b) from 6 April 2026 (art 1). Art 29: standard minimum # guarantee 238.00 single and 363.25 couple; Savings Credit threshold 208.07 -# single. +# single and 329.75 couple. # SI 2025/295 Sch 1 Part V: IIDB 20% 45.06; art 29: single standard minimum # guarantee 227.10. -# MA and IB are rarely paid above State Pension age (the Enhanced FRS has no -# pension-age recipients); the cases test classification. None of these -# claimants pays income tax, so tax does not enter any expected value. +# The cases test classification, not whether a pensioner would be paid MA or +# IB. None of these claimants pays income tax, so tax does not enter any +# expected value; the model taxes IB, so the IB cases also assert that its +# income tax is zero. - name: Case 1, single pensioner with IIDB at 20 percent. period: 2026 @@ -463,6 +465,7 @@ benunits: benunit: members: [person1, person2] + # As in Case 16 (the working-age disregard is HB Regs 2006 Sch 4). housing_benefit_applicable_income_disregard: 0 households: household: @@ -474,3 +477,65 @@ income_tax: [0, 0] # HB Regs 2006 reg 40(1): 7,623.20 + 6,206.20 = 13,829.40. housing_benefit_applicable_income: (146.60 + 103.85 + 15.50) * 52 + +- name: Case 18, working tax credit does not add to a positive Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 221.82 * 52 + benunits: + benunit: + members: [person1] + # An illustrative award, as in Case 15. + working_tax_credit: 200 + households: + household: + members: [person1] + country: ENGLAND + output: + # 11,534.64 + 200 = 11,734.64. + pension_credit_income: 221.82 * 52 + 200 + # Reg 9(a): the pension only. + savings_credit_income: 221.82 * 52 + # 0.6 x (221.82 - 208.07) x 52 = 429.00; income is below the minimum + # guarantee, so nothing is withdrawn. Counting the tax credit would give + # 0.6 x (11,734.64 - 10,819.64) = 549.00. + savings_credit: 0.6 * (221.82 - 208.07) * 52 + # 12,376 - 11,734.64 = 641.36. + guarantee_credit: 238 * 52 - (221.82 * 52 + 200) + +- name: Case 19, a partner's SDA is not qualifying income for a couple's Savings Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 12_000 + person2: + age: 78 + state_pension: 0 + sda_reported: 1 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + country: ENGLAND + output: + # 12,000 + 6,206.20 = 18,206.20. + pension_credit_income: 12_000 + (103.85 + 15.50) * 52 + # Reg 9(d): qualifying income is the 12,000 pension, below the couple + # threshold of 329.75 x 52 = 17,147. Counting the partner's SDA would give + # 0.6 x (18,206.20 - 17,147) = 635.52. + savings_credit_income: 12_000 + is_savings_credit_eligible: false + savings_credit: 0 + # 18,889 - 18,206.20 = 682.80. + guarantee_credit: 363.25 * 52 - (12_000 + (103.85 + 15.50) * 52) diff --git a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py new file mode 100644 index 0000000000..ad6f252a14 --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py @@ -0,0 +1,333 @@ +"""Prescribed benefits in Pension Credit and Housing Benefit income. + +The State Pension Credit Regulations 2002 reg 15(1) prescribe "all social +security benefits" as income except a closed list. The list does not name +industrial injuries disablement benefit (IIDB), severe disablement allowance +(SDA), incapacity benefit (IB) or maternity allowance (MA). For IIDB it +excepts the constant attendance and exceptionally severe disablement +increases (reg 15(1)(c)-(e)), which the reported IIDB amount cannot separate. +Pension-age Housing Benefit counts the same benefits under HB (SPC) Regs 2006 +reg 29(1)(j), whose exceptions for these benefits match reg 15(1)'s. Reg 9 +makes working tax credit, IB, SDA, MA, contribution-based JSA and contributory +ESA non-qualifying income for Savings Credit; IIDB stays qualifying income. + +Draws: single people and couples over State Pension age in England, Wales or +Scotland, renting from the council in 2026, with State Pension, a private +pension and any of the four benefits (each on a drawn member, at a drawn +amount). Some families also have a carer receiving Carer's Allowance or, in +Scotland, Carer Support Payment, so the sources #1952 added are covered +together with these. There are no earnings, pension contributions or +children, and capital is at most £10,000, so neither Pension Credit deemed +income nor Housing Benefit tariff income arises. The benefit amounts are +inputs standing for actual awards, independent of the model's rate formulas. + +Invariants: + +1. Differential: Pension Credit and pension-age Housing Benefit assess the + same income. Where Guarantee Credit is not paid and Housing Benefit income + is above its zero floor, Housing Benefit applicable income plus its income + disregard equals Pension Credit income. The disregard is added back because + the model applies the HB earnings disregard (HB (SPC) Regs Sch 4) whether or + not there are earnings; there are none here. There is no other intended + difference on this domain: both measures deduct the claimant's and + partner's income tax and National Insurance, and neither counts the + Scottish Carer Supplement (SPC Regs reg 15(1)(ri); HB (SPC) Regs reg + 29(1)(j)(xviiha)). +2. Guarantee Credit is non-increasing in each of the four benefits, and while + it is positive it falls pound for pound with the extra Pension Credit + income, which is the extra benefit less the extra income tax and National + Insurance (the model taxes IB). +3. Savings Credit income never exceeds Pension Credit income. Adding an amount + of a reg 9 source (working tax credit, IB, SDA, MA, contribution-based JSA + or contributory ESA) changes Savings Credit income only through income tax + and National Insurance, and never raises Savings Credit. Draws for this + invariant are aged 77 or over: the model dates State Pension age as birth + year plus the current State Pension age (66 in 2026), so they meet its + test of reaching it before the 2016 Savings Credit cutoff. +4. Boundary: on the private-pension step where Guarantee Credit ends, + household net income, excluding the TV licence fee, does not fall. The free + TV licence for over-75s on Pension Credit is a genuine statutory cliff, so + it is excluded, as in test_pension_credit_carer_income_properties.py. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PENSIONS = np.arange(0, 25_001, 500) +AMOUNTS = np.arange(0, 15_001, 1_250) +PRESCRIBED_BENEFITS = ("iidb", "sda", "incapacity_benefit", "maternity_allowance") +# Reg 9 sources and the input each is set through: the tax base reads +# reported contribution-based JSA and contributory ESA. +REG_9_INPUTS = { + "working_tax_credit": "working_tax_credit", + "incapacity_benefit": "incapacity_benefit", + "sda": "sda", + "maternity_allowance": "maternity_allowance", + "jsa_contrib": "jsa_contrib_reported", + "esa_contrib": "esa_contrib_reported", +} +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +REGIONS = { + "ENGLAND": "NORTH_WEST", + "WALES": "WALES", + "SCOTLAND": "SCOTLAND", +} + + +def money(high, low=0): + return st.floats(low, high, allow_nan=False, allow_infinity=False) + + +@st.composite +def family(draw, min_age=67, high=12_000, state_pension=15_000): + adults = [ + dict( + age=draw(st.integers(min_age, 95)), + state_pension=draw(money(state_pension)), + ) + for _ in range(draw(st.integers(1, 2))) + ] + members = st.integers(0, len(adults) - 1) + return dict( + adults=adults, + # Each benefit goes to one drawn member; an amount of zero is common. + benefits={ + benefit: (draw(members), draw(st.one_of(st.just(0.0), money(high)))) + for benefit in PRESCRIBED_BENEFITS + }, + # As in #1952, at most one carer, by caring hours or a reported award. + carer=draw(st.one_of(st.none(), members)), + by_hours=draw(st.booleans()), + country=draw(st.sampled_from(tuple(REGIONS))), + rent=draw(money(12_000)), + council_tax=draw(money(3_000)), + savings=draw(st.one_of(st.just(0.0), money(10_000))), + ) + + +def situation(rows): + """One benefit unit and household per (family, private pension, inputs) row. + + The inputs map a variable to an amount added to the family's first adult, + or set for the benefit unit when the variable is working tax credit. + """ + people, benunits, households = {}, {}, {} + for r, (fam, pension, inputs) in enumerate(rows): + names = [] + for j, a in enumerate(fam["adults"]): + name = f"p{r}_{j}" + person = { + "age": {YEAR: a["age"]}, + "state_pension": {YEAR: a["state_pension"]}, + # All private pension goes to the first adult. + "private_pension_income": {YEAR: float(pension) * (j == 0)}, + } + for benefit, (recipient, amount) in fam["benefits"].items(): + person[benefit] = {YEAR: amount * (j == recipient)} + if j == fam["carer"]: + if fam["by_hours"]: + person["care_hours"] = {YEAR: 35} + else: + person["carers_allowance_reported"] = {YEAR: 1} + if j == 0: + for variable, amount in inputs.items(): + if variable != "working_tax_credit": + drawn = person.get(variable, {YEAR: 0.0})[YEAR] + person[variable] = {YEAR: drawn + float(amount)} + people[name] = person + names.append(name) + benunit = {"members": names} + if "working_tax_credit" in inputs: + benunit["working_tax_credit"] = {YEAR: float(inputs["working_tax_credit"])} + benunits[f"b{r}"] = benunit + households[f"h{r}"] = { + "members": names, + "country": {YEAR: fam["country"]}, + "region": {YEAR: REGIONS[fam["country"]]}, + "tenure_type": {YEAR: "RENT_FROM_COUNCIL"}, + "rent": {YEAR: fam["rent"]}, + "council_tax": {YEAR: fam["council_tax"]}, + "savings": {YEAR: fam["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(rows, shape, variables): + """Each variable per benefit unit (summed over members), reshaped.""" + simulation = Simulation(situation=situation(rows)) + return { + variable: np.asarray( + simulation.calculate(variable, YEAR, map_to="benunit") + ).reshape(shape) + for variable in variables + } + + +def pension_grid(families, variables): + rows = [(fam, pension, {}) for fam in families for pension in PENSIONS] + return calculate(rows, (len(families), len(PENSIONS)), variables) + + +def with_amount(fam, benefit, amount): + """The family with one benefit's award set to amount, on its recipient.""" + recipient, _ = fam["benefits"][benefit] + return {**fam, "benefits": {**fam["benefits"], benefit: (recipient, amount)}} + + +@PROPERTY_SETTINGS +@given(st.lists(family(), min_size=1, max_size=4)) +def test_pension_credit_and_housing_benefit_assess_the_same_prescribed_benefits( + families, +): + g = pension_grid( + families, + [ + "guarantee_credit", + "pension_credit_income", + "housing_benefit_eligible", + "housing_benefit_applicable_income", + "housing_benefit_applicable_income_disregard", + ], + ) + hb_income = g["housing_benefit_applicable_income"] + # Where Guarantee Credit is not paid, and away from the zero floor on + # Housing Benefit income. + compared = ( + (g["guarantee_credit"] <= 0) + & g["housing_benefit_eligible"].astype(bool) + & (hb_income > 0) + ) + before_disregard = hb_income + g["housing_benefit_applicable_income_disregard"] + pc_income = g["pension_credit_income"] + assert np.allclose(before_disregard[compared], pc_income[compared], atol=0.01), ( + families, + before_disregard[compared], + pc_income[compared], + ) + + +@PROPERTY_SETTINGS +@given(st.lists(family(state_pension=8_000), min_size=1, max_size=4)) +def test_guarantee_credit_falls_pound_for_pound_with_each_prescribed_benefit( + families, +): + rows = [ + (with_amount(fam, benefit, amount), 0, {}) + for fam in families + for benefit in PRESCRIBED_BENEFITS + for amount in AMOUNTS + ] + shape = (len(families), len(PRESCRIBED_BENEFITS), len(AMOUNTS)) + g = calculate( + rows, + shape, + [ + "guarantee_credit", + "pension_credit_income", + "income_tax", + "national_insurance", + ], + ) + gc = g["guarantee_credit"] + pc_income = g["pension_credit_income"] + tax = g["income_tax"] + g["national_insurance"] + assert np.all(np.diff(gc, axis=-1) <= 0.01), (families, gc) + # The extra Pension Credit income is the extra benefit less the extra tax. + extra_income = np.diff(pc_income, axis=-1) + positive_income = (pc_income[..., :-1] > 0) & (pc_income[..., 1:] > 0) + assert np.allclose( + extra_income[positive_income], + (np.diff(AMOUNTS) - np.diff(tax, axis=-1))[positive_income], + atol=0.01, + ), (families, pc_income, tax) + # While Guarantee Credit is paid, it falls by exactly that much. + paid = gc[..., 1:] > 0 + assert np.allclose(-np.diff(gc, axis=-1)[paid], extra_income[paid], atol=0.01), ( + families, + gc, + pc_income, + ) + + +@PROPERTY_SETTINGS +@given( + st.lists( + family(min_age=77, high=3_000, state_pension=4_000), min_size=2, max_size=4 + ), + # Private pensions that put qualifying income near the Savings Credit + # window, so that Savings Credit is often payable before the addition. + st.lists(money(18_000, low=8_000), min_size=4, max_size=4), + money(15_000), +) +def test_reg_9_sources_change_savings_credit_income_only_through_tax( + families, pensions, amount +): + rows = [ + (fam, pension, {REG_9_INPUTS[source]: added}) + for fam, pension in zip(families, pensions) + for source in REG_9_INPUTS + for added in (0, amount) + ] + shape = (len(families), len(REG_9_INPUTS), 2) + g = calculate( + rows, + shape, + [ + "pension_credit_income", + "savings_credit_income", + "savings_credit", + "income_tax", + "national_insurance", + ], + ) + sc_income = g["savings_credit_income"] + assert np.all(sc_income <= g["pension_credit_income"] + 0.01), (families, g) + savings_credit = g["savings_credit"] + assert np.all(savings_credit[..., 1] <= savings_credit[..., 0] + 0.01), ( + families, + amount, + savings_credit, + ) + tax = g["income_tax"] + g["national_insurance"] + extra_tax = tax[..., 1] - tax[..., 0] + assert np.allclose( + sc_income[..., 1], np.maximum(sc_income[..., 0] - extra_tax, 0), atol=0.01 + ), (families, amount, sc_income, tax) + + +@PROPERTY_SETTINGS +@given(st.lists(family(high=4_000, state_pension=4_000), min_size=1, max_size=4)) +def test_net_income_does_not_fall_where_guarantee_credit_ends(families): + g = pension_grid( + families, + [ + "guarantee_credit", + "household_net_income", + "tv_licence", + "housing_benefit", + "council_tax_benefit", + ], + ) + step = np.diff(g["household_net_income"] + g["tv_licence"], axis=1) + gc = g["guarantee_credit"] + ends = (gc[:, :-1] > 0) & (gc[:, 1:] <= 0) + for i, fam in enumerate(families): + for k in np.flatnonzero(ends[i]): + assert step[i, k] >= -0.01, ( + fam, + f"private pension {PENSIONS[k]} -> {PENSIONS[k + 1]}", + f"net income change {step[i, k]:.2f}", + f"HB {g['housing_benefit'][i, k]:.2f} -> " + f"{g['housing_benefit'][i, k + 1]:.2f}", + f"CTR {g['council_tax_benefit'][i, k]:.2f} -> " + f"{g['council_tax_benefit'][i, k + 1]:.2f}", + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index ad10cb9549..17e6697cb1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -67,12 +67,13 @@ def formula(benunit, period, parameters): "statutory_sick_pay", "statutory_maternity_pay", "ssmg", - # Counted by both regimes: HB (SPC) Regs 2006 reg 29(1)(j), which - # excepts only the IIDB increases under SSCBA ss.104-105 and Sch 8 - # (heads (iii)-(v)), and HB Regs 2006 reg 40 with Sch 5, whose - # para 9 attendance allowance disregard covers the ss.104-105 - # increases (reg 2(1)). iidb is the whole reported payment and - # cannot separate those increases. + # Counted by both regimes: HB (SPC) Regs 2006 reg 29(1)(j), whose + # exceptions touching these are the IIDB increases under SSCBA + # ss.104-105 and Sch 8 (heads (iii)-(v)) and non-partner dependant + # increases (head (viii)), and HB Regs 2006 reg 40 with Sch 5, + # whose para 9 attendance allowance disregard covers the + # ss.104-105 increases (reg 2(1)). iidb is the whole reported + # payment and cannot separate those increases. "iidb", "sda", "incapacity_benefit", From 7b225563e58d6aca76f8bee0680eb5fb9a3ff709 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 16:27:28 -0400 Subject: [PATCH 3/5] Floor Savings Credit excluded sources at nil; test working-age HB without a UC claim savings_credit_income now floors each person-level excluded source at nil, as pension_credit_income counts them since #2259, so the two never disagree on a negative input. Case 17 tested working-age Housing Benefit income, but on current main the couple's Universal Credit entitlement passports Housing Benefit (#2133) and makes its income nil. Set claims_universal_credit to false so the case tests the means test itself. Co-Authored-By: Claude Opus 5.5 --- .../pension_credit_prescribed_benefits_income.yaml | 4 ++++ .../pension_credit/savings_credit/savings_credit_income.py | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml index 5833bd54c2..26babd2558 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml @@ -467,6 +467,10 @@ members: [person1, person2] # As in Case 16 (the working-age disregard is HB Regs 2006 Sch 4). housing_benefit_applicable_income_disregard: 0 + # A legacy Housing Benefit claim: a Universal Credit claim would + # passport Housing Benefit (housing_benefit_on_passporting_benefit) + # and make its income nil, and this case tests the means test itself. + claims_universal_credit: false households: household: members: [person1, person2] diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py index 9cc98ec6fa..418d15124f 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/savings_credit_income.py @@ -23,11 +23,12 @@ def formula(benunit, period, parameters): is_claimant_or_partner = person("is_claimant_or_partner", period) excluded_income = 0 for source in pc.savings_credit.excluded_income: - # Person-level sources count for the claimant and partner only; + # Person-level sources count for the claimant and partner only, + # each floored at nil as pension_credit_income counts them; # benefit-unit sources (working tax credit) count once. if person.entity.get_variable(source).entity.is_person: excluded_income += benunit.sum( - person(source, period) * is_claimant_or_partner + max_(0, person(source, period)) * is_claimant_or_partner ) else: excluded_income += add(benunit, period, [source]) From 414e91532a3a73d80d014a8f04cb9672c0ed74e1 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 17:55:16 -0400 Subject: [PATCH 4/5] Fit the prescribed-benefit properties to savings-credit-only HB and the carers overlap Two behaviours that reached main after these properties were written made them fail with every prescribed benefit at nil: - With a savings-credit-only award, Housing Benefit takes the Secretary of State's assessment plus the savings credit (HB (SPC) Regs 2006 reg 27), so the differential now compares only where neither credit is due. - SDA, IB and MA (and contribution-based JSA and contributory ESA) reduce Carer's Allowance and Carer Support Payment under the overlapping benefits rules (carers_allowance_overlapping_benefits). The pound-for-pound and reg 9 properties now add back the carer's benefit the payment displaces. Measured on the derandomized draws: the differential compares 849 of 1,071 points, 770 of them with a prescribed benefit and 220 with a carer. All four properties pass in 44 s. Co-Authored-By: Claude Opus 5.5 --- ...n_credit_prescribed_benefits_properties.py | 46 +++++++++++++------ 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py index ad6f252a14..defdbfb181 100644 --- a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py +++ b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py @@ -24,9 +24,11 @@ Invariants: 1. Differential: Pension Credit and pension-age Housing Benefit assess the - same income. Where Guarantee Credit is not paid and Housing Benefit income - is above its zero floor, Housing Benefit applicable income plus its income - disregard equals Pension Credit income. The disregard is added back because + same income. Where neither Guarantee Credit nor Savings Credit is due and + Housing Benefit income is above its zero floor, Housing Benefit applicable + income plus its income disregard equals Pension Credit income. (With a + savings-credit-only award, Housing Benefit takes the Secretary of State's + assessment plus the savings credit, HB (SPC) Regs 2006 reg 27, by design.) The disregard is added back because the model applies the HB earnings disregard (HB (SPC) Regs Sch 4) whether or not there are earnings; there are none here. There is no other intended difference on this domain: both measures deduct the claimant's and @@ -36,11 +38,14 @@ 2. Guarantee Credit is non-increasing in each of the four benefits, and while it is positive it falls pound for pound with the extra Pension Credit income, which is the extra benefit less the extra income tax and National - Insurance (the model taxes IB). + Insurance (the model taxes IB), less any Carer's Allowance or Carer Support + Payment the benefit displaces: SDA, IB and MA overlap with them (Social + Security (Overlapping Benefits) Regulations 1979; carers_allowance_overlapping_benefits). 3. Savings Credit income never exceeds Pension Credit income. Adding an amount of a reg 9 source (working tax credit, IB, SDA, MA, contribution-based JSA or contributory ESA) changes Savings Credit income only through income tax - and National Insurance, and never raises Savings Credit. Draws for this + and National Insurance and through any Carer's Allowance or Carer Support + Payment it displaces (as in invariant 2), and never raises Savings Credit. Draws for this invariant are aged 77 or over: the model dates State Pension age as birth year plus the current State Pension age (66 in 2026), so they meet its test of reaching it before the 2016 Savings Credit cutoff. @@ -192,6 +197,7 @@ def test_pension_credit_and_housing_benefit_assess_the_same_prescribed_benefits( families, [ "guarantee_credit", + "savings_credit", "pension_credit_income", "housing_benefit_eligible", "housing_benefit_applicable_income", @@ -199,10 +205,11 @@ def test_pension_credit_and_housing_benefit_assess_the_same_prescribed_benefits( ], ) hb_income = g["housing_benefit_applicable_income"] - # Where Guarantee Credit is not paid, and away from the zero floor on - # Housing Benefit income. + # Where neither credit is due, and away from the zero floor on Housing + # Benefit income. compared = ( (g["guarantee_credit"] <= 0) + & (g["savings_credit"] <= 0) & g["housing_benefit_eligible"].astype(bool) & (hb_income > 0) ) @@ -235,20 +242,25 @@ def test_guarantee_credit_falls_pound_for_pound_with_each_prescribed_benefit( "pension_credit_income", "income_tax", "national_insurance", + "carers_allowance", + "carer_support_payment", ], ) gc = g["guarantee_credit"] pc_income = g["pension_credit_income"] tax = g["income_tax"] + g["national_insurance"] + carer_benefit = g["carers_allowance"] + g["carer_support_payment"] assert np.all(np.diff(gc, axis=-1) <= 0.01), (families, gc) - # The extra Pension Credit income is the extra benefit less the extra tax. + # The extra Pension Credit income is the extra benefit less the extra tax + # and less any carer's benefit the benefit displaces. extra_income = np.diff(pc_income, axis=-1) positive_income = (pc_income[..., :-1] > 0) & (pc_income[..., 1:] > 0) + expected = ( + np.diff(AMOUNTS) - np.diff(tax, axis=-1) + np.diff(carer_benefit, axis=-1) + ) assert np.allclose( - extra_income[positive_income], - (np.diff(AMOUNTS) - np.diff(tax, axis=-1))[positive_income], - atol=0.01, - ), (families, pc_income, tax) + extra_income[positive_income], expected[positive_income], atol=0.01 + ), (families, pc_income, tax, carer_benefit) # While Guarantee Credit is paid, it falls by exactly that much. paid = gc[..., 1:] > 0 assert np.allclose(-np.diff(gc, axis=-1)[paid], extra_income[paid], atol=0.01), ( @@ -287,6 +299,8 @@ def test_reg_9_sources_change_savings_credit_income_only_through_tax( "savings_credit", "income_tax", "national_insurance", + "carers_allowance", + "carer_support_payment", ], ) sc_income = g["savings_credit_income"] @@ -299,9 +313,13 @@ def test_reg_9_sources_change_savings_credit_income_only_through_tax( ) tax = g["income_tax"] + g["national_insurance"] extra_tax = tax[..., 1] - tax[..., 0] + carer_benefit = g["carers_allowance"] + g["carer_support_payment"] + carer_change = carer_benefit[..., 1] - carer_benefit[..., 0] assert np.allclose( - sc_income[..., 1], np.maximum(sc_income[..., 0] - extra_tax, 0), atol=0.01 - ), (families, amount, sc_income, tax) + sc_income[..., 1], + np.maximum(sc_income[..., 0] - extra_tax + carer_change, 0), + atol=0.01, + ), (families, amount, sc_income, tax, carer_benefit) @PROPERTY_SETTINGS From 4422b97b90989f83cc9e375af167aaf41378d045 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 22:23:29 -0400 Subject: [PATCH 5/5] Review fixes: real HB disregard in tests, reg 15(1)(c)-(e) wording, removed-parameter registry From the independent review of #2507: - The Housing Benefit earnings disregard is capped at net earnings, so it is nil without earnings. Cases 16 and 17 no longer override it, and the differential property compares Housing Benefit income with Pension Credit income directly instead of adding the disregard back. - Case 9 runs on the model's 2025 minimum guarantee (227.10), which is already the statutory amount; drop the override and its stale comment. - Reg 15(1)(c) is the IIDB increases (SSCBA ss.104-105); (d) and (e) are payments under the Sch 8 schemes for old industrial injury cases. Fix the wording in the income list, the HB comment, the YAML header and the property docstring, and note that incapacity benefit's non-partner dependant increases (reg 15(1)(h)) are not separated either. - Register the removed savings_credit_excluded_sources in REMOVED_PARAMETERS, so a reform naming it fails with directions. - New cases: savings-credit-only Housing Benefit with IIDB (Case 20) and working-age Housing Benefit with IIDB (Case 21). - Correct invariant 3's description of how the model dates State Pension age. Co-Authored-By: Claude Opus 5.5 --- .../guarantee_credit/income.yaml | 8 +- ...ion_credit_prescribed_benefits_income.yaml | 82 +++++++++++++++---- ...n_credit_prescribed_benefits_properties.py | 29 ++++--- policyengine_uk/utils/parameters.py | 7 ++ .../housing_benefit_applicable_income.py | 5 +- 5 files changed, 97 insertions(+), 34 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml index ec4fa71901..2f570e3c66 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml @@ -26,9 +26,11 @@ values: # Supplement, which is a separate variable. - carers_allowance - carer_support_payment - # Reg. 15(1) does not list these either. Reg. 15(1)(c) and (e) except the - # constant attendance and exceptionally severe disablement increases of - # IIDB, but iidb is the whole reported payment and cannot separate them. + # Reg. 15(1) does not list these either. Reg. 15(1)(c) excepts the constant + # attendance and exceptionally severe disablement increases of IIDB (SSCBA + # ss.104 and 105), but iidb is the whole reported payment and cannot + # separate them. Incapacity benefit's increases for a dependant other than + # a partner (reg. 15(1)(h)) are not separated either. - iidb - sda - incapacity_benefit diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml index 26babd2558..1287e36891 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml @@ -5,8 +5,9 @@ # SPC Regs 2002 reg 15(1): "all social security benefits are prescribed except" # a closed list. The list does not name the four benefits. For IIDB it excepts # the increases under SSCBA 1992 ss.104 and 105 (constant attendance and -# exceptionally severe disablement) and Sch 8 paras 4 and 7(2)(b) (reg -# 15(1)(c)-(e)); (h) excepts increases for a dependant other than a partner. +# exceptionally severe disablement; reg 15(1)(c)); (d) and (e) except payments +# under the Sch 8 schemes for old industrial injury cases, and (h) increases +# for a dependant other than a partner. # Reg 9 makes WTC, IB, SDA and MA (with contribution-based JSA, contributory # ESA and reg 15(5)(d) maintenance) non-qualifying income for Savings Credit; # IIDB stays qualifying income. HB (SPC) Regs 2006 reg 29(1)(j) counts the same @@ -243,10 +244,6 @@ benunits: benunit: members: [person1] - # SI 2025/295 art 29 sets the single standard minimum guarantee at - # 227.10; the model's 2025 parameter holds 221.85, so the statutory - # amount is supplied to isolate the income rule. - minimum_guarantee: 227.10 * 52 households: household: members: [person1] @@ -254,6 +251,7 @@ output: # 1,000 + 2,343.12 = 3,343.12. pension_credit_income: 1_000 + 45.06 * 52 + # SI 2025/295 art 29: single standard minimum guarantee 227.10. # 11,809.20 - 3,343.12 = 8,466.08. guarantee_credit: 227.10 * 52 - (1_000 + 45.06 * 52) @@ -434,10 +432,6 @@ benunits: benunit: members: [person1] - # The model applies the HB earnings disregard (HB (SPC) Regs Sch 4) - # whether or not there are earnings; there are none here, so it is - # set to zero to isolate the income rule. - housing_benefit_applicable_income_disregard: 0 households: household: members: [person1] @@ -448,7 +442,8 @@ # 1,000 + 12,162.80 = 13,162.80, above the 12,376 minimum guarantee. pension_credit_income: 1_000 + 233.90 * 52 guarantee_credit: 0 - # HB (SPC) Regs reg 29(1)(j): the same 13,162.80. + # HB (SPC) Regs reg 29(1)(j): the same 13,162.80. There are no earnings, + # so the earnings disregard (capped at net earnings) is nil. housing_benefit_applicable_income: 1_000 + 233.90 * 52 - name: Case 17, working-age Housing Benefit counts incapacity benefit and SDA. @@ -465,8 +460,6 @@ benunits: benunit: members: [person1, person2] - # As in Case 16 (the working-age disregard is HB Regs 2006 Sch 4). - housing_benefit_applicable_income_disregard: 0 # A legacy Housing Benefit claim: a Universal Credit claim would # passport Housing Benefit (housing_benefit_on_passporting_benefit) # and make its income nil, and this case tests the means test itself. @@ -479,7 +472,8 @@ rent: 6_000 output: income_tax: [0, 0] - # HB Regs 2006 reg 40(1): 7,623.20 + 6,206.20 = 13,829.40. + # HB Regs 2006 reg 40(1): 7,623.20 + 6,206.20 = 13,829.40; no earnings, + # so no earnings disregard. housing_benefit_applicable_income: (146.60 + 103.85 + 15.50) * 52 - name: Case 18, working tax credit does not add to a positive Savings Credit. @@ -543,3 +537,63 @@ savings_credit: 0 # 18,889 - 18,206.20 = 682.80. guarantee_credit: 363.25 * 52 - (12_000 + (103.85 + 15.50) * 52) + +- name: Case 20, savings-credit-only Housing Benefit counts IIDB through the Pension Credit assessment. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 0 + private_pension_income: 200 * 52 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + country: ENGLAND + region: NORTH_WEST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + # 10,400 + 2,432.56 = 12,832.56, above the 12,376 minimum guarantee; no + # income tax (below the personal allowance; IIDB is not taxable). + pension_credit_income: 200 * 52 + 46.78 * 52 + guarantee_credit: 0 + # Amount A = min(0.6 * (238 - 208.07), 0.6 * (246.78 - 208.07)) * 52 + # = 0.6 * 29.93 * 52 = 933.816; amount B = 0.4 * (12,832.56 - + # 12,376) = 182.624; savings credit 751.192, so the award is + # savings credit only. + savings_credit: 751.192 + # HB (SPC) Regs 2006 reg 27: the Secretary of State's assessment plus the + # savings credit payable, no earnings disregard (no earnings): + # 12,832.56 + 751.192 = 13,583.752. On main IIDB was not Pension Credit + # income, Guarantee Credit was paid and Housing Benefit income was nil. + housing_benefit_applicable_income: 13_583.752 + +- name: Case 21, working-age Housing Benefit counts IIDB. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + iidb_reported: 233.90 * 52 + benunits: + benunit: + members: [person1] + # A legacy Housing Benefit claim (see Case 17). + claims_universal_credit: false + households: + household: + members: [person1] + country: ENGLAND + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + income_tax: 0 + # HB Regs 2006 reg 40(1): IIDB at the 100% rate, 233.90 * 52 = 12,162.80. + housing_benefit_applicable_income: 233.90 * 52 diff --git a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py index defdbfb181..67448845bd 100644 --- a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py +++ b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py @@ -5,7 +5,9 @@ industrial injuries disablement benefit (IIDB), severe disablement allowance (SDA), incapacity benefit (IB) or maternity allowance (MA). For IIDB it excepts the constant attendance and exceptionally severe disablement -increases (reg 15(1)(c)-(e)), which the reported IIDB amount cannot separate. +increases (reg 15(1)(c)), which the reported IIDB amount cannot separate; +(d) and (e) except payments under the Sch 8 schemes for old industrial +injury cases. Pension-age Housing Benefit counts the same benefits under HB (SPC) Regs 2006 reg 29(1)(j), whose exceptions for these benefits match reg 15(1)'s. Reg 9 makes working tax credit, IB, SDA, MA, contribution-based JSA and contributory @@ -26,12 +28,11 @@ 1. Differential: Pension Credit and pension-age Housing Benefit assess the same income. Where neither Guarantee Credit nor Savings Credit is due and Housing Benefit income is above its zero floor, Housing Benefit applicable - income plus its income disregard equals Pension Credit income. (With a - savings-credit-only award, Housing Benefit takes the Secretary of State's - assessment plus the savings credit, HB (SPC) Regs 2006 reg 27, by design.) The disregard is added back because - the model applies the HB earnings disregard (HB (SPC) Regs Sch 4) whether or - not there are earnings; there are none here. There is no other intended - difference on this domain: both measures deduct the claimant's and + income equals Pension Credit income. (With a savings-credit-only award, + Housing Benefit takes the Secretary of State's assessment plus the savings + credit, HB (SPC) Regs 2006 reg 27, by design.) There are no earnings, so + the Housing Benefit earnings disregard, which is capped at net earnings, is + nil. There is no other intended difference on this domain: both measures deduct the claimant's and partner's income tax and National Insurance, and neither counts the Scottish Carer Supplement (SPC Regs reg 15(1)(ri); HB (SPC) Regs reg 29(1)(j)(xviiha)). @@ -45,10 +46,10 @@ of a reg 9 source (working tax credit, IB, SDA, MA, contribution-based JSA or contributory ESA) changes Savings Credit income only through income tax and National Insurance and through any Carer's Allowance or Carer Support - Payment it displaces (as in invariant 2), and never raises Savings Credit. Draws for this - invariant are aged 77 or over: the model dates State Pension age as birth - year plus the current State Pension age (66 in 2026), so they meet its - test of reaching it before the 2016 Savings Credit cutoff. + Payment it displaces (as in invariant 2), and never raises Savings Credit. + Draws for this invariant are aged 77 or over, old enough to have reached + State Pension age, on the model's timetable by date of birth, before the + April 2016 Savings Credit cutoff. 4. Boundary: on the private-pension step where Guarantee Credit ends, household net income, excluding the TV licence fee, does not fall. The free TV licence for over-75s on Pension Credit is a genuine statutory cliff, so @@ -201,7 +202,6 @@ def test_pension_credit_and_housing_benefit_assess_the_same_prescribed_benefits( "pension_credit_income", "housing_benefit_eligible", "housing_benefit_applicable_income", - "housing_benefit_applicable_income_disregard", ], ) hb_income = g["housing_benefit_applicable_income"] @@ -213,11 +213,10 @@ def test_pension_credit_and_housing_benefit_assess_the_same_prescribed_benefits( & g["housing_benefit_eligible"].astype(bool) & (hb_income > 0) ) - before_disregard = hb_income + g["housing_benefit_applicable_income_disregard"] pc_income = g["pension_credit_income"] - assert np.allclose(before_disregard[compared], pc_income[compared], atol=0.01), ( + assert np.allclose(hb_income[compared], pc_income[compared], atol=0.01), ( families, - before_disregard[compared], + hb_income[compared], pc_income[compared], ) diff --git a/policyengine_uk/utils/parameters.py b/policyengine_uk/utils/parameters.py index e7d1be6aed..66f93aa66a 100644 --- a/policyengine_uk/utils/parameters.py +++ b/policyengine_uk/utils/parameters.py @@ -179,6 +179,13 @@ class RemovedParameter(NamedTuple): "gov.simulation.labour_supply_responses.obr_elasticities": RemovedParameter( "removed OBR elasticities switch", _OBR_ELASTICITIES_REPLACEMENT ), + "gov.dwp.pension_credit.income.savings_credit_excluded_sources": RemovedParameter( + "removed duplicate Savings Credit excluded-income list", + "The model never read it. Change " + "gov.dwp.pension_credit.savings_credit.excluded_income, the list of " + "income that is not qualifying income for Savings Credit (SPC Regs " + "2002 reg 9), instead.", + ), } diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 17e6697cb1..f376690d42 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -69,8 +69,9 @@ def formula(benunit, period, parameters): "ssmg", # Counted by both regimes: HB (SPC) Regs 2006 reg 29(1)(j), whose # exceptions touching these are the IIDB increases under SSCBA - # ss.104-105 and Sch 8 (heads (iii)-(v)) and non-partner dependant - # increases (head (viii)), and HB Regs 2006 reg 40 with Sch 5, + # ss.104-105 (head (iii)) and non-partner dependant increases (head + # (viii)); heads (iv) and (v) cover the Sch 8 schemes for old + # industrial injury cases. And HB Regs 2006 reg 40 with Sch 5, # whose para 9 attendance allowance disregard covers the # ss.104-105 increases (reg 2(1)). iidb is the whole reported # payment and cannot separate those increases.