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..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,3 +26,12 @@ values: # Supplement, which is a separate variable. - carers_allowance - carer_support_payment + # 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 + - 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..1287e36891 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_prescribed_benefits_income.yaml @@ -0,0 +1,599 @@ +# 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 +# the increases under SSCBA 1992 ss.104 and 105 (constant attendance and +# 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 +# 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 and 329.75 couple. +# SI 2025/295 Sch 1 Part V: IIDB 20% 45.06; art 29: single standard minimum +# guarantee 227.10. +# 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 + 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] + households: + household: + members: [person1] + country: ENGLAND + 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) + +- 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] + 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. 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. + 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] + # 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] + 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; 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. + 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) + +- 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 new file mode 100644 index 0000000000..67448845bd --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py @@ -0,0 +1,350 @@ +"""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)), 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 +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 neither Guarantee Credit nor Savings Credit is due and + Housing Benefit income is above its zero floor, Housing Benefit applicable + 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)). +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), 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 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, 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 + 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", + "savings_credit", + "pension_credit_income", + "housing_benefit_eligible", + "housing_benefit_applicable_income", + ], + ) + hb_income = g["housing_benefit_applicable_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) + ) + pc_income = g["pension_credit_income"] + assert np.allclose(hb_income[compared], pc_income[compared], atol=0.01), ( + families, + hb_income[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", + "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 + # 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], 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), ( + 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", + "carers_allowance", + "carer_support_payment", + ], + ) + 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] + 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 + carer_change, 0), + atol=0.01, + ), (families, amount, sc_income, tax, carer_benefit) + + +@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/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 b97d9ac316..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 @@ -67,6 +67,17 @@ def formula(benunit, period, parameters): "statutory_sick_pay", "statutory_maternity_pay", "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 (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. + "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..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 @@ -21,8 +21,15 @@ 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, + # 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( + max_(0, person(source, period)) * is_claimant_or_partner + ) + else: + excluded_income += add(benunit, period, [source]) return max_(0, guarantee_credit_income - excluded_income)