From aa45dd7603bc15df5430ba9fa55541edc95a6036 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 06:58:33 -0400 Subject: [PATCH 01/13] Use the State Pension Credit qualifying age, and start-of-year status for Class 4 State Pension Credit Act 2002 s.1(6) sets the qualifying age as a woman's pensionable age, and for a man that of a woman born on the same day. Pension Credit, Universal Credit (WRA 2012 s.4(1)(b), (4)), Housing Benefit (reg 5 of SI 2006/213 and 2006/214; SI 2014/1230 reg 6A(4)), Council Tax Reduction pension-age schemes, Income Support (SSCBA 1992 s.124(1)(aa)), the benefit cap's pensioner exception and Winter Fuel Payment (SI 2000/729 reg 2(1)(b)) now use it instead of the person's own pensionable age. They differ only for men born before 6 December 1953, so only in 2018-19 and earlier. Class 4 NI (SI 2001/1004 reg 91(a)) now excepts only people over State Pension age at the beginning of the tax year (6 April), not on 6 October. Co-Authored-By: Claude Opus 5.5 --- ...4-state-pension-age-start-of-year.fixed.md | 1 + .../spc-qualifying-age-class-4-spa.added.md | 1 + .../spc-qualifying-age-class-4-spa.fixed.md | 1 + docs/book/programs/gov/dwp/state-pension.md | 22 ++ .../state_pension_credit_qualifying_age.yaml | 321 ++++++++++++++++++ .../class_4/ni_class_4_liable.yaml | 145 ++++++++ ...est_state_pension_credit_qualifying_age.py | 211 ++++++++++++ policyengine_uk/utils/state_pension_age.py | 76 +++++ .../housing_benefit_applicable_income.py | 8 +- .../housing_benefit_tariff_income.py | 14 +- .../housing_benefit_applicable_amount.py | 14 +- .../housing_benefit_assessable_capital.py | 8 +- .../housing_benefit_eligible.py | 15 +- .../gov/dwp/income_support_eligible.py | 8 +- .../gov/dwp/is_benefit_cap_exempt_other.py | 11 +- .../gov/dwp/months_since_state_pension_age.py | 13 +- ...ned_state_pension_credit_qualifying_age.py | 29 ++ .../is_pension_credit_eligible.py | 11 +- .../pension_credit_assessable_capital.py | 13 +- .../state_pension_credit_qualifying_age.py | 43 +++ .../variables/gov/dwp/state_pension_age.py | 40 +-- .../dwp/universal_credit/is_uc_eligible.py | 10 +- .../gov/dwp/winter_fuel_allowance.py | 13 +- .../national_insurance/class_4/ni_class_4.py | 2 +- .../class_4/ni_class_4_liable.py | 33 ++ .../class_4/ni_class_4_main.py | 2 +- ...council_tax_reduction_applicable_amount.py | 14 +- ...l_tax_reduction_household_has_pensioner.py | 8 +- 28 files changed, 1003 insertions(+), 84 deletions(-) create mode 100644 changelog.d/class-4-state-pension-age-start-of-year.fixed.md create mode 100644 changelog.d/spc-qualifying-age-class-4-spa.added.md create mode 100644 changelog.d/spc-qualifying-age-class-4-spa.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml create mode 100644 policyengine_uk/tests/test_state_pension_credit_qualifying_age.py create mode 100644 policyengine_uk/utils/state_pension_age.py create mode 100644 policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py create mode 100644 policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py create mode 100644 policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py diff --git a/changelog.d/class-4-state-pension-age-start-of-year.fixed.md b/changelog.d/class-4-state-pension-age-start-of-year.fixed.md new file mode 100644 index 0000000000..e9b6f49ad1 --- /dev/null +++ b/changelog.d/class-4-state-pension-age-start-of-year.fixed.md @@ -0,0 +1 @@ +Class 4 National Insurance now stops from the tax year after State Pension age is reached (Social Security (Contributions) Regulations 2001 reg 91(a)): a person over State Pension age on 6 April is excepted, and one who reaches it during the year pays Class 4 for that whole year. Before, anyone over State Pension age on 6 October was excepted. diff --git a/changelog.d/spc-qualifying-age-class-4-spa.added.md b/changelog.d/spc-qualifying-age-class-4-spa.added.md new file mode 100644 index 0000000000..03f0ca6072 --- /dev/null +++ b/changelog.d/spc-qualifying-age-class-4-spa.added.md @@ -0,0 +1 @@ +Added `state_pension_credit_qualifying_age` and `has_attained_state_pension_credit_qualifying_age`, the qualifying age for State Pension Credit from each person's date of birth, and `ni_class_4_liable`, Class 4 liability by age at the start of the tax year. diff --git a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md new file mode 100644 index 0000000000..b15d7e797a --- /dev/null +++ b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md @@ -0,0 +1 @@ +Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap's pensioner exception and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age. diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index db743051f2..36f1cbd8e5 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -70,6 +70,28 @@ extracts. The weighted share of each age over State Pension age then matches the statute: three quarters of 66-year-olds in 2026-27, a quarter in 2027-28 and none from 2028-29. +Other programmes test State Pension age in two ways: + +- **The qualifying age for State Pension Credit** (State Pension Credit Act + 2002 s.1(6)) is a woman's State Pension age, and for a man the State + Pension age of a woman born on the same day. `state_pension_credit_qualifying_age` + applies the timetable without rule (1), and + `has_attained_state_pension_credit_qualifying_age` is whether it is reached + by 6 October. Pension Credit, Universal Credit (Welfare Reform Act 2012 + s.4(1)(b)), Housing Benefit (Housing Benefit Regulations 2006 reg 5 in both + sets), Council Tax Reduction's pension-age schemes, Income Support + (SSCBA 1992 s.124(1)(aa)), the benefit cap's pensioner exception and + Winter Fuel Payment (to September 2024) use it. It differs from + `is_SP_age` only for men born before 6 December 1953, so only in 2018-19 + and earlier: a man born on 6 April 1952 reached it on 6 May 2014, nearly + three years before his State Pension age of 65. +- **Class 4 National Insurance** stops from the tax year after State Pension + age: a person over it at the beginning of the tax year (6 April) is + excepted (Social Security (Contributions) Regulations 2001 reg 91(a)), so + `ni_class_4_liable` needs `months_since_state_pension_age` below 6. + Class 1 employee contributions stop at State Pension age itself (SSCBA + 1992 s.6(3)), which the annual model reads as `is_SP_age`. + A person attains an age at the start of the anniversary of their birth (Family Law Reform Act 1969 s.9(1)), and an age of "N years and M months" on the same day of the month, or the month's last day where that day does not diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml new file mode 100644 index 0000000000..c3a18c319f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -0,0 +1,321 @@ +# The qualifying age for State Pension Credit (State Pension Credit Act 2002 +# s.1(6)): a woman's pensionable age, and for a man the pensionable age of a +# woman born on the same day. Men born before 6 December 1953 have a +# pensionable age of 65 (Pensions Act 1995 Sch 4 para 1 rule (1)) but reach the +# qualifying age on the day in table 1 for a woman with their date of birth. +# Universal Credit (Welfare Reform Act 2012 s.4(1)(b), (4)), Housing Benefit +# (SI 2006/213 and 2006/214 reg 5; SI 2014/1230 reg 6A(4)), Council Tax +# Reduction (SI 2012/2885 reg 3), Income Support (SSCBA 1992 s.124(1)(aa)) and +# Winter Fuel Payment (SI 2000/729 reg 2(1)(b)) use the qualifying age. +# +# age and months_since_last_birthday place the date of birth: the person was +# born that many years and months before 6 October of the fiscal year. +# Expected days come from table 1 of Sch 4 para 1. + +- name: Case 1, man born 6 April 1952 reached the qualifying age in 2014 but not State Pension age in 2016-17. + # Born 6 April 1952: table 1 puts a woman born 6 April to 5 May 1952 at + # pensionable age on 6 May 2014, age 62 years and 1 month. His own + # pensionable age is 65, on 6 April 2017, after 6 October 2016. + period: 2016 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 62.083 + has_attained_state_pension_credit_qualifying_age: true + state_pension_age: 65 + is_SP_age: false + is_pension_credit_eligible: true + is_uc_eligible: false + council_tax_reduction_household_has_pensioner: true + is_benefit_cap_exempt_other: true + # SI 2000/729 reg 2(1)(i): 200 for a person under 80 not in residential care. + winter_fuel_allowance: 200 + +- name: Case 2, a woman born the same day has the same qualifying age and State Pension age. + period: 2016 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: false + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 62.083 + has_attained_state_pension_credit_qualifying_age: true + state_pension_age: 62.083 + is_SP_age: true + is_pension_credit_eligible: true + +- name: Case 3, man born 6 April 1952 already had the qualifying age in 2015-16. + period: 2015 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 63 + months_since_last_birthday: 6 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 62.083 + has_attained_state_pension_credit_qualifying_age: true + is_SP_age: false + is_pension_credit_eligible: true + is_uc_eligible: false + +- name: Case 4, man born 6 April 1950 is over both ages in 2015-16. + # A woman born 6 April to 5 May 1950 reaches pensionable age on 6 May 2010 + # (age 60 years and 1 month); he reaches 65 on 6 April 2015. + period: 2015 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 65 + months_since_last_birthday: 6 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 60.083 + has_attained_state_pension_credit_qualifying_age: true + state_pension_age: 65 + is_SP_age: true + +- name: Case 5, man born 6 June 1953 reached the qualifying age in March 2017. + # Table 1: a woman born 6 June to 5 July 1953 reaches pensionable age on 6 + # March 2017 (63 years and 9 months), before 6 October 2017. His own + # pensionable age, 65, is on 6 June 2018. + period: 2017 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 64 + months_since_last_birthday: 4 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 63.75 + has_attained_state_pension_credit_qualifying_age: true + state_pension_age: 65 + is_SP_age: false + is_pension_credit_eligible: true + is_uc_eligible: false + winter_fuel_allowance: 200 + +- name: Case 6, man born 6 September 1953 is under both ages in 2017-18. + # Table 1: a woman born 6 September to 5 October 1953 reaches pensionable + # age on 6 March 2018 (64 years and 6 months), after 6 October 2017. + period: 2017 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 64 + months_since_last_birthday: 1 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 64.5 + has_attained_state_pension_credit_qualifying_age: false + is_SP_age: false + is_pension_credit_eligible: false + is_uc_eligible: true + council_tax_reduction_household_has_pensioner: false + is_benefit_cap_exempt_other: false + winter_fuel_allowance: 0 + +- name: Case 7, the same man is over both ages in 2018-19. + # Qualifying age on 6 March 2018; pensionable age (65) on 6 September 2018. + period: 2018 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 65 + months_since_last_birthday: 1 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 64.5 + has_attained_state_pension_credit_qualifying_age: true + state_pension_age: 65 + is_SP_age: true + +- name: Case 8, man born 6 December 1953 follows the common timetable, so both ages agree. + # Rule (1) covers men born before 6 December 1953 only. Table 2 puts + # everyone born 6 December 1953 to 5 January 1954 at pensionable age on 6 + # March 2019 (65 years and 3 months). + period: 2018 + absolute_error_margin: 0.001 + input: + people: + person1: + age: 64 + months_since_last_birthday: 10 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + state_pension_credit_qualifying_age: 65.25 + state_pension_age: 65.25 + has_attained_state_pension_credit_qualifying_age: false + is_SP_age: false + +- name: Case 9, a man over the qualifying age can make a new Housing Benefit claim. + # SI 2014/1230 reg 6A(4): a single person who has reached the qualifying age + # for State Pension Credit may claim Housing Benefit without an existing + # award. Born 6 April 1952, he reached it on 6 May 2014. + period: 2016 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + housing_benefit_reported: 0 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + tenure_type: RENT_FROM_COUNCIL + output: + housing_benefit_eligible: true + +- name: Case 10, a man under the qualifying age needs an existing award to keep Housing Benefit. + # Born 6 September 1953: qualifying age on 6 March 2018. + period: 2017 + input: + people: + person1: + age: 64 + months_since_last_birthday: 1 + is_male: true + housing_benefit_reported: 0 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + tenure_type: RENT_FROM_COUNCIL + output: + housing_benefit_eligible: false + +- name: Case 11, Income Support stops at the qualifying age, not at the man's pensionable age. + # SSCBA 1992 s.124(1)(aa). A carer born 6 April 1952 with an existing award. + period: 2016 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + is_carer_for_benefits: true + income_support_reported: 1_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + income_support_eligible: false + +- name: Case 12, the same carer is eligible for Income Support below the qualifying age. + # Born 6 September 1953: under the qualifying age on 6 October 2017. + period: 2017 + input: + people: + person1: + age: 64 + months_since_last_birthday: 1 + is_male: true + is_carer_for_benefits: true + income_support_reported: 1_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + income_support_eligible: true + +- name: Case 13, a couple both born 6 June 1953 is not a mixed-age couple in 2017-18. + # She reached pensionable age, and he the qualifying age, on 6 March 2017; + # his own pensionable age is 6 June 2018. State Pension Credit Act 2002 + # s.4(1A) excludes a couple only where the other member "has not attained + # the qualifying age", so they can claim Pension Credit, not Universal + # Credit. + period: 2017 + input: + people: + person1: + age: 64 + months_since_last_birthday: 4 + is_male: true + person2: + age: 64 + months_since_last_birthday: 4 + is_male: false + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + output: + has_attained_state_pension_credit_qualifying_age: [true, true] + is_SP_age: [false, true] + is_pension_credit_eligible: true + is_uc_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml new file mode 100644 index 0000000000..a73f2cc052 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml @@ -0,0 +1,145 @@ +# Class 4 National Insurance stops from the tax year after State Pension age. +# Social Security (Contributions) Regulations 2001 reg 91(a) excepts an earner +# who "at the beginning of a year of assessment is over pensionable age"; HMRC +# NIM24510: "A person who attains State pension age during the course of the +# year of assessment remains liable for Class 4 NICs for the whole of that +# year." +# +# age and months_since_last_birthday place the date of birth: the person was +# born that many years and months before 6 October of the fiscal year. +# Class 4 for 2016-17: 9% of profits between 8,060 and 43,000. For 2024-25 to +# 2026-27: 6% of profits between 12,570 and 50,270. + +- name: Case 1, born 6 June 1960 reaches State Pension age on 6 September 2026 and pays Class 4 for 2026-27. + # Table 3: born 6 June to 5 July 1960, 66 years and 3 months. Over it on 6 + # October 2026 but not on 6 April 2026. (30,000 - 12,570) x 6% = 1,045.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + months_since_last_birthday: 4 + self_employment_income: 30_000 + output: + is_SP_age: true + ni_class_4_liable: true + ni_class_4_main: 1_045.80 + ni_class_4: 1_045.80 + +- name: Case 2, the same person is over State Pension age on 6 April 2027 and pays no Class 4 for 2027-28. + period: 2027 + input: + people: + person1: + age: 67 + months_since_last_birthday: 4 + self_employment_income: 30_000 + output: + ni_class_4_liable: false + ni_class_4_main: 0 + ni_class_4: 0 + +- name: Case 3, reaching State Pension age after 6 October still leaves the whole year liable. + # Born 6 September 1960: 66 years and 6 months, so 6 March 2027. Under it on + # both 6 April and 6 October 2026. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + months_since_last_birthday: 1 + self_employment_income: 30_000 + output: + is_SP_age: false + ni_class_4_liable: true + ni_class_4: 1_045.80 + +- name: Case 4, the same person is excepted for 2027-28. + period: 2027 + input: + people: + person1: + age: 67 + months_since_last_birthday: 1 + self_employment_income: 30_000 + output: + ni_class_4_liable: false + ni_class_4: 0 + +- name: Case 5, reaching State Pension age on 6 April itself excepts that year. + # Born 6 April 1959: rule (6), 66, so the commencement of 6 April 2025, the + # first day of 2025-26. Pensionable age is attained at the commencement of + # the day, so the person is over it at the beginning of the year. + period: 2025 + input: + people: + person1: + age: 66 + months_since_last_birthday: 6 + self_employment_income: 30_000 + output: + ni_class_4_liable: false + ni_class_4: 0 + +- name: Case 6, the same person paid Class 4 for 2024-25. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + months_since_last_birthday: 6 + self_employment_income: 30_000 + output: + ni_class_4_liable: true + ni_class_4: 1_045.80 + +- name: Case 7, reaching State Pension age a few days into the year leaves that year liable. + # 5.9 months since the last birthday on 6 October 2025 places the birth on + # 9 April 1959, so State Pension age is 9 April 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + months_since_last_birthday: 5.9 + self_employment_income: 30_000 + output: + ni_class_4_liable: true + ni_class_4: 1_045.80 + +- name: Case 8, a man reaching 65 in June 2016 pays Class 4 for 2016-17. + # Born 6 June 1951: rule (1), 65, so 6 June 2016. + # (30,000 - 8,060) x 9% = 1,974.60. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 65 + months_since_last_birthday: 4 + is_male: true + self_employment_income: 30_000 + output: + is_SP_age: true + ni_class_4_liable: true + ni_class_4: 1_974.60 + +- name: Case 9, Class 1 employee contributions still stop at State Pension age within the year. + # SSCBA 1992 s.6(3): no primary Class 1 contributions on earnings paid after + # pensionable age. The annual model treats the person as over it for the + # year from 6 October; Class 4 follows reg 91 instead. + period: 2026 + input: + people: + person1: + age: 66 + months_since_last_birthday: 4 + employment_income: 30_000 + output: + ni_liable: false + ni_class_1_employee: 0 + ni_class_4_liable: true diff --git a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py new file mode 100644 index 0000000000..feb55c4285 --- /dev/null +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -0,0 +1,211 @@ +"""The qualifying age for State Pension Credit, and Class 4 liability by age. + +State Pension Credit Act 2002 s.1(6): the qualifying age is a woman's +pensionable age, and for a man the pensionable age of a woman born on the same +day. The model is checked against the day-level reading of Pensions Act 1995 +Sch 4 para 1 in test_state_pension_age, which shares no code with the model: +a man's qualifying day is that reference's day for a woman born on his birthday. + +Class 4: Social Security (Contributions) Regulations 2001 reg 91(a) excepts a +person over pensionable age at the beginning of the year of assessment (6 +April), so someone who reaches it during a year is liable for that whole year. +""" + +from datetime import date + +import numpy as np +import pytest +from hypothesis import given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.tests.test_state_pension_age import ( + YEARS, + differential_simulation, + grid_months, + grid_months_to_date, + reference_attainment_day, +) + +# Men born before this date have a pensionable age of 65 (Sch 4 para 1 rule +# (1)); for everyone else pensionable age and the qualifying age coincide. +MALE_RULE_BORN_BEFORE = date(1953, 12, 6) +# The last fiscal year in which the two can differ: men born on 5 December 1953 +# reach 65 on 5 December 2018. +LAST_YEAR_THEY_DIFFER = 2018 + + +def reference_qualifying_day(birth: date) -> date: + """The day a person reaches the qualifying age for State Pension Credit: + the day a woman born on the same day attains pensionable age.""" + return reference_attainment_day(birth, male=False) + + +def one_person_units(people: dict) -> dict: + """A situation with each person in their own benefit unit and household.""" + return { + "people": people, + "benunits": {f"b_{name}": {"members": [name]} for name in people}, + "households": {f"h_{name}": {"members": [name]} for name in people}, + } + + +@pytest.mark.parametrize("year", YEARS) +def test_qualifying_age_matches_statute_for_every_birth_date(year): + """For births every third day from 1945 to 1984 and on both sides of every + statutory boundary, at both sexes: the day the model puts the qualifying + age on is the statute's day for a woman born the same day, and status on 6 + October follows it.""" + sim, keys = differential_simulation() + qualifying_age = sim.calculate("state_pension_credit_qualifying_age", year) + attained = sim.calculate("has_attained_state_pension_credit_qualifying_age", year) + mid_year = date(year, 10, 6) + mismatches = [] + for i, (birth, male) in enumerate(keys): + expected_day = reference_qualifying_day(birth) + model_day = grid_months_to_date(grid_months(birth) + 12 * qualifying_age[i]) + if model_day != expected_day or attained[i] != (expected_day <= mid_year): + mismatches.append((birth, male, expected_day, model_day, attained[i])) + assert not mismatches, mismatches[:10] + + +@pytest.mark.parametrize("year", YEARS) +def test_class_4_liability_matches_statute_for_every_birth_date(year): + """Class 4 liability by age is exactly "not over pensionable age on 6 + April", read from the statute for every birth date and both sexes.""" + sim, keys = differential_simulation() + liable = sim.calculate("ni_class_4_liable", year) + start_of_year = date(year, 4, 6) + mismatches = [ + (birth, male, reference_attainment_day(birth, male), liable[i]) + for i, (birth, male) in enumerate(keys) + if liable[i] == (reference_attainment_day(birth, male) <= start_of_year) + ] + assert not mismatches, mismatches[:10] + + +@pytest.mark.parametrize("year", YEARS) +def test_qualifying_age_and_pensionable_age_differ_only_for_men_born_before_6_december_1953( + year, +): + """The qualifying age never exceeds a person's pensionable age; the two are + equal for women and for men born on or after 6 December 1953; so status at + the qualifying age holds wherever is_SP_age does, and from 2019-20 the two + agree for everyone.""" + sim, keys = differential_simulation() + qualifying_age = sim.calculate("state_pension_credit_qualifying_age", year) + spa = sim.calculate("state_pension_age", year) + attained = sim.calculate("has_attained_state_pension_credit_qualifying_age", year) + sp_age = sim.calculate("is_SP_age", year) + male_rule = np.array( + [male and birth < MALE_RULE_BORN_BEFORE for birth, male in keys] + ) + assert np.all(qualifying_age <= spa + 1e-6) + assert np.allclose(qualifying_age[~male_rule], spa[~male_rule], atol=1e-6) + assert np.all(attained >= sp_age) + if year > LAST_YEAR_THEY_DIFFER: + assert np.array_equal(attained, sp_age) + + +def test_the_two_ages_do_differ_in_2015_to_2018(): + """The years before 2019-20 do contain men over the qualifying age and + under their own pensionable age, so the checks above are not vacuous.""" + sim, keys = differential_simulation() + for year in (2015, 2016, 2017, 2018): + attained = sim.calculate( + "has_attained_state_pension_credit_qualifying_age", year + ) + sp_age = sim.calculate("is_SP_age", year) + differ = attained & ~sp_age + assert differ.any() + assert all( + male and birth < MALE_RULE_BORN_BEFORE + for (birth, male), d in zip(keys, differ) + if d + ) + + +@settings(max_examples=30, deadline=None) +@given( + births=st.lists( + st.dates(min_value=date(1945, 1, 1), max_value=date(1965, 12, 31)), + min_size=1, + max_size=40, + unique=True, + ), + male=st.booleans(), + year=st.integers(min_value=2015, max_value=2030), +) +def test_consumers_use_the_qualifying_age(births, male, year): + """For single people with no income or capital, each programme that the + statute ties to the qualifying age for State Pension Credit follows it, + not the person's own pensionable age: Pension Credit is available exactly + from it, Universal Credit exactly before it, and the pension-age Council + Tax Reduction scheme and the benefit cap exception exactly from it. Winter + Fuel Payment follows it until 2023-24, when it still needed no benefit.""" + people = {} + for i, birth in enumerate(births): + age_in_months = grid_months(date(year, 10, 6)) - grid_months(birth) + age = int(age_in_months // 12) + people[f"p{i}"] = { + "age": {year: age}, + "months_since_last_birthday": {year: age_in_months - 12 * age}, + "is_male": {year: male}, + } + sim = Simulation(situation=one_person_units(people)) + attained = sim.calculate("has_attained_state_pension_credit_qualifying_age", year) + expected = np.array( + [reference_qualifying_day(b) <= date(year, 10, 6) for b in births] + ) + assert np.array_equal(attained, expected) + assert np.array_equal(sim.calculate("is_pension_credit_eligible", year), attained) + assert np.array_equal(sim.calculate("is_uc_eligible", year), ~attained) + assert np.array_equal( + sim.calculate("council_tax_reduction_household_has_pensioner", year), attained + ) + assert np.array_equal(sim.calculate("is_benefit_cap_exempt_other", year), attained) + if year <= 2023: + assert np.array_equal( + sim.calculate("winter_fuel_allowance", year) > 0, attained + ) + + +@settings(max_examples=40, deadline=None) +@given( + births=st.lists( + st.dates(min_value=date(1945, 1, 1), max_value=date(1975, 12, 31)), + min_size=1, + max_size=30, + unique=True, + ), + male=st.booleans(), + year=st.integers(min_value=2015, max_value=2045), +) +def test_class_4_liability_properties(births, male, year): + """For any dates of birth: a person liable for Class 4 by age next year is + liable this year (liability never resumes); liability holds exactly while + State Pension age is reached after 6 April, so wherever the person is under + State Pension age on 6 October; and profits are only charged when liable.""" + people = {} + for i, birth in enumerate(births): + ages, months = {}, {} + for y in (year, year + 1): + age_in_months = grid_months(date(y, 10, 6)) - grid_months(birth) + ages[y] = int(age_in_months // 12) + months[y] = age_in_months - 12 * ages[y] + people[f"p{i}"] = { + "age": ages, + "months_since_last_birthday": months, + "is_male": {y: male for y in (year, year + 1)}, + "self_employment_income": {y: 60_000 for y in (year, year + 1)}, + } + sim = Simulation(situation=one_person_units(people)) + liable = sim.calculate("ni_class_4_liable", year) + liable_next = sim.calculate("ni_class_4_liable", year + 1) + since = sim.calculate("months_since_state_pension_age", year) + sp_age = sim.calculate("is_SP_age", year) + class_4 = sim.calculate("ni_class_4", year) + assert np.all(liable_next <= liable) + assert np.array_equal(liable, since < 6) + assert np.all(liable[~sp_age]) + assert np.all((class_4 > 0) == liable) diff --git a/policyengine_uk/utils/state_pension_age.py b/policyengine_uk/utils/state_pension_age.py new file mode 100644 index 0000000000..7904e1029b --- /dev/null +++ b/policyengine_uk/utils/state_pension_age.py @@ -0,0 +1,76 @@ +"""Pensionable age by date of birth, and ages that follow its timetable. + +Pensions Act 1995 Schedule 4 paragraph 1 sets pensionable age (State Pension +age) by date of birth. Rule (1) gives men born before 6 December 1953 the age +of 65; the other rules set an age or a day for everyone else, including every +woman. The qualifying age for State Pension Credit (State Pension Credit Act +2002 s.1(6)) is a woman's pensionable age, and for a man the pensionable age of +a woman born on the same day: the same timetable without rule (1). + +Dates are measured in months on the grid in policyengine_uk.utils.dates, whose +months start on the 6th, so the middle of the fiscal year (6 October) is a +whole month. +""" + +import numpy as np + +from policyengine_uk.utils.dates import ( + add_months_to_yyyymmdd, + exact_age_in_months, + grid_month, + grid_months_to_yyyymmdd, + yyyymmdd_to_grid_months, +) + + +def date_of_birth(age, months_since_last_birthday, year: int) -> tuple: + """The instant of birth, in grid months, and the day of birth (YYYYMMDD). + + The person was born age years and months_since_last_birthday months before + 6 October of the fiscal year starting in ``year``. The day of birth is the + one starting at or after that instant, so the person's legal age on 6 + October is age. + """ + birth = grid_month(year, 10) - exact_age_in_months(age, months_since_last_birthday) + return birth, grid_months_to_yyyymmdd(birth) + + +def age_attaining_pensionable_age( + timetable, birth, birth_date, male_rule +) -> np.ndarray: + """The age, in years, at which pensionable age is attained. + + ``timetable`` is the gov.dwp.state_pension.age parameter node. Where + ``male_rule`` is true, rule (1) applies (the male age); elsewhere the + timetable in rules (2) to (10) sets an age or a day, and the later of the + two is when pensionable age is attained. + """ + age_rule = np.where( + male_rule, timetable.male.age, timetable.age_by_birth_date.calc(birth_date) + ) + day_rule = np.where(male_rule, 0, timetable.day_by_birth_date.calc(birth_date)) + day_rule_month = np.where( + day_rule > 0, + yyyymmdd_to_grid_months(np.maximum(day_rule, 10101)), + -np.inf, + ) + # An age of N years and M months is attained at the commencement of the + # anniversary (Family Law Reform Act 1969 s.9(1)): the same day of the + # month, or the month's last day where that day does not exist, which + # also gives the three days in rule (7A). + anniversary = yyyymmdd_to_grid_months(add_months_to_yyyymmdd(birth_date, age_rule)) + attained = np.maximum(anniversary, day_rule_month) + return (attained - birth) / 12 + + +def months_since_attaining(age, months_since_last_birthday, attained_age) -> np.ndarray: + """Months between attaining ``attained_age`` and 6 October; negative if + the person attains it later. + + Rounding to a thousandth of a month (under an hour) removes float error, + so attaining the age exactly on 6 October, or exactly on 6 April six + months earlier, counts as having attained it then. + """ + age_in_months = exact_age_in_months(age, months_since_last_birthday) + months_since = age_in_months - 12 * np.asarray(attained_age, dtype=np.float64) + return np.round(months_since, 3) 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 999da75d2a..548804ae27 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 @@ -9,7 +9,9 @@ class housing_benefit_applicable_income(Variable): unit = GBP def formula(benunit, period, parameters): - any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) + any_over_qualifying_age = benunit.any( + benunit.members("has_attained_state_pension_credit_qualifying_age", period) + ) BENUNIT_MEANS_TESTED_BENEFITS = [ "child_benefit", "income_support", @@ -63,5 +65,7 @@ def formula(benunit, period, parameters): - disregard - childcare_element, ) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + guarantee_credit = any_over_qualifying_age & ( + benunit("guarantee_credit", period) > 0 + ) return where(guarantee_credit, 0, applicable_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py index 70b6c8bbc4..364b058b4c 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py @@ -11,21 +11,25 @@ class housing_benefit_tariff_income(Variable): def formula(benunit, period, parameters): capital = benunit("housing_benefit_assessable_capital", period) - any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + any_over_qualifying_age = benunit.any( + benunit.members("has_attained_state_pension_credit_qualifying_age", period) + ) + guarantee_credit = any_over_qualifying_age & ( + benunit("guarantee_credit", period) > 0 + ) p = parameters(period).gov.dwp.housing_benefit.means_test.capital threshold = where( - any_over_SP_age, + any_over_qualifying_age, p.pension_age.tariff_income.threshold, p.working_age.tariff_income.threshold, ) step = where( - any_over_SP_age, + any_over_qualifying_age, p.pension_age.tariff_income.step, p.working_age.tariff_income.step, ) amount = where( - any_over_SP_age, + any_over_qualifying_age, p.pension_age.tariff_income.amount, p.working_age.tariff_income.amount, ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py index 50c086675b..b20cbbf962 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py @@ -11,31 +11,33 @@ class housing_benefit_applicable_amount(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.allowances - any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) + any_over_qualifying_age = benunit.any( + benunit.members("has_attained_state_pension_credit_qualifying_age", period) + ) eldest_age = benunit("eldest_adult_age", period) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold u_25 = eldest_age < older_age_threshold - o_25 = (eldest_age >= older_age_threshold) & ~any_over_SP_age - o_18 = (eldest_age >= younger_age_threshold) * ~any_over_SP_age + o_25 = (eldest_age >= older_age_threshold) & ~any_over_qualifying_age + o_18 = (eldest_age >= younger_age_threshold) * ~any_over_qualifying_age single = benunit("is_single_person", period) couple = benunit("is_couple", period) lone_parent = benunit("is_lone_parent", period) single_personal_allowance = ( u_25 * p.single.younger + o_25 * p.single.older - + any_over_SP_age * p.single.aged + + any_over_qualifying_age * p.single.aged ) couple_personal_allowance = ( u_18 * p.couple.younger + o_18 * p.couple.older - + any_over_SP_age * p.couple.aged + + any_over_qualifying_age * p.couple.aged ) lone_parent_personal_allowance = ( u_18 * p.lone_parent.younger + o_18 * p.lone_parent.older - + any_over_SP_age * p.lone_parent.aged + + any_over_qualifying_age * p.lone_parent.aged ) personal_allowance = ( single * single_personal_allowance diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py index c978c5f699..6551385eb3 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py @@ -16,7 +16,9 @@ class housing_benefit_assessable_capital(Variable): def formula(benunit, period, parameters): household = benunit.household person = benunit.members - any_over_SP_age = benunit.any(person("is_SP_age", period)) + any_over_qualifying_age = benunit.any( + person("has_attained_state_pension_credit_qualifying_age", period) + ) p = parameters(period).gov.dwp.housing_benefit.means_test.capital household_capital = sum(household(source, period) for source in p.sources) benunit_adults = add(benunit, period, ["is_adult"]) @@ -29,5 +31,7 @@ def formula(benunit, period, parameters): household_capital * benunit_adults / adult_divisor, 0, ) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + guarantee_credit = any_over_qualifying_age & ( + benunit("guarantee_credit", period) > 0 + ) return where(guarantee_credit, 0, max_(0, household_capital_proxy)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py index 9cab415763..0ce36c93f8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py @@ -25,7 +25,9 @@ class housing_benefit_eligible(Variable): def formula(benunit, period, parameters): person = benunit.members - sp_age = person("is_SP_age", period) + qualifying_age = person( + "has_attained_state_pension_credit_qualifying_age", period + ) # New claims are barred except where the claimant, and any partner, # has reached the qualifying age for State Pension Credit # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)). Every adult in @@ -36,7 +38,9 @@ def formula(benunit, period, parameters): # route receives Universal Credit; change the three together. adult = person("is_adult", period) adult_count = benunit.sum(adult) - pension_age = (adult_count > 0) & (benunit.sum(adult & sp_age) == adult_count) + pension_age = (adult_count > 0) & ( + benunit.sum(adult & qualifying_age) == adult_count + ) # Working-age and mixed-age families (since 15 May 2019) claim # Universal Credit instead. They keep an existing award until they # claim it (reg 8(2A)), so this route also rules out receiving @@ -49,11 +53,14 @@ def formula(benunit, period, parameters): continuing_award = already_claiming & ~claiming_uc social = benunit.any(person("in_social_housing", period)) lha_eligible = benunit("LHA_eligible", period) - any_over_SP_age = benunit.any(sp_age) + # Housing Benefit Regulations 2006 reg 5: the pension-age regulations + # (SI 2006/214) apply where the claimant or partner has attained the + # qualifying age for State Pension Credit. + any_over_qualifying_age = benunit.any(qualifying_age) capital = benunit("housing_benefit_assessable_capital", period) hb_capital = parameters(period).gov.dwp.housing_benefit.means_test.capital limit = where( - any_over_SP_age, + any_over_qualifying_age, hb_capital.pension_age.limit, hb_capital.working_age.limit, ) diff --git a/policyengine_uk/variables/gov/dwp/income_support_eligible.py b/policyengine_uk/variables/gov/dwp/income_support_eligible.py index 0dc992a5bb..e1aa3b6872 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_eligible.py +++ b/policyengine_uk/variables/gov/dwp/income_support_eligible.py @@ -13,14 +13,18 @@ def formula(benunit, period, parameters): lone_parent = benunit("is_lone_parent", period) lone_parent_with_young_child = lone_parent & youngest_child_5_or_under has_carers = add(benunit, period, ["is_carer_for_benefits"]) > 0 - none_SP_age = ~benunit.any(benunit.members("is_SP_age", period)) + # SSCBA 1992 s.124(1)(aa): the claimant has not attained the qualifying + # age for State Pension Credit. + none_over_qualifying_age = ~benunit.any( + benunit.members("has_attained_state_pension_credit_qualifying_age", period) + ) has_esa_income = benunit("esa_income", period) > 0 already_claiming = add(benunit, period, ["income_support_reported"]) > 0 capital = benunit("income_support_assessable_capital", period) limit = IS.means_test.capital.limit return ( (has_carers | lone_parent_with_young_child) - & none_SP_age + & none_over_qualifying_age & ~has_esa_income & already_claiming & (capital <= limit) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py index 0fb1547506..414e557f35 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py @@ -9,10 +9,15 @@ class is_benefit_cap_exempt_other(Variable): reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" def formula(benunit, period, parameters): - # Check if anyone in benefit unit is over state pension age + # The benefit cap applies to working-age Housing Benefit (Housing + # Benefit Regulations 2006 Part 8A) and Universal Credit, which both + # end at the qualifying age for State Pension Credit (reg 5; Welfare + # Reform Act 2012 s.4(1)(b)). person = benunit.members - over_pension_age = person("is_SP_age", period) - has_pensioner = benunit.any(over_pension_age) + over_qualifying_age = person( + "has_attained_state_pension_credit_qualifying_age", period + ) + has_pensioner = benunit.any(over_qualifying_age) # UC-specific exemptions # Limited capability for work and work-related activity diff --git a/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py b/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py index 53726ce48c..dc0a669c81 100644 --- a/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py +++ b/policyengine_uk/variables/gov/dwp/months_since_state_pension_age.py @@ -1,5 +1,5 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.dates import exact_age_in_months +from policyengine_uk.utils.state_pension_age import months_since_attaining class months_since_state_pension_age(Variable): @@ -15,11 +15,8 @@ class months_since_state_pension_age(Variable): unit = "month" def formula(person, period, parameters): - age_in_months = exact_age_in_months( - person("age", period), person("months_since_last_birthday", period) + return months_since_attaining( + person("age", period), + person("months_since_last_birthday", period), + person("state_pension_age", period), ) - spa = person("state_pension_age", period).astype(np.float64) - months_since = age_in_months - 12 * spa - # Rounding to a thousandth of a month (under an hour) removes float - # error, so reaching State Pension age exactly on 6 October counts. - return np.round(months_since, 3) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py b/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py new file mode 100644 index 0000000000..9f613f818e --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.state_pension_age import months_since_attaining + + +class has_attained_state_pension_credit_qualifying_age(Variable): + value_type = bool + entity = Person + label = "has attained the qualifying age for State Pension Credit" + documentation = ( + "Whether the person has reached the qualifying age for State Pension " + "Credit by the middle of the fiscal year (6 October), so is over it for " + "most of the year. Men born before 6 December 1953 reach it before " + "their own State Pension age of 65, so in 2018-19 and earlier this can " + "hold where is_SP_age does not; from 2019-20 the two agree for " + "everyone." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + ) + + def formula(person, period, parameters): + months_since = months_since_attaining( + person("age", period), + person("months_since_last_birthday", period), + person("state_pension_credit_qualifying_age", period), + ) + return months_since >= 0 diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py index d3c12d4372..fb8c8e1398 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py @@ -12,13 +12,18 @@ class is_pension_credit_eligible(Variable): def formula(benunit, period, parameters): adult = benunit.members("is_adult", period) adult_count = benunit.sum(adult) - all_adults_are_sp_age = ( - benunit.sum(adult & benunit.members("is_SP_age", period)) == adult_count + # State Pension Credit Act 2002 s.1(2)(b) and s.4(1A): the claimant, + # and any partner, must have attained the qualifying age. + qualifying_age = benunit.members( + "has_attained_state_pension_credit_qualifying_age", period + ) + all_adults_have_qualifying_age = ( + benunit.sum(adult & qualifying_age) == adult_count ) is_gc_eligible = benunit("is_guarantee_credit_eligible", period) is_sc_eligible = benunit("is_savings_credit_eligible", period) return ( (adult_count > 0) - & all_adults_are_sp_age + & all_adults_have_qualifying_age & (is_gc_eligible | is_sc_eligible) ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py index 9f4c1fde97..4e343cee5a 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_assessable_capital.py @@ -20,10 +20,17 @@ def formula(benunit, period, parameters): person = benunit.members p = parameters(period).gov.dwp.pension_credit.income.capital household_capital = sum(household(source, period) for source in p.sources) - any_pension_age = benunit.any(person("is_SP_age", period)) - benunit_pension_age_adults = benunit.sum(person("is_SP_age", period)) + # Pension-age adults: those who have attained the qualifying age for + # State Pension Credit (State Pension Credit Act 2002 s.1(6)). + pension_age = person("has_attained_state_pension_credit_qualifying_age", period) + any_pension_age = benunit.any(pension_age) + benunit_pension_age_adults = benunit.sum(pension_age) household_pension_age_adults = benunit.max( - person.household.sum(person.household.members("is_SP_age", period)) + person.household.sum( + person.household.members( + "has_attained_state_pension_credit_qualifying_age", period + ) + ) ) adult_divisor = max_(1, household_pension_age_adults) household_capital_proxy = ( diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py new file mode 100644 index 0000000000..7b89de1530 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py @@ -0,0 +1,43 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.state_pension_age import ( + age_attaining_pensionable_age, + date_of_birth, +) + + +class state_pension_credit_qualifying_age(Variable): + value_type = float + entity = Person + label = "qualifying age for State Pension Credit for this person" + documentation = ( + "The age at which this person reaches the qualifying age for State " + "Pension Credit: pensionable age for a woman, and for a man the " + "pensionable age of a woman born on the same day. It differs from the " + "person's own State Pension age only for men born before 6 December " + "1953, whose pensionable age is 65. Universal Credit, Housing Benefit, " + "Council Tax Reduction, Income Support and Winter Fuel Payment use this " + "age as well as Pension Credit. The date of birth comes from age and " + "months_since_last_birthday." + ) + definition_period = YEAR + unit = "year" + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/1995/26/schedule/4", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.state_pension.age + birth, birth_date = date_of_birth( + person("age", period), + person("months_since_last_birthday", period), + period.start.year, + ) + # State Pension Credit Act 2002 s.1(6): "(a) in the case of a woman, + # pensionable age; or (b) in the case of a man, the age which is + # pensionable age in the case of a woman born on the same day as the + # man". Every date of birth follows the women's timetable, so the male + # rule in Pensions Act 1995 Sch 4 para 1 rule (1) never applies. + return age_attaining_pensionable_age( + p, birth, birth_date, male_rule=np.zeros(person.count, dtype=bool) + ) diff --git a/policyengine_uk/variables/gov/dwp/state_pension_age.py b/policyengine_uk/variables/gov/dwp/state_pension_age.py index f93a15799e..6681ef9b63 100644 --- a/policyengine_uk/variables/gov/dwp/state_pension_age.py +++ b/policyengine_uk/variables/gov/dwp/state_pension_age.py @@ -1,10 +1,7 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.dates import ( - add_months_to_yyyymmdd, - exact_age_in_months, - grid_month, - grid_months_to_yyyymmdd, - yyyymmdd_to_grid_months, +from policyengine_uk.utils.state_pension_age import ( + age_attaining_pensionable_age, + date_of_birth, ) @@ -27,34 +24,13 @@ class state_pension_age(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.state_pension.age - # Dates are in months on a grid whose months start on the 6th, so the - # middle of the fiscal year (6 October) is a whole month. - mid_year = grid_month(period.start.year, 10) - age_in_months = exact_age_in_months( - person("age", period), person("months_since_last_birthday", period) + birth, birth_date = date_of_birth( + person("age", period), + person("months_since_last_birthday", period), + period.start.year, ) - birth = mid_year - age_in_months - # The day of birth: the one starting at or after that instant, so the - # person's legal age on 6 October is age. - birth_date = grid_months_to_yyyymmdd(birth) - # Pensions Act 1995 Sch 4 para 1 rule (1): men born before 6 December # 1953 attain pensionable age at 65. Everyone else follows the # timetable in rules (2) to (10), which sets either an age or a day. male_rule = person("is_male", period) & (birth_date < p.male.born_before) - age_rule = where(male_rule, p.male.age, p.age_by_birth_date.calc(birth_date)) - day_rule = where(male_rule, 0, p.day_by_birth_date.calc(birth_date)) - day_rule_month = where( - day_rule > 0, - yyyymmdd_to_grid_months(np.maximum(day_rule, 10101)), - -np.inf, - ) - # An age of N years and M months is attained at the commencement of - # the anniversary (Family Law Reform Act 1969 s.9(1)): the same day of - # the month, or the month's last day where that day does not exist, - # which also gives the three days in rule (7A). - anniversary = yyyymmdd_to_grid_months( - add_months_to_yyyymmdd(birth_date, age_rule) - ) - attained = np.maximum(anniversary, day_rule_month) - return (attained - birth) / 12 + return age_attaining_pensionable_age(p, birth, birth_date, male_rule) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py index 2dbd88a4f9..3ff07bcc6c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py @@ -11,5 +11,13 @@ class is_uc_eligible(Variable): def formula(benunit, period, parameters): capital = benunit("uc_assessable_capital", period) limit = parameters(period).gov.dwp.universal_credit.means_test.capital.limit - has_working_age_adult = benunit.any(benunit.members("is_WA_adult", period)) + # Welfare Reform Act 2012 s.4(1)(b) and (4): a claimant must not have + # reached the qualifying age for State Pension Credit; one member of a + # couple under it suffices (Universal Credit Regulations 2013 reg + # 3(2)(a)). + adult = benunit.members("is_adult", period) + qualifying_age = benunit.members( + "has_attained_state_pension_credit_qualifying_age", period + ) + has_working_age_adult = benunit.any(adult & ~qualifying_age) return has_working_age_adult & (capital <= limit) diff --git a/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py b/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py index f158779646..5ce996428f 100644 --- a/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +++ b/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py @@ -11,7 +11,13 @@ class winter_fuel_allowance(Variable): def formula(household, period, parameters): in_scotland = household("country", period).decode_to_str() == "SCOTLAND" age = household.members("age", period) - is_SP_age = household.members("is_SP_age", period) + # Social Fund Winter Fuel Payment Regulations 2000 reg 2(1)(b): the + # qualifying age for State Pension Credit. From 16 September 2024 SI + # 2024/869 reg 2 uses pensionable age instead; the two agree for + # every date of birth from 2019-20, so one test serves both. + qualifying_age = household.members( + "has_attained_state_pension_credit_qualifying_age", period + ) wfp = parameters(period).gov.dwp.winter_fuel_payment on_mtb = ( add( @@ -27,12 +33,11 @@ def formula(household, period, parameters): > 0 ) taxable_income = household.members("total_income", period) - is_SP_age = household.members("is_SP_age", period) country = household("country", period).decode_to_str() in_england_or_wales = np.isin(country, ["ENGLAND", "WALES"]) meets_income_passport = ( household.any( - is_SP_age + qualifying_age & ( taxable_income < wfp.eligibility.taxable_income_test.maximum_taxable_income @@ -45,7 +50,7 @@ def formula(household, period, parameters): meets_mtb_requirement = ( on_mtb | (not wfp.eligibility.require_benefits) | meets_income_passport ) - meets_spa_requirement = household.any(is_SP_age) | ( + meets_spa_requirement = household.any(qualifying_age) | ( not wfp.eligibility.state_pension_age_requirement ) meets_higher_age_requirement = household.any( diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py index 6f43033d47..7f8d4105c2 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4.py @@ -7,7 +7,7 @@ class ni_class_4(Variable): label = "NI Class 4 contributions" definition_period = YEAR unit = GBP - defined_for = "ni_liable" + defined_for = "ni_class_4_liable" reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/15" def formula(person, period, parameters): diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py new file mode 100644 index 0000000000..01026c0d4e --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class ni_class_4_liable(Variable): + value_type = bool + entity = Person + label = "liable for Class 4 National Insurance by age" + documentation = ( + "Whether the person is liable for Class 4 contributions for the tax " + "year by their age. A person over State Pension age at the beginning of " + "the tax year (6 April) is excepted; a person who reaches it during the " + "year stays liable for the whole year. People under 16 are treated as " + "excepted, as they can be on application under regulation 93." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2001/1004/regulation/91", + "https://www.legislation.gov.uk/uksi/2001/1004/regulation/93", + "https://www.gov.uk/hmrc-internal-manuals/national-insurance-manual/nim24510", + ) + + def formula(person, period, parameters): + # Social Security (Contributions) Regulations 2001 reg 91(a): an + # earner who "at the beginning of a year of assessment is over + # pensionable age" is excepted from Class 4 liability. The enabling + # power (SSCBA 1992 s.17(2)(b)) is for "a person having attained + # pensionable age", and pensionable age is attained at the + # commencement of the day, so attaining it on 6 April itself counts. + # 6 April is six months before 6 October, the middle of the year. + over_pension_age_at_start_of_year = ( + person("months_since_state_pension_age", period) >= 6 + ) + return person("over_16", period) & ~over_pension_age_at_start_of_year diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py index 6f738716ba..67fbfce39a 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_main.py @@ -7,7 +7,7 @@ class ni_class_4_main(Variable): label = "NI Class 4 main contributions" definition_period = YEAR unit = GBP - defined_for = "ni_liable" + defined_for = "ni_class_4_liable" reference = "https://www.legislation.gov.uk/ukpga/1992/4/section/15" def formula(person, period, parameters): diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py index c096429589..4884163c21 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py @@ -10,31 +10,33 @@ class council_tax_reduction_applicable_amount(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.allowances - any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) + any_over_qualifying_age = benunit.any( + benunit.members("has_attained_state_pension_credit_qualifying_age", period) + ) eldest_age = benunit("eldest_adult_age", period) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold u_25 = eldest_age < older_age_threshold - o_25 = (eldest_age >= older_age_threshold) & ~any_over_SP_age - o_18 = (eldest_age >= younger_age_threshold) & ~any_over_SP_age + o_25 = (eldest_age >= older_age_threshold) & ~any_over_qualifying_age + o_18 = (eldest_age >= younger_age_threshold) & ~any_over_qualifying_age single = benunit("is_single_person", period) couple = benunit("is_couple", period) lone_parent = benunit("is_lone_parent", period) single_personal_allowance = ( u_25 * p.single.younger + o_25 * p.single.older - + any_over_SP_age * p.single.aged + + any_over_qualifying_age * p.single.aged ) couple_personal_allowance = ( u_18 * p.couple.younger + o_18 * p.couple.older - + any_over_SP_age * p.couple.aged + + any_over_qualifying_age * p.couple.aged ) lone_parent_personal_allowance = ( u_18 * p.lone_parent.younger + o_18 * p.lone_parent.older - + any_over_SP_age * p.lone_parent.aged + + any_over_qualifying_age * p.lone_parent.aged ) personal_allowance = ( single * single_personal_allowance diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py index 555231a5ab..3ef546b717 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py @@ -10,4 +10,10 @@ class council_tax_reduction_household_has_pensioner(Variable): def formula(household, period, parameters): person = household.members claimant_benunit = person.benunit("benunit_contains_household_head", period) - return household.any(claimant_benunit & person("is_SP_age", period)) + # A "pensioner" has attained the qualifying age for State Pension + # Credit: SI 2012/2885 reg 3(1)(a) (England), SI 2013/3029 reg 3(1)(a) + # (Wales), and SSI 2012/319 reg 12(1) (Scotland). + qualifying_age = person( + "has_attained_state_pension_credit_qualifying_age", period + ) + return household.any(claimant_benunit & qualifying_age) From b9d8573d696946bd4ccaaa09382664154df4d788 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 08:21:57 -0400 Subject: [PATCH 02/13] Address review: accurate benefit cap and CTR comments, date s.4(1A), more tests - is_benefit_cap_exempt_other: say the cap has no age exception and that exempting any unit with a member over the qualifying age misses Universal Credit mixed-age couples (UC Regs 2013 reg 3(2)(a)); behaviour unchanged. - council_tax_reduction_household_has_pensioner: note the unmodelled "not on UC, IS, JSA(IB) or ESA(IR)" limb of SI 2012/2885 reg 3(1)(a)(ii). - is_pension_credit_eligible and YAML case 13: s.4(1A) is in force from 15 May 2019 (SI 2019/37 art. 3); the model applies it in every year. - ni_class_4_liable: label the 6 April reading as an interpretation; name the six-month offset MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR (also used by the Savings Credit age test). - Changelog and docs: describe the benefit cap as reaching only working-age HB and UC, and point the qualifying-age entry to the Class 4 entry. - Tests: YAML cases 14-20 pin the HB and CTR applicable amounts, HB tariff income and capital, the guarantee credit route and Pension Credit capital (4 of the 7 fail on the parent commit; 3 are controls). A property test now samples only the cohort where the two ages differ (men born 6 April 1950 to 6 December 1953, 2015-16 to 2018-19) and also checks the pension-age CTR allowance. Co-Authored-By: Claude Opus 5.5 --- .../spc-qualifying-age-class-4-spa.fixed.md | 2 +- docs/book/programs/gov/dwp/state-pension.md | 9 +- .../state_pension_credit_qualifying_age.yaml | 168 +++++++++++++++++- .../class_4/ni_class_4_liable.yaml | 3 +- ...est_state_pension_credit_qualifying_age.py | 60 +++++-- policyengine_uk/utils/dates.py | 4 + .../gov/dwp/is_benefit_cap_exempt_other.py | 13 +- .../is_pension_credit_eligible.py | 6 +- .../meets_savings_credit_age_requirement.py | 10 +- .../class_4/ni_class_4_liable.py | 16 +- ...l_tax_reduction_household_has_pensioner.py | 8 +- 11 files changed, 260 insertions(+), 39 deletions(-) diff --git a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md index b15d7e797a..2a4f7c7679 100644 --- a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md +++ b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md @@ -1 +1 @@ -Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap's pensioner exception and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age. +Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 36f1cbd8e5..6fb00eab9d 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -80,15 +80,18 @@ Other programmes test State Pension age in two ways: by 6 October. Pension Credit, Universal Credit (Welfare Reform Act 2012 s.4(1)(b)), Housing Benefit (Housing Benefit Regulations 2006 reg 5 in both sets), Council Tax Reduction's pension-age schemes, Income Support - (SSCBA 1992 s.124(1)(aa)), the benefit cap's pensioner exception and - Winter Fuel Payment (to September 2024) use it. It differs from + (SSCBA 1992 s.124(1)(aa)), the benefit cap (which reaches only + working-age Housing Benefit and Universal Credit) and Winter Fuel Payment + (to September 2024) use it. It differs from `is_SP_age` only for men born before 6 December 1953, so only in 2018-19 and earlier: a man born on 6 April 1952 reached it on 6 May 2014, nearly three years before his State Pension age of 65. - **Class 4 National Insurance** stops from the tax year after State Pension age: a person over it at the beginning of the tax year (6 April) is excepted (Social Security (Contributions) Regulations 2001 reg 91(a)), so - `ni_class_4_liable` needs `months_since_state_pension_age` below 6. + `ni_class_4_liable` needs `months_since_state_pension_age` below 6. Someone + who reaches it on 6 April itself is read as over it at the beginning of + that year. Class 1 employee contributions stop at State Pension age itself (SSCBA 1992 s.6(3)), which the annual model reads as `is_SP_age`. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml index c3a18c319f..e8fa71c0d4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -293,10 +293,11 @@ - name: Case 13, a couple both born 6 June 1953 is not a mixed-age couple in 2017-18. # She reached pensionable age, and he the qualifying age, on 6 March 2017; - # his own pensionable age is 6 June 2018. State Pension Credit Act 2002 - # s.4(1A) excludes a couple only where the other member "has not attained - # the qualifying age", so they can claim Pension Credit, not Universal - # Credit. + # his own pensionable age is 6 June 2018. Both have attained the qualifying + # age, so they can claim Pension Credit, not Universal Credit. (State + # Pension Credit Act 2002 s.4(1A), which excludes a couple where the other + # member "has not attained the qualifying age", came into force on 15 May + # 2019; the model applies it in every year, and it is met here either way.) period: 2017 input: people: @@ -319,3 +320,162 @@ is_SP_age: [false, true] is_pension_credit_eligible: true is_uc_eligible: false + +- name: Case 14, the pension-age Housing Benefit and CTR personal allowance applies from the qualifying age. + # Born 6 April 1952, over the qualifying age in 2016-17, so the pension-age + # regulations (SI 2006/214) apply. The model's single pension-age rate for + # 2016-17 is 181 a week: 181 x 52 = 9,412. No premiums apply. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + tenure_type: RENT_FROM_COUNCIL + output: + benefits_premiums: 0 + housing_benefit_applicable_amount: 9_412 + council_tax_reduction_applicable_amount: 9_412 + +- name: Case 15, below the qualifying age the working-age allowance for a single person aged 25 or over applies. + # Born 6 September 1953, under the qualifying age in 2017-18: 73.10 a week, + # 73.10 x 52 = 3,801.20. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 64 + months_since_last_birthday: 1 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + output: + benefits_premiums: 0 + council_tax_reduction_applicable_amount: 3_801.20 + +- name: Case 16, Housing Benefit tariff income uses the pension-age rule from the qualifying age. + # Born 6 April 1952, over the qualifying age in 2016-17, with 16,000 of + # savings and a 15,000 private pension (so no guarantee credit). Pension-age + # tariff income is 1 a week for each 500, or part, above 10,000: + # ceil(6,000 / 500) = 12 a week, 12 x 52 = 624. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + private_pension_income: 15_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + savings: 16_000 + output: + guarantee_credit: 0 + housing_benefit_assessable_capital: 16_000 + housing_benefit_tariff_income: 624 + +- name: Case 17, below the qualifying age Housing Benefit tariff income uses the working-age rule. + # Born 6 September 1953, under the qualifying age in 2017-18. Working-age + # tariff income is 1 a week for each 250, or part, above 6,000: + # ceil(10,000 / 250) = 40 a week, 40 x 52 = 2,080. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 64 + months_since_last_birthday: 1 + is_male: true + private_pension_income: 15_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + savings: 16_000 + output: + housing_benefit_assessable_capital: 16_000 + housing_benefit_tariff_income: 2_080 + +- name: Case 18, guarantee credit from the qualifying age means Housing Benefit counts no income or capital. + # Born 6 April 1952, over the qualifying age in 2016-17, with a 3,000 + # private pension, well below the standard minimum guarantee, and 5,000 of + # savings, so guarantee credit is payable. The model then counts no income + # or capital for pension-age Housing Benefit. + period: 2016 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + private_pension_income: 3_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + savings: 5_000 + output: + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + +- name: Case 19, Pension Credit counts household capital for adults over the qualifying age. + # Born 6 April 1952, over the qualifying age in 2016-17. The household's + # 20,000 of savings is split over its adults who have attained the + # qualifying age; he is the only one. + period: 2016 + input: + people: + person1: + age: 64 + months_since_last_birthday: 6 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + savings: 20_000 + output: + pension_credit_assessable_capital: 20_000 + +- name: Case 20, Pension Credit counts no capital below the qualifying age. + # Born 6 September 1953, under the qualifying age in 2017-18. + period: 2017 + input: + people: + person1: + age: 64 + months_since_last_birthday: 1 + is_male: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + savings: 20_000 + output: + pension_credit_assessable_capital: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml index a73f2cc052..377d0dd60e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml @@ -71,7 +71,8 @@ - name: Case 5, reaching State Pension age on 6 April itself excepts that year. # Born 6 April 1959: rule (6), 66, so the commencement of 6 April 2025, the # first day of 2025-26. Pensionable age is attained at the commencement of - # the day, so the person is over it at the beginning of the year. + # the day, so the model reads the person as over it at the beginning of the + # year (an interpretation: reg 91 and NIM24510 do not address this day). period: 2025 input: people: diff --git a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py index feb55c4285..723a52b3f1 100644 --- a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -19,6 +19,7 @@ from hypothesis import strategies as st from policyengine_uk import Simulation +from policyengine_uk.model_api import WEEKS_IN_YEAR from policyengine_uk.tests.test_state_pension_age import ( YEARS, differential_simulation, @@ -125,24 +126,14 @@ def test_the_two_ages_do_differ_in_2015_to_2018(): ) -@settings(max_examples=30, deadline=None) -@given( - births=st.lists( - st.dates(min_value=date(1945, 1, 1), max_value=date(1965, 12, 31)), - min_size=1, - max_size=40, - unique=True, - ), - male=st.booleans(), - year=st.integers(min_value=2015, max_value=2030), -) -def test_consumers_use_the_qualifying_age(births, male, year): +def assert_consumers_follow_the_qualifying_age(births, male, year): """For single people with no income or capital, each programme that the statute ties to the qualifying age for State Pension Credit follows it, not the person's own pensionable age: Pension Credit is available exactly from it, Universal Credit exactly before it, and the pension-age Council - Tax Reduction scheme and the benefit cap exception exactly from it. Winter - Fuel Payment follows it until 2023-24, when it still needed no benefit.""" + Tax Reduction scheme, the benefit cap exception and the pension-age + Housing Benefit and CTR personal allowance exactly from it. Winter Fuel + Payment follows it until 2023-24, when it still needed no benefit.""" people = {} for i, birth in enumerate(births): age_in_months = grid_months(date(year, 10, 6)) - grid_months(birth) @@ -164,12 +155,53 @@ def test_consumers_use_the_qualifying_age(births, male, year): sim.calculate("council_tax_reduction_household_has_pensioner", year), attained ) assert np.array_equal(sim.calculate("is_benefit_cap_exempt_other", year), attained) + aged = sim.tax_benefit_system.parameters( + f"{year}-06-01" + ).gov.dwp.housing_benefit.allowances.single.aged + assert np.array_equal( + np.isclose( + sim.calculate("council_tax_reduction_applicable_amount", year), + aged * WEEKS_IN_YEAR, + ), + attained, + ) if year <= 2023: assert np.array_equal( sim.calculate("winter_fuel_allowance", year) > 0, attained ) +@settings(max_examples=30, deadline=None) +@given( + births=st.lists( + st.dates(min_value=date(1945, 1, 1), max_value=date(1965, 12, 31)), + min_size=1, + max_size=40, + unique=True, + ), + male=st.booleans(), + year=st.integers(min_value=2015, max_value=2030), +) +def test_consumers_use_the_qualifying_age(births, male, year): + assert_consumers_follow_the_qualifying_age(births, male, year) + + +@settings(max_examples=20, deadline=None) +@given( + births=st.lists( + st.dates(min_value=date(1950, 4, 6), max_value=MALE_RULE_BORN_BEFORE), + min_size=1, + max_size=40, + unique=True, + ), + year=st.integers(min_value=2015, max_value=LAST_YEAR_THEY_DIFFER), +) +def test_consumers_use_the_qualifying_age_where_it_differs(births, year): + """The same, for the cohort where the two ages differ: men born from 6 + April 1950 to 6 December 1953, in 2015-16 to 2018-19.""" + assert_consumers_follow_the_qualifying_age(births, True, year) + + @settings(max_examples=40, deadline=None) @given( births=st.lists( diff --git a/policyengine_uk/utils/dates.py b/policyengine_uk/utils/dates.py index 658ae263bf..b2be7f25c0 100644 --- a/policyengine_uk/utils/dates.py +++ b/policyengine_uk/utils/dates.py @@ -17,6 +17,10 @@ _MONTHS_BEFORE_1970 = 12 * 1970 +# The fiscal year starts on 6 April, six grid months before its middle (6 +# October), where annual status such as is_SP_age is read. +MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR = 6 + def grid_month(year: int, month: int) -> int: """The grid month that starts on the 6th of ``month`` in ``year``.""" diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py index 414e557f35..c18e687d25 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py @@ -9,10 +9,15 @@ class is_benefit_cap_exempt_other(Variable): reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" def formula(benunit, period, parameters): - # The benefit cap applies to working-age Housing Benefit (Housing - # Benefit Regulations 2006 Part 8A) and Universal Credit, which both - # end at the qualifying age for State Pension Credit (reg 5; Welfare - # Reform Act 2012 s.4(1)(b)). + # The benefit cap has no age exception (Universal Credit Regulations + # 2013 regs 79 to 83; Housing Benefit Regulations 2006 Part 8A). It + # does not reach pension-age Housing Benefit (SI 2006/214, which + # applies once the claimant or partner has attained the qualifying age + # for State Pension Credit: reg 5), and a single person or couple over + # that age cannot get Universal Credit (Welfare Reform Act 2012 + # s.4(1)(b)). A Universal Credit mixed-age couple can still be capped + # (reg 3(2)(a)); exempting any unit with a member over the age does + # not model that. person = benunit.members over_qualifying_age = person( "has_attained_state_pension_credit_qualifying_age", period diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py index fb8c8e1398..7c7a6fbe70 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py @@ -12,8 +12,10 @@ class is_pension_credit_eligible(Variable): def formula(benunit, period, parameters): adult = benunit.members("is_adult", period) adult_count = benunit.sum(adult) - # State Pension Credit Act 2002 s.1(2)(b) and s.4(1A): the claimant, - # and any partner, must have attained the qualifying age. + # State Pension Credit Act 2002 s.1(2)(b): the claimant must have + # attained the qualifying age; s.4(1A), in force from 15 May 2019, + # requires any partner to have attained it too. The model applies + # s.4(1A) in every year. qualifying_age = benunit.members( "has_attained_state_pension_credit_qualifying_age", period ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py index 92eb215656..e7c69f67ff 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/meets_savings_credit_age_requirement.py @@ -1,5 +1,8 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.dates import exact_age_in_months +from policyengine_uk.utils.dates import ( + MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR, + exact_age_in_months, +) class meets_savings_credit_age_requirement(Variable): @@ -17,7 +20,10 @@ def formula(person, period, parameters): # State Pension Credit Act 2002 s.3(1)(a): the claimant "has attained # pensionable age before 6 April 2016 and has attained the age of 65". p = parameters(period).gov.dwp.pension_credit.savings_credit - months_from_cutoff_to_mid_year = 12 * (period.start.year - p.cutoff_year) + 6 + months_from_cutoff_to_mid_year = ( + 12 * (period.start.year - p.cutoff_year) + + MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR + ) reached_before_cutoff = ( person("months_since_state_pension_age", period) > months_from_cutoff_to_mid_year diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py index 01026c0d4e..b8a24a5dd9 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR class ni_class_4_liable(Variable): @@ -22,12 +23,15 @@ class ni_class_4_liable(Variable): def formula(person, period, parameters): # Social Security (Contributions) Regulations 2001 reg 91(a): an # earner who "at the beginning of a year of assessment is over - # pensionable age" is excepted from Class 4 liability. The enabling - # power (SSCBA 1992 s.17(2)(b)) is for "a person having attained - # pensionable age", and pensionable age is attained at the - # commencement of the day, so attaining it on 6 April itself counts. - # 6 April is six months before 6 October, the middle of the year. + # pensionable age" is excepted from Class 4 liability. Neither the + # regulations nor HMRC's manual (NIM24510) address someone who + # attains pensionable age on 6 April itself. + # This reads them as over it at the beginning of that year: + # pensionable age is attained at the commencement of the day, and + # the enabling power (SSCBA 1992 s.17(2)(b)) is for "a person having + # attained pensionable age". over_pension_age_at_start_of_year = ( - person("months_since_state_pension_age", period) >= 6 + person("months_since_state_pension_age", period) + >= MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR ) return person("over_16", period) & ~over_pension_age_at_start_of_year diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py index 3ef546b717..c66503c789 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py @@ -11,8 +11,12 @@ def formula(household, period, parameters): person = household.members claimant_benunit = person.benunit("benunit_contains_household_head", period) # A "pensioner" has attained the qualifying age for State Pension - # Credit: SI 2012/2885 reg 3(1)(a) (England), SI 2013/3029 reg 3(1)(a) - # (Wales), and SSI 2012/319 reg 12(1) (Scotland). + # Credit: SI 2012/2885 reg 3(1)(a)(i) (England), SI 2013/3029 reg + # 3(1)(a)(i) (Wales), and SSI 2012/319 reg 12(1) (Scotland). The + # further condition that neither the person nor a partner is on + # Universal Credit, Income Support, income-based Jobseeker's Allowance + # or income-related Employment and Support Allowance (reg 3(1)(a)(ii); + # reg 12(2)) is not modelled. qualifying_age = person( "has_attained_state_pension_credit_qualifying_age", period ) From cae3131950e745760abc8225d35fc8ae2f0f46ab Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 08:28:32 -0400 Subject: [PATCH 03/13] Apply round-2 review nits: Class 4 wording and the cohort test bound Class 4 "stops from the first tax year that begins on or after the day State Pension age is reached", matching the 6 April reading. The cohort property test samples births up to 5 December 1953, the last day rule (1) covers. Co-Authored-By: Claude Opus 5.5 --- .../class-4-state-pension-age-start-of-year.fixed.md | 2 +- docs/book/programs/gov/dwp/state-pension.md | 4 ++-- .../tests/test_state_pension_credit_qualifying_age.py | 9 ++++++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/changelog.d/class-4-state-pension-age-start-of-year.fixed.md b/changelog.d/class-4-state-pension-age-start-of-year.fixed.md index e9b6f49ad1..779d5befa6 100644 --- a/changelog.d/class-4-state-pension-age-start-of-year.fixed.md +++ b/changelog.d/class-4-state-pension-age-start-of-year.fixed.md @@ -1 +1 @@ -Class 4 National Insurance now stops from the tax year after State Pension age is reached (Social Security (Contributions) Regulations 2001 reg 91(a)): a person over State Pension age on 6 April is excepted, and one who reaches it during the year pays Class 4 for that whole year. Before, anyone over State Pension age on 6 October was excepted. +Class 4 National Insurance now stops from the first tax year that begins on or after the day State Pension age is reached (Social Security (Contributions) Regulations 2001 reg 91(a)): a person over State Pension age on 6 April is excepted, and one who reaches it during the year pays Class 4 for that whole year. Before, anyone over State Pension age on 6 October was excepted. diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 6fb00eab9d..0906f6918f 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -86,8 +86,8 @@ Other programmes test State Pension age in two ways: `is_SP_age` only for men born before 6 December 1953, so only in 2018-19 and earlier: a man born on 6 April 1952 reached it on 6 May 2014, nearly three years before his State Pension age of 65. -- **Class 4 National Insurance** stops from the tax year after State Pension - age: a person over it at the beginning of the tax year (6 April) is +- **Class 4 National Insurance** stops from the first tax year that begins on + or after the day State Pension age is reached: a person over it at the beginning of the tax year (6 April) is excepted (Social Security (Contributions) Regulations 2001 reg 91(a)), so `ni_class_4_liable` needs `months_since_state_pension_age` below 6. Someone who reaches it on 6 April itself is read as over it at the beginning of diff --git a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py index 723a52b3f1..c0da720962 100644 --- a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -11,7 +11,7 @@ April), so someone who reaches it during a year is liable for that whole year. """ -from datetime import date +from datetime import date, timedelta import numpy as np import pytest @@ -189,7 +189,10 @@ def test_consumers_use_the_qualifying_age(births, male, year): @settings(max_examples=20, deadline=None) @given( births=st.lists( - st.dates(min_value=date(1950, 4, 6), max_value=MALE_RULE_BORN_BEFORE), + st.dates( + min_value=date(1950, 4, 6), + max_value=MALE_RULE_BORN_BEFORE - timedelta(days=1), + ), min_size=1, max_size=40, unique=True, @@ -198,7 +201,7 @@ def test_consumers_use_the_qualifying_age(births, male, year): ) def test_consumers_use_the_qualifying_age_where_it_differs(births, year): """The same, for the cohort where the two ages differ: men born from 6 - April 1950 to 6 December 1953, in 2015-16 to 2018-19.""" + April 1950 to 5 December 1953, in 2015-16 to 2018-19.""" assert_consumers_follow_the_qualifying_age(births, True, year) From 09ec4bf002f0b9cc07049a01a273bd344299433c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 22:50:28 -0400 Subject: [PATCH 04/13] Take Class 4 out of ni_liable's description after merging main #1895 described ni_liable as covering primary Class 1, Class 2 and Class 4. Class 4 now uses ni_class_4_liable (status at 6 April, SI 2001/1004 reg 91(a)), so ni_liable covers primary Class 1 and Class 2 only. The National Insurance docs page states the Class 4 age rule. Co-Authored-By: Claude Opus 5.5 --- .../gov/hmrc/national-insurance.ipynb | 2 +- .../national_insurance/class_1/ni_liable.py | 19 +++++++++---------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/book/programs/gov/hmrc/national-insurance.ipynb b/docs/book/programs/gov/hmrc/national-insurance.ipynb index 8a3e68d368..7dbb4d49e9 100644 --- a/docs/book/programs/gov/hmrc/national-insurance.ipynb +++ b/docs/book/programs/gov/hmrc/national-insurance.ipynb @@ -171,7 +171,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## How PolicyEngine computes National Insurance liability\n\nFor each person, PolicyEngine computes NI contributions class-by-class:\n\n- **Class 1 employee** (`ni_class_1_employee`): the sum of `ni_class_1_employee_primary` (main rate on earnings between the Primary Threshold and the Upper Earnings Limit) and `ni_class_1_employee_additional` (additional rate on earnings above the UEL). These primary contributions are due from age 16 until state pension age (`ni_liable`): SSCBA 1992 s.6(3) ends them at pensionable age.\n- **Class 1 employer** (`ni_class_1_employer`): paid on earnings above the Secondary Threshold; surfaced through the `ni_employer` variable. These secondary contributions are due on earners over 16 with no upper age limit (`ni_class_1_secondary_liable`), so employers keep paying them for employees over state pension age (SSCBA 1992 s.6(1)(b) and s.6(3)).\n- **Class 2** (`ni_class_2`): a flat weekly rate paid by the self-employed with profits above the Small Profits Threshold.\n- **Class 3** (`ni_class_3`): a voluntary flat weekly rate; the variable is exposed but defaults to zero unless explicitly set.\n- **Class 4** (`ni_class_4`): main rate between the Lower and Upper Profits Limits plus additional rate above the UPL. Where the person also pays primary Class 1 contributions (or, before 6 April 2024, Class 2 contributions), liability is capped at the annual maximum in regulation 100 of the Social Security (Contributions) Regulations 2001 (`ni_class_4_maximum`).\n\nThe individual total `national_insurance` sums the four employee and self-employed components directly — `ni_class_1_employee + ni_class_2 + ni_class_3 + ni_class_4` — so voluntary Class 3 contributions are included in the household-side liability. The convenience aggregates `ni_employee` (Class 1 employee only) and `ni_self_employed` (Class 2 + Class 4) are also exposed for reporting. The Class 1 *employer* charge (`ni_class_1_employer`, surfaced as `ni_employer`) is an employer-side cost rather than part of the individual's `national_insurance`, so it is tracked separately and feeds the government revenue aggregates on its own.\n\nParameters live in `policyengine_uk/parameters/gov/hmrc/national_insurance/` and the per-class formulas in `policyengine_uk/variables/gov/hmrc/national_insurance/`." + "source": "## How PolicyEngine computes National Insurance liability\n\nFor each person, PolicyEngine computes NI contributions class-by-class:\n\n- **Class 1 employee** (`ni_class_1_employee`): the sum of `ni_class_1_employee_primary` (main rate on earnings between the Primary Threshold and the Upper Earnings Limit) and `ni_class_1_employee_additional` (additional rate on earnings above the UEL). These primary contributions are due from age 16 until state pension age (`ni_liable`): SSCBA 1992 s.6(3) ends them at pensionable age.\n- **Class 1 employer** (`ni_class_1_employer`): paid on earnings above the Secondary Threshold; surfaced through the `ni_employer` variable. These secondary contributions are due on earners over 16 with no upper age limit (`ni_class_1_secondary_liable`), so employers keep paying them for employees over state pension age (SSCBA 1992 s.6(1)(b) and s.6(3)).\n- **Class 2** (`ni_class_2`): a flat weekly rate paid by the self-employed with profits above the Small Profits Threshold.\n- **Class 3** (`ni_class_3`): a voluntary flat weekly rate; the variable is exposed but defaults to zero unless explicitly set.\n- **Class 4** (`ni_class_4`): main rate between the Lower and Upper Profits Limits plus additional rate above the UPL. Where the person also pays primary Class 1 contributions (or, before 6 April 2024, Class 2 contributions), liability is capped at the annual maximum in regulation 100 of the Social Security (Contributions) Regulations 2001 (`ni_class_4_maximum`). Class 4 is due from age 16 until the first tax year that begins on or after the day the person reaches state pension age (`ni_class_4_liable`): regulation 91(a) excepts anyone over pensionable age at the beginning of the tax year, so a person who reaches it during a year pays for that whole year.\n\nThe individual total `national_insurance` sums the four employee and self-employed components directly — `ni_class_1_employee + ni_class_2 + ni_class_3 + ni_class_4` — so voluntary Class 3 contributions are included in the household-side liability. The convenience aggregates `ni_employee` (Class 1 employee only) and `ni_self_employed` (Class 2 + Class 4) are also exposed for reporting. The Class 1 *employer* charge (`ni_class_1_employer`, surfaced as `ni_employer`) is an employer-side cost rather than part of the individual's `national_insurance`, so it is tracked separately and feeds the government revenue aggregates on its own.\n\nParameters live in `policyengine_uk/parameters/gov/hmrc/national_insurance/` and the per-class formulas in `policyengine_uk/variables/gov/hmrc/national_insurance/`." }, { "cell_type": "markdown", diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py index f1aba43503..062ca4af05 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_1/ni_liable.py @@ -2,16 +2,16 @@ class ni_liable(Variable): - label = "age-liable for primary Class 1, Class 2 and Class 4 NI" + label = "age-liable for primary Class 1 and Class 2 NI" documentation = ( - "Whether this person's age allows primary (employee) Class 1, Class 2 " - "and Class 4 contributions: 16 or over and under state pension age. " - "Primary Class 1 ends at pensionable age (SSCBA 1992 s.6(3)), Class 2 " - "after the week the earner reaches it (s.11(7)(b)) and Class 4 for " - "anyone over it at the start of the tax year (SI 2001/1004 reg 91(a)). " - "The model applies all three with the annual is_SP_age flag. Secondary " - "(employer) Class 1 has no upper age limit; see " - "ni_class_1_secondary_liable." + "Whether this person's age allows primary (employee) Class 1 and " + "Class 2 contributions: 16 or over and under state pension age. " + "Primary Class 1 ends at pensionable age (SSCBA 1992 s.6(3)) and Class " + "2 after the week the earner reaches it (s.11(7)(b)). The model " + "applies both with the annual is_SP_age flag. Class 4 ends for anyone " + "over pensionable age at the start of the tax year (SI 2001/1004 reg " + "91(a)); see ni_class_4_liable. Secondary (employer) Class 1 has no " + "upper age limit; see ni_class_1_secondary_liable." ) entity = Person definition_period = YEAR @@ -19,7 +19,6 @@ class ni_liable(Variable): reference = [ "https://www.legislation.gov.uk/ukpga/1992/4/section/6", "https://www.legislation.gov.uk/ukpga/1992/4/section/11", - "https://www.legislation.gov.uk/uksi/2001/1004/regulation/91", ] def formula(person, period, parameters): From 0408ef142c99d074c1189167c4d313e62a5b7109 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 23:08:38 -0400 Subject: [PATCH 05/13] Rewrap the Class 4 paragraph in the State Pension docs Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/state-pension.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 0906f6918f..7106e0a519 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -87,13 +87,13 @@ Other programmes test State Pension age in two ways: and earlier: a man born on 6 April 1952 reached it on 6 May 2014, nearly three years before his State Pension age of 65. - **Class 4 National Insurance** stops from the first tax year that begins on - or after the day State Pension age is reached: a person over it at the beginning of the tax year (6 April) is - excepted (Social Security (Contributions) Regulations 2001 reg 91(a)), so - `ni_class_4_liable` needs `months_since_state_pension_age` below 6. Someone - who reaches it on 6 April itself is read as over it at the beginning of - that year. - Class 1 employee contributions stop at State Pension age itself (SSCBA - 1992 s.6(3)), which the annual model reads as `is_SP_age`. + or after the day State Pension age is reached: a person over it at the + beginning of the tax year (6 April) is excepted (Social Security + (Contributions) Regulations 2001 reg 91(a)), so `ni_class_4_liable` needs + `months_since_state_pension_age` below 6. Someone who reaches it on 6 April + itself is read as over it at the beginning of that year. Class 1 employee + contributions stop at State Pension age itself (SSCBA 1992 s.6(3)), which + the annual model reads as `is_SP_age`. A person attains an age at the start of the anniversary of their birth (Family Law Reform Act 1969 s.9(1)), and an age of "N years and M months" on From fcfcd41899275900cc8c14821a037e4dae28ee59 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 09:18:07 -0400 Subject: [PATCH 06/13] Document Class 4 boundary and date Winter Fuel age tests Make Winter Fuel Payment follow qualifying age before September 2024 and pensionable age afterwards, including reforms that separate those ages. Assert the historical couple is not mixed-age and reuse the loaded country model in qualifying-age properties without changing expectations. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mt-1907-build.changed.md | 1 + docs/book/programs/gov/dwp/state-pension.md | 6 ++- .../state_pension_credit_qualifying_age.yaml | 8 +++- .../gov/dwp/winter_fuel_allowance.yaml | 47 ++++++++++++++++++- ...est_state_pension_credit_qualifying_age.py | 34 +++++++++++++- ...ned_state_pension_credit_qualifying_age.py | 4 +- .../state_pension_credit_qualifying_age.py | 5 +- .../gov/dwp/winter_fuel_allowance.py | 21 +++++---- .../class_4/ni_class_4_liable.py | 10 +++- 9 files changed, 117 insertions(+), 19 deletions(-) create mode 100644 changelog.d/mt-1907-build.changed.md diff --git a/changelog.d/mt-1907-build.changed.md b/changelog.d/mt-1907-build.changed.md new file mode 100644 index 0000000000..793cd25978 --- /dev/null +++ b/changelog.d/mt-1907-build.changed.md @@ -0,0 +1 @@ +- Applied pensionable age to Winter Fuel Payment from 2024-25 while retaining the State Pension Credit qualifying-age test for earlier years, including when reforms separate the timetables. diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index d14f6e8748..50d97635b7 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -96,7 +96,11 @@ Other programmes test State Pension age in two ways: beginning of the tax year (6 April) is excepted (Social Security (Contributions) Regulations 2001 reg 91(a)), so `ni_class_4_liable` needs `months_since_state_pension_age` below 6. Someone who reaches it on 6 April - itself is read as over it at the beginning of that year. Class 1 employee + itself is read as over it at the beginning of that year. With default + whole-number ages in a household situation, the inferred birthday is + 6 April. Entering an integer State Pension age therefore makes this + boundary reading determine the Class 4 result; fractional ages and explicit + birthday-month inputs can place the birthday on another day. Class 1 employee contributions stop at State Pension age itself (SSCBA 1992 s.6(3)), which the annual model reads as `is_SP_age`. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml index e8fa71c0d4..d16bdce73b 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -6,7 +6,8 @@ # Universal Credit (Welfare Reform Act 2012 s.4(1)(b), (4)), Housing Benefit # (SI 2006/213 and 2006/214 reg 5; SI 2014/1230 reg 6A(4)), Council Tax # Reduction (SI 2012/2885 reg 3), Income Support (SSCBA 1992 s.124(1)(aa)) and -# Winter Fuel Payment (SI 2000/729 reg 2(1)(b)) use the qualifying age. +# Winter Fuel Payment before September 2024 (SI 2000/729 reg 2(1)(b)) use +# the qualifying age. # # age and months_since_last_birthday place the date of birth: the person was # born that many years and months before 6 October of the fiscal year. @@ -297,7 +298,8 @@ # age, so they can claim Pension Credit, not Universal Credit. (State # Pension Credit Act 2002 s.4(1A), which excludes a couple where the other # member "has not attained the qualifying age", came into force on 15 May - # 2019; the model applies it in every year, and it is met here either way.) + # 2019. Both members meet the shared age condition, before and after that + # exclusion; they are not a mixed-age couple under SI 2019/37 art. 2.) period: 2017 input: people: @@ -318,6 +320,8 @@ output: has_attained_state_pension_credit_qualifying_age: [true, true] is_SP_age: [false, true] + is_mixed_age_couple: false + meets_pension_credit_age_conditions: true is_pension_credit_eligible: true is_uc_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_allowance.yaml index 8167a7bf74..aa70b63530 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_allowance.yaml @@ -8,6 +8,51 @@ output: winter_fuel_allowance: 200 +# Controlled age-status inputs distinguish the statutory predicates when a +# reform separates the timetables. SI 2000/729 reg 2(1)(b) tests qualifying +# age; from September 2024 SI 2024/869 reg 2(1)(a) tests pensionable age. +- name: Before 2024 Winter Fuel Payment follows qualifying age before pensionable age + period: 2023 + input: + age: 70 + country: ENGLAND + has_attained_state_pension_credit_qualifying_age: true + is_SP_age: false + output: + winter_fuel_allowance: 200 + +- name: Before 2024 pensionable-age status alone does not satisfy the Winter Fuel qualifying-age test + period: 2023 + input: + age: 70 + country: ENGLAND + has_attained_state_pension_credit_qualifying_age: false + is_SP_age: true + output: + winter_fuel_allowance: 0 + +- name: From 2024 qualifying-age status alone does not satisfy the Winter Fuel pensionable-age test + period: 2024 + input: + age: 70 + country: ENGLAND + pension_credit: 100 + has_attained_state_pension_credit_qualifying_age: true + is_SP_age: false + output: + winter_fuel_allowance: 0 + +- name: From 2024 Winter Fuel Payment follows pensionable age independently of qualifying-age status + period: 2024 + input: + age: 70 + country: ENGLAND + pension_credit: 100 + has_attained_state_pension_credit_qualifying_age: false + is_SP_age: true + output: + winter_fuel_allowance: 200 + - name: Single pensioner aged 80+ gets higher rate WFA period: 2023 absolute_error_margin: 1 @@ -58,4 +103,4 @@ total_income: 50000 income_support: 80 output: - winter_fuel_allowance: 200 \ No newline at end of file + winter_fuel_allowance: 200 diff --git a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py index c0da720962..3880ec222c 100644 --- a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -12,6 +12,7 @@ """ from datetime import date, timedelta +from functools import lru_cache import numpy as np import pytest @@ -20,6 +21,7 @@ from policyengine_uk import Simulation from policyengine_uk.model_api import WEEKS_IN_YEAR +from policyengine_uk.system import system from policyengine_uk.tests.test_state_pension_age import ( YEARS, differential_simulation, @@ -28,6 +30,18 @@ reference_attainment_day, ) + +@pytest.fixture(scope="module", autouse=True) +def reuse_country_model(): + # Keep the real simulation constructors and independent parameter trees, + # without importing every variable again for each synthetic household. + with pytest.MonkeyPatch.context() as patch: + patch.setattr( + "policyengine_uk.simulation.CountryTaxBenefitSystem", system.clone + ) + yield + + # Men born before this date have a pensionable age of 65 (Sch 4 para 1 rule # (1)); for everyone else pensionable age and the qualifying age coincide. MALE_RULE_BORN_BEFORE = date(1953, 12, 6) @@ -51,6 +65,22 @@ def one_person_units(people: dict) -> dict: } +@lru_cache(maxsize=1) +def one_person_units_template(): + return Simulation(situation=one_person_units({"template": {}})) + + +def property_simulation(people: dict): + # These properties never reform parameters or variable definitions. Share + # that model and rebuild the populations instead of repeatedly importing + # every variable and retaining generated modules in sys.modules. + sim = one_person_units_template().clone(clone_tax_benefit_system=False) + sim.build_from_situation(one_person_units(people)) + sim.input_variables = sim.get_known_variables() + sim.calculated_periods = [] + return sim + + @pytest.mark.parametrize("year", YEARS) def test_qualifying_age_matches_statute_for_every_birth_date(year): """For births every third day from 1945 to 1984 and on both sides of every @@ -143,7 +173,7 @@ def assert_consumers_follow_the_qualifying_age(births, male, year): "months_since_last_birthday": {year: age_in_months - 12 * age}, "is_male": {year: male}, } - sim = Simulation(situation=one_person_units(people)) + sim = property_simulation(people) attained = sim.calculate("has_attained_state_pension_credit_qualifying_age", year) expected = np.array( [reference_qualifying_day(b) <= date(year, 10, 6) for b in births] @@ -234,7 +264,7 @@ def test_class_4_liability_properties(births, male, year): "is_male": {y: male for y in (year, year + 1)}, "self_employment_income": {y: 60_000 for y in (year, year + 1)}, } - sim = Simulation(situation=one_person_units(people)) + sim = property_simulation(people) liable = sim.calculate("ni_class_4_liable", year) liable_next = sim.calculate("ni_class_4_liable", year + 1) since = sim.calculate("months_since_state_pension_age", year) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py b/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py index 9f613f818e..8d077a2379 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/has_attained_state_pension_credit_qualifying_age.py @@ -11,8 +11,8 @@ class has_attained_state_pension_credit_qualifying_age(Variable): "Credit by the middle of the fiscal year (6 October), so is over it for " "most of the year. Men born before 6 December 1953 reach it before " "their own State Pension age of 65, so in 2018-19 and earlier this can " - "hold where is_SP_age does not; from 2019-20 the two agree for " - "everyone." + "hold where is_SP_age does not; under the statutory timetable, from " + "2019-20 the two agree for everyone." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py index 7b89de1530..a4590c7e8a 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py @@ -15,8 +15,9 @@ class state_pension_credit_qualifying_age(Variable): "pensionable age of a woman born on the same day. It differs from the " "person's own State Pension age only for men born before 6 December " "1953, whose pensionable age is 65. Universal Credit, Housing Benefit, " - "Council Tax Reduction, Income Support and Winter Fuel Payment use this " - "age as well as Pension Credit. The date of birth comes from age and " + "Council Tax Reduction, Income Support and Winter Fuel Payment before " + "September 2024 use this age as well as Pension Credit. The date of " + "birth comes from age and " "months_since_last_birthday." ) definition_period = YEAR diff --git a/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py b/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py index 5ce996428f..5792f3b07d 100644 --- a/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +++ b/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py @@ -11,12 +11,17 @@ class winter_fuel_allowance(Variable): def formula(household, period, parameters): in_scotland = household("country", period).decode_to_str() == "SCOTLAND" age = household.members("age", period) - # Social Fund Winter Fuel Payment Regulations 2000 reg 2(1)(b): the - # qualifying age for State Pension Credit. From 16 September 2024 SI - # 2024/869 reg 2 uses pensionable age instead; the two agree for - # every date of birth from 2019-20, so one test serves both. - qualifying_age = household.members( - "has_attained_state_pension_credit_qualifying_age", period + # SI 2000/729 reg 2(1)(b): "has attained the qualifying age for state + # pension credit". From 16 September 2024, SI 2024/869 reg 2(1)(a) + # uses "has reached pensionable age" (also SI 2025/969 reg 2(a)). + # The statutory ages coincide in these later years, but a reform + # can change their timetables independently. + age_eligible = where( + period.start.year >= 2024, + household.members("is_SP_age", period), + household.members( + "has_attained_state_pension_credit_qualifying_age", period + ), ) wfp = parameters(period).gov.dwp.winter_fuel_payment on_mtb = ( @@ -37,7 +42,7 @@ def formula(household, period, parameters): in_england_or_wales = np.isin(country, ["ENGLAND", "WALES"]) meets_income_passport = ( household.any( - qualifying_age + age_eligible & ( taxable_income < wfp.eligibility.taxable_income_test.maximum_taxable_income @@ -50,7 +55,7 @@ def formula(household, period, parameters): meets_mtb_requirement = ( on_mtb | (not wfp.eligibility.require_benefits) | meets_income_passport ) - meets_spa_requirement = household.any(qualifying_age) | ( + meets_spa_requirement = household.any(age_eligible) | ( not wfp.eligibility.state_pension_age_requirement ) meets_higher_age_requirement = household.any( diff --git a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py index b8a24a5dd9..bb61d2b274 100644 --- a/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py @@ -11,7 +11,12 @@ class ni_class_4_liable(Variable): "year by their age. A person over State Pension age at the beginning of " "the tax year (6 April) is excepted; a person who reaches it during the " "year stays liable for the whole year. People under 16 are treated as " - "excepted, as they can be on application under regulation 93." + "excepted, as they can be on application under regulation 93. The model " + "also excepts someone reaching State Pension age on 6 April itself. " + "With default whole-number household ages, the inferred birthday is " + "6 April, so entering an integer State Pension age makes this boundary " + "reading determine the Class 4 result. Explicit birthday-month inputs " + "and fractional ages can place the birthday on another day." ) definition_period = YEAR reference = ( @@ -30,6 +35,9 @@ def formula(person, period, parameters): # pensionable age is attained at the commencement of the day, and # the enabling power (SSCBA 1992 s.17(2)(b)) is for "a person having # attained pensionable age". + # Default whole-number household ages put the birthday on 6 April + # (six months before 6 October). At an integer State Pension age, + # this boundary reading therefore determines that household's result. over_pension_age_at_start_of_year = ( person("months_since_state_pension_age", period) >= MONTHS_FROM_TAX_YEAR_START_TO_MID_YEAR From 739c7bd45d5be1f2d9ffafd77b690966819cfe15 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 11:41:15 -0400 Subject: [PATCH 07/13] Align qualifying-age property timing checks with dependency Preserve all assertions, strategies and example counts while suppressing the slow-generation health check already handled by the State Pension age dependency. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mt-1907-build-tests.fixed.md | 1 + ...est_state_pension_credit_qualifying_age.py | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 changelog.d/mt-1907-build-tests.fixed.md diff --git a/changelog.d/mt-1907-build-tests.fixed.md b/changelog.d/mt-1907-build-tests.fixed.md new file mode 100644 index 0000000000..fd5f836166 --- /dev/null +++ b/changelog.d/mt-1907-build-tests.fixed.md @@ -0,0 +1 @@ +- Prevented qualifying-age property tests from failing solely due to slow example generation while preserving their strategies, example counts and assertions. diff --git a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py index 3880ec222c..d537262b94 100644 --- a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -16,7 +16,7 @@ import numpy as np import pytest -from hypothesis import given, settings +from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -201,7 +201,11 @@ def assert_consumers_follow_the_qualifying_age(births, male, year): ) -@settings(max_examples=30, deadline=None) +@settings( + max_examples=30, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) @given( births=st.lists( st.dates(min_value=date(1945, 1, 1), max_value=date(1965, 12, 31)), @@ -216,7 +220,11 @@ def test_consumers_use_the_qualifying_age(births, male, year): assert_consumers_follow_the_qualifying_age(births, male, year) -@settings(max_examples=20, deadline=None) +@settings( + max_examples=20, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) @given( births=st.lists( st.dates( @@ -235,7 +243,11 @@ def test_consumers_use_the_qualifying_age_where_it_differs(births, year): assert_consumers_follow_the_qualifying_age(births, True, year) -@settings(max_examples=40, deadline=None) +@settings( + max_examples=40, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) @given( births=st.lists( st.dates(min_value=date(1945, 1, 1), max_value=date(1975, 12, 31)), From 1c9f354036bc39b6b7528ce4269948703eaaa07c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 11:43:08 -0400 Subject: [PATCH 08/13] Route pension-age means tests through statutory scheme conditions Use the shared Housing Benefit regime for applicable allowances, tariff, guarantee-credit passports and capital-limit selection, and the per-family CTR pensioner predicate for applicable allowances. Preserve rates and claimant/partner structure, add focused historical and component regressions, and clarify public metadata. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mt-1907-build-regimes.fixed.md | 1 + ...ousing_benefit_pension_age_means_test.yaml | 153 ++++++++++++++++++ .../housing_benefit_applicable_income.py | 8 +- .../housing_benefit_tariff_income.py | 19 ++- .../housing_benefit_applicable_amount.py | 22 ++- .../housing_benefit_assessable_capital.py | 8 +- .../housing_benefit_eligible.py | 9 +- .../guarantee_credit/guarantee_credit.py | 5 +- ...council_tax_reduction_applicable_amount.py | 25 +-- .../council_tax_reduction_pensioner.py | 5 +- 10 files changed, 214 insertions(+), 41 deletions(-) create mode 100644 changelog.d/mt-1907-build-regimes.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml diff --git a/changelog.d/mt-1907-build-regimes.fixed.md b/changelog.d/mt-1907-build-regimes.fixed.md new file mode 100644 index 0000000000..e94783f4ed --- /dev/null +++ b/changelog.d/mt-1907-build-regimes.fixed.md @@ -0,0 +1 @@ +Routed Housing Benefit means tests and Council Tax Reduction pensioner allowances through their shared statutory regime conditions, preserving qualifying-age UC and legacy-benefit exclusions. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml new file mode 100644 index 0000000000..9e098b60f5 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml @@ -0,0 +1,153 @@ +# HB Regs 2006 reg 5 and HB (SPC) Regs 2006 reg 5 route a claimant on +# income-based JSA to the working-age regulations even after the qualifying +# age. CTR SI 2012/2885 reg 3 applies the same exclusion to its pensioner test. +# These tests preserve the model's existing historical allowance parameters: +# 73.10 * 52 = 3,801.20 and 181 * 52 = 9,412, without asserting that the +# shared pension-age allowance parameter reproduces every statutory age band. + +- name: Qualifying-age claimant on income-based JSA uses working-age HB and CTR allowances + period: 2017 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 64 + months_since_last_birthday: 4 + is_male: true + benunits: + benunit: + members: [claimant] + jsa_income: 100 + income_support: 0 + esa_income: 0 + guarantee_credit: 0 + households: + household: + members: [claimant] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + output: + has_attained_state_pension_credit_qualifying_age: true + is_SP_age: false + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: false + council_tax_reduction_pensioner: false + benefits_premiums: 0 + housing_benefit_applicable_amount: 3_801.20 + council_tax_reduction_applicable_amount: 3_801.20 + +- name: Qualifying-age claimant without excluded benefits retains pension-age HB and CTR allowances + period: 2017 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 64 + months_since_last_birthday: 4 + is_male: true + benunits: + benunit: + members: [claimant] + jsa_income: 0 + income_support: 0 + esa_income: 0 + guarantee_credit: 0 + households: + household: + members: [claimant] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + output: + has_attained_state_pension_credit_qualifying_age: true + is_SP_age: false + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + council_tax_reduction_pensioner: true + benefits_premiums: 0 + housing_benefit_applicable_amount: 9_412 + council_tax_reduction_applicable_amount: 9_412 + +- name: Synthetic component routing does not apply the pension-age guarantee-credit passport under working-age flags + # This fixes the scheme flags and income components to test their consumers; + # it does not assert lawful receipt of these benefits together. HB (SPC) + # reg 26 disregards all income and capital only within that scheme. Working + # HB reg 52 gives (12,000 - 6,000) / 250 * 52 = 1,248 tariff income here. + # The separate working-age capital passports are outside this component test. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 64 + months_since_last_birthday: 4 + is_male: true + employment_income: 1_000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + benunits: + benunit: + members: [claimant] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: false + council_tax_reduction_pensioner: false + guarantee_credit: 1_000 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + benefits_premiums: 0 + housing_benefit_applicable_income_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + households: + household: + members: [claimant] + savings: 12_000 + output: + has_attained_state_pension_credit_qualifying_age: true + housing_benefit_applicable_amount: 3_801.20 + council_tax_reduction_applicable_amount: 3_801.20 + housing_benefit_assessable_capital: 12_000 + housing_benefit_tariff_income: 1_248 + housing_benefit_applicable_income: 2_248 + +- name: Synthetic component routing applies the guarantee-credit passport under pension-age flags + # The same controlled components now fall under the pension-age scheme. + # HB (SPC) reg 26: "the whole of his capital and income shall be disregarded". + period: 2017 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 64 + months_since_last_birthday: 4 + is_male: true + employment_income: 1_000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + benunits: + benunit: + members: [claimant] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + council_tax_reduction_pensioner: true + guarantee_credit: 1_000 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + benefits_premiums: 0 + housing_benefit_applicable_income_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + households: + household: + members: [claimant] + savings: 12_000 + output: + has_attained_state_pension_credit_qualifying_age: true + housing_benefit_applicable_amount: 9_412 + council_tax_reduction_applicable_amount: 9_412 + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + housing_benefit_applicable_income: 0 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 d0cc132c69..f8ada37026 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 @@ -17,8 +17,8 @@ def formula(benunit, period, parameters): members = person("is_claimant_or_partner", period) | person( "is_child_or_young_person_for_legacy_benefits", period ) - any_over_qualifying_age = benunit.any( - benunit.members("has_attained_state_pension_credit_qualifying_age", period) + pension_age_regulations = benunit( + "housing_benefit_pension_age_regulations_apply", period ) BENUNIT_MEANS_TESTED_BENEFITS = [ "child_benefit", @@ -83,7 +83,9 @@ def formula(benunit, period, parameters): - disregard - childcare_element, ) - guarantee_credit = any_over_qualifying_age & ( + # Pension HB reg 26 disregards "the whole of his capital and income" + # for guarantee-credit recipients within that regulation set. + guarantee_credit = pension_age_regulations & ( benunit("guarantee_credit", period) > 0 ) return where(guarantee_credit, 0, applicable_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py index 364b058b4c..de12150aad 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py @@ -8,28 +8,35 @@ class housing_benefit_tariff_income(Variable): documentation = "Weekly Housing Benefit tariff income from capital annualised to the model period." definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/52", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/26", + ) def formula(benunit, period, parameters): capital = benunit("housing_benefit_assessable_capital", period) - any_over_qualifying_age = benunit.any( - benunit.members("has_attained_state_pension_credit_qualifying_age", period) + # The pension tariff applies "For the purposes of these Regulations" + # (SI 2006/214 reg 29(2)); SI 2006/213 reg 52 supplies the working tariff. + pension_age_regulations = benunit( + "housing_benefit_pension_age_regulations_apply", period ) - guarantee_credit = any_over_qualifying_age & ( + guarantee_credit = pension_age_regulations & ( benunit("guarantee_credit", period) > 0 ) p = parameters(period).gov.dwp.housing_benefit.means_test.capital threshold = where( - any_over_qualifying_age, + pension_age_regulations, p.pension_age.tariff_income.threshold, p.working_age.tariff_income.threshold, ) step = where( - any_over_qualifying_age, + pension_age_regulations, p.pension_age.tariff_income.step, p.working_age.tariff_income.step, ) amount = where( - any_over_qualifying_age, + pension_age_regulations, p.pension_age.tariff_income.amount, p.working_age.tariff_income.amount, ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py index 957cd9e66f..cba43eaafe 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_applicable_amount.py @@ -8,37 +8,43 @@ class housing_benefit_applicable_amount(Variable): definition_period = YEAR unit = GBP defined_for = "housing_benefit_eligible" - reference = "https://www.legislation.gov.uk/uksi/2006/213/schedule/3" + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3", + ) def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.allowances - any_over_qualifying_age = benunit.any( - benunit.members("has_attained_state_pension_credit_qualifying_age", period) + # Each regulation set has its own Schedule 3 allowance table. Reg 5 + # keeps qualifying-age UC and specified legacy recipients in the + # working-age regulations, so age alone does not choose the table. + pension_age_regulations = benunit( + "housing_benefit_pension_age_regulations_apply", period ) eldest_age = benunit("eldest_claimant_or_partner_age", period) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold u_25 = eldest_age < older_age_threshold - o_25 = (eldest_age >= older_age_threshold) & ~any_over_qualifying_age - o_18 = (eldest_age >= younger_age_threshold) * ~any_over_qualifying_age + o_25 = (eldest_age >= older_age_threshold) & ~pension_age_regulations + o_18 = (eldest_age >= younger_age_threshold) * ~pension_age_regulations single = benunit("is_single_person", period) couple = benunit("is_couple", period) lone_parent = benunit("is_lone_parent", period) single_personal_allowance = ( u_25 * p.single.younger + o_25 * p.single.older - + any_over_qualifying_age * p.single.aged + + pension_age_regulations * p.single.aged ) couple_personal_allowance = ( u_18 * p.couple.younger + o_18 * p.couple.older - + any_over_qualifying_age * p.couple.aged + + pension_age_regulations * p.couple.aged ) lone_parent_personal_allowance = ( u_18 * p.lone_parent.younger + o_18 * p.lone_parent.older - + any_over_qualifying_age * p.lone_parent.aged + + pension_age_regulations * p.lone_parent.aged ) personal_allowance = ( single * single_personal_allowance diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py index 7d9e6a9866..78f86fde91 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py @@ -23,8 +23,8 @@ class housing_benefit_assessable_capital(Variable): def formula(benunit, period, parameters): household = benunit.household person = benunit.members - any_over_qualifying_age = benunit.any( - person("has_attained_state_pension_credit_qualifying_age", period) + pension_age_regulations = benunit( + "housing_benefit_pension_age_regulations_apply", period ) p = parameters(period).gov.dwp.housing_benefit.means_test.capital household_capital = sum(household(source, period) for source in p.sources) @@ -47,7 +47,9 @@ def formula(benunit, period, parameters): / claimant_partner_divisor, 0, ) - guarantee_credit = any_over_qualifying_age & ( + # Pension HB reg 26 disregards "the whole of his capital and income" + # for guarantee-credit recipients within that regulation set. + guarantee_credit = pension_age_regulations & ( benunit("guarantee_credit", period) > 0 ) return where( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py index 723aa13df7..3811f0f227 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py @@ -27,9 +27,6 @@ class housing_benefit_eligible(Variable): def formula(benunit, period, parameters): person = benunit.members - qualifying_age = person( - "has_attained_state_pension_credit_qualifying_age", period - ) # New claims are barred except where the claimant, and any partner, # has reached the qualifying age for State Pension Credit # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)), or the claim @@ -53,11 +50,13 @@ def formula(benunit, period, parameters): continuing_award = already_claiming & ~claiming_uc social = benunit.any(person("in_social_housing", period)) lha_eligible = benunit("LHA_eligible", period) - any_over_qualifying_age = benunit.any(qualifying_age) + pension_age_regulations = benunit( + "housing_benefit_pension_age_regulations_apply", period + ) capital = benunit("housing_benefit_assessable_capital", period) hb_capital = parameters(period).gov.dwp.housing_benefit.means_test.capital limit = where( - any_over_qualifying_age, + pension_age_regulations, hb_capital.pension_age.limit, hb_capital.working_age.limit, ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py index 6f2c714ac5..043a439337 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/guarantee_credit.py @@ -14,8 +14,9 @@ class guarantee_credit(Variable): documentation = ( "The guarantee credit element of State Pension Credit. It is part of " "State Pension Credit, so it is zero unless the benefit unit meets " - "the Pension Credit entitlement conditions (including State Pension " - "age for the claimant and any partner); other schemes that passport " + "the Pension Credit entitlement conditions, including the shared " + "qualifying-age conditions and any preserved mixed-age-couple saving; " + "other schemes that passport " "on Guarantee Credit read this variable." ) defined_for = "is_pension_credit_eligible" diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py index c8bc17edb7..6c3ba4e089 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py @@ -8,39 +8,40 @@ class council_tax_reduction_applicable_amount(Variable): definition_period = YEAR unit = GBP reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/6", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/2", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/2", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/7", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/1", "https://www.legislation.gov.uk/ssi/2021/249/schedule/1", ) def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.allowances - any_over_qualifying_age = benunit.any( - benunit.members("has_attained_state_pension_credit_qualifying_age", period) - ) + # England Sch 1 para 6 specifies the "applicable amount for a pensioner"; + # Wales and Scotland also have separate pensioner allowance tables. + # The shared definition includes qualifying age and award exclusions. + pensioner = benunit("council_tax_reduction_pensioner", period) eldest_age = benunit("eldest_claimant_or_partner_age", period) older_age_threshold = p.age_threshold.older younger_age_threshold = p.age_threshold.younger u_18 = eldest_age < younger_age_threshold u_25 = eldest_age < older_age_threshold - o_25 = (eldest_age >= older_age_threshold) & ~any_over_qualifying_age - o_18 = (eldest_age >= younger_age_threshold) & ~any_over_qualifying_age + o_25 = (eldest_age >= older_age_threshold) & ~pensioner + o_18 = (eldest_age >= younger_age_threshold) & ~pensioner single = benunit("is_single_person", period) couple = benunit("is_couple", period) lone_parent = benunit("is_lone_parent", period) single_personal_allowance = ( - u_25 * p.single.younger - + o_25 * p.single.older - + any_over_qualifying_age * p.single.aged + u_25 * p.single.younger + o_25 * p.single.older + pensioner * p.single.aged ) couple_personal_allowance = ( - u_18 * p.couple.younger - + o_18 * p.couple.older - + any_over_qualifying_age * p.couple.aged + u_18 * p.couple.younger + o_18 * p.couple.older + pensioner * p.couple.aged ) lone_parent_personal_allowance = ( u_18 * p.lone_parent.younger + o_18 * p.lone_parent.older - + any_over_qualifying_age * p.lone_parent.aged + + pensioner * p.lone_parent.aged ) personal_allowance = ( single * single_personal_allowance diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py index 72c0074af1..8c6115ce04 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py @@ -12,8 +12,9 @@ class council_tax_reduction_pensioner(Variable): "Income Support, income-based Jobseeker's Allowance or income-related " "Employment and Support Allowance, or has an award of Universal Credit. " "A mixed-age couple on Universal Credit is therefore not a pensioner in " - "England, Wales or Scotland. The model routes English schemes on this " - "test; the Welsh and Scottish formulas do not yet use it." + "England, Wales or Scotland. The shared applicable allowance uses this " + "test, as does English scheme selection; Welsh and Scottish scheme " + "selection retains its existing approximations." ) definition_period = YEAR reference = ( From efca9d4bde5bd2bd8d875768b660b6a21b43d397 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 18:29:37 -0400 Subject: [PATCH 09/13] Pin non-pensioner allowances for Welsh mixed-age UC regression Welsh regulation 3 excludes UC recipients from pensioner status. Assert the corrected shared-model allowance explicitly and retain the documented, separate UC-specific calculation gap. Co-Authored-By: Claude Opus 5.5 --- changelog.d/mt-1907-build-welsh-ctr.fixed.md | 1 + .../council_tax_reduction_pensioner.yaml | 22 ++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 changelog.d/mt-1907-build-welsh-ctr.fixed.md diff --git a/changelog.d/mt-1907-build-welsh-ctr.fixed.md b/changelog.d/mt-1907-build-welsh-ctr.fixed.md new file mode 100644 index 0000000000..a6de0c0efb --- /dev/null +++ b/changelog.d/mt-1907-build-welsh-ctr.fixed.md @@ -0,0 +1 @@ +Correct the Welsh mixed-age Council Tax Reduction regression to assert the non-pensioner allowance when the family has a Universal Credit award. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml index 444244a941..10e31d77bf 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml @@ -303,15 +303,15 @@ council_tax_reduction_household_has_pensioner: false council_tax_reduction_scheme_supported: true -- name: In Wales the cap reaches Council Tax Reduction through Universal Credit income - # The model's Welsh formula does not route on pensioner status, but it - # counts Universal Credit as income. Capping a mixed-age couple's award - # (cap 22,020 outside London; 30,000 + 3,000 - 22,020 = 10,980) lowers that - # income and so raises the reduction. Without the cap Universal Credit of - # 30,000 would take income far above the applicable amount. 1,582.84 is the - # model's pension-age formula, not the law: the Welsh route for Universal - # Credit applicants (WSI 2013/3029 Sch 6 paras 3 and 9) would give this - # family the maximum reduction, which the model does not yet implement. +- name: A capped Welsh mixed-age UC award does not use pensioner CTR allowances + # WSI 2013/3029 reg 3 excludes a "person with an award of universal credit" + # from pensioner status; Sch 7 covers "persons who are not pensioners". + # The cap still lowers UC income: 30,000 + 3,000 - 22,020 = 10,980. + # Under the model's shared national means test, the working-age applicable + # amount is 7,807.80 and the remaining 22,020 income exhausts the reduction. + # This pins scheme selection, not a statutory Welsh UC award: Sch 6 paras + # 3 and 9 use UC's maximum amount and income calculation, which the shared + # formula does not yet implement. That separate gap is flagged for review. period: 2026 absolute_error_margin: 0.01 input: @@ -340,4 +340,6 @@ benefit_cap_reduction: 10_980 universal_credit: 19_020 council_tax_reduction_applicable_income: 22_020 - council_tax_benefit: 1_582.84 + council_tax_reduction_pensioner: false + council_tax_reduction_applicable_amount: 7_807.80 + council_tax_benefit: 0 From 4906146770124e5c8bd7249692d8db2d11c519e4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 07:19:32 -0400 Subject: [PATCH 10/13] Choose each programme's premium schedule with its own regime test Main's #1938 picked the pension-age premium schedule for HB, CTR and Income Support when any member was over State Pension age, while this branch moves the HB and CTR allowances to their own statutory tests (HB(SPC) Regs reg 5; the CTR pensioner tests). A mixed-age couple on Universal Credit then got the working-age allowance with pension-age premiums. Each programme now chooses the schedule with the test that chooses its allowance; Income Support, whose Schedule 2 is a working-age schedule, uses the working-age premiums; and the pension-age severe disability premium follows the qualifying age of a claimant or partner. Also folds the build's changelog fragments into this PR's own. Co-Authored-By: Claude Opus 5.5 --- .../mt-1907-build-main-refresh.fixed.md | 1 - changelog.d/mt-1907-build-regimes.fixed.md | 1 - changelog.d/mt-1907-build-tests.fixed.md | 1 - changelog.d/mt-1907-build-welsh-ctr.fixed.md | 1 - changelog.d/mt-1907-build.changed.md | 1 - changelog.d/mt-1907-build.fixed.md | 1 - .../spc-qualifying-age-class-4-spa.changed.md | 1 + .../spc-qualifying-age-class-4-spa.fixed.md | 2 +- .../housing_benefit/pension_age_premiums.yaml | 36 +++++++++- ...ge_severe_disability_premium_properties.py | 22 ++++++- .../dwp/income_support_applicable_amount.py | 4 +- ...council_tax_reduction_applicable_amount.py | 8 ++- .../pension_age_severe_disability_premium.py | 19 ++++-- .../household/income/benefits_premiums.py | 66 ++++++++++++++----- 14 files changed, 132 insertions(+), 32 deletions(-) delete mode 100644 changelog.d/mt-1907-build-main-refresh.fixed.md delete mode 100644 changelog.d/mt-1907-build-regimes.fixed.md delete mode 100644 changelog.d/mt-1907-build-tests.fixed.md delete mode 100644 changelog.d/mt-1907-build-welsh-ctr.fixed.md delete mode 100644 changelog.d/mt-1907-build.changed.md delete mode 100644 changelog.d/mt-1907-build.fixed.md create mode 100644 changelog.d/spc-qualifying-age-class-4-spa.changed.md diff --git a/changelog.d/mt-1907-build-main-refresh.fixed.md b/changelog.d/mt-1907-build-main-refresh.fixed.md deleted file mode 100644 index 40d0443cf3..0000000000 --- a/changelog.d/mt-1907-build-main-refresh.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Preserved Housing Benefit's legacy income and capital passports alongside qualifying-age regime routing and aligned the carer overlap test reference with the statutory State Pension type cutoff. diff --git a/changelog.d/mt-1907-build-regimes.fixed.md b/changelog.d/mt-1907-build-regimes.fixed.md deleted file mode 100644 index e94783f4ed..0000000000 --- a/changelog.d/mt-1907-build-regimes.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Routed Housing Benefit means tests and Council Tax Reduction pensioner allowances through their shared statutory regime conditions, preserving qualifying-age UC and legacy-benefit exclusions. diff --git a/changelog.d/mt-1907-build-tests.fixed.md b/changelog.d/mt-1907-build-tests.fixed.md deleted file mode 100644 index fd5f836166..0000000000 --- a/changelog.d/mt-1907-build-tests.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Prevented qualifying-age property tests from failing solely due to slow example generation while preserving their strategies, example counts and assertions. diff --git a/changelog.d/mt-1907-build-welsh-ctr.fixed.md b/changelog.d/mt-1907-build-welsh-ctr.fixed.md deleted file mode 100644 index a6de0c0efb..0000000000 --- a/changelog.d/mt-1907-build-welsh-ctr.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Correct the Welsh mixed-age Council Tax Reduction regression to assert the non-pensioner allowance when the family has a Universal Credit award. diff --git a/changelog.d/mt-1907-build.changed.md b/changelog.d/mt-1907-build.changed.md deleted file mode 100644 index 793cd25978..0000000000 --- a/changelog.d/mt-1907-build.changed.md +++ /dev/null @@ -1 +0,0 @@ -- Applied pensionable age to Winter Fuel Payment from 2024-25 while retaining the State Pension Credit qualifying-age test for earlier years, including when reforms separate the timetables. diff --git a/changelog.d/mt-1907-build.fixed.md b/changelog.d/mt-1907-build.fixed.md deleted file mode 100644 index ee3de5918a..0000000000 --- a/changelog.d/mt-1907-build.fixed.md +++ /dev/null @@ -1 +0,0 @@ -- Reconciled State Pension Credit qualifying-age consumers with claimant and partner roles, mixed-age couple savings, and the Housing Benefit and Council Tax Reduction pension-age switches. diff --git a/changelog.d/spc-qualifying-age-class-4-spa.changed.md b/changelog.d/spc-qualifying-age-class-4-spa.changed.md new file mode 100644 index 0000000000..562284a8a0 --- /dev/null +++ b/changelog.d/spc-qualifying-age-class-4-spa.changed.md @@ -0,0 +1 @@ +Winter Fuel Payment uses pensionable age from 2024-25 and the State Pension Credit qualifying age in earlier years, as the regulations in force in each year require. diff --git a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md index 2a4f7c7679..9c89eec154 100644 --- a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md +++ b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md @@ -1 +1 @@ -Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). +Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). Housing Benefit and Council Tax Reduction choose their pension-age or working-age allowances, means tests and premium schedules with their own statutory tests (HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests), which exclude families on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance; the pension-age severe disability premium follows the qualifying age. Income Support uses its own working-age premiums. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml index 6a6f03994f..f48a31b1f6 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_premiums.yaml @@ -260,7 +260,7 @@ output: severe_disability_minimum_guarantee_addition: 86.05 * 52 -- name: The pension-age premium is zero below State Pension age +- name: The pension-age premium is zero below the State Pension Credit qualifying age period: 2026 absolute_error_margin: 0.01 input: @@ -277,3 +277,37 @@ country: {2026: ENGLAND} output: pension_age_severe_disability_premium: 0 + +- name: A mixed-age couple on Universal Credit uses the working-age premium schedule + # HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests exclude a couple on + # Universal Credit, so the working-age allowance and premiums apply even + # though one partner is over State Pension age. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: {2026: 50} + is_disabled_for_benefits: {2026: true} + partner: + age: {2026: 70} + state_pension: {2026: 3_000} + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: {2026: ENGLAND} + tenure_type: {2026: RENT_FROM_COUNCIL} + rent: {2026: 6_240} + council_tax: {2026: 2_000} + output: + is_uc_entitled: true + housing_benefit_pension_age_regulations_apply: false + council_tax_reduction_pensioner: false + pension_age_severe_disability_premium: 0 + # Legacy disability premium (couple) 64.00 * 52 = 3,328. + benefits_premiums: 64.00 * 52 + # Working-age couple allowance 150.15 * 52 = 7,807.80, plus the premium. + council_tax_reduction_applicable_amount: (150.15 + 64.00) * 52 diff --git a/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py b/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py index 1475662ab8..a0deb726ef 100644 --- a/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py +++ b/policyengine_uk/tests/test_pension_age_severe_disability_premium_properties.py @@ -215,9 +215,21 @@ def test_premiums_follow_the_pension_age_and_working_age_schedules(adults, age): def get(variable): return float(simulation.calculate(variable, YEAR)[0]) - pension_age = bool(simulation.calculate("is_SP_age", YEAR).any()) + # The premium needs a claimant or partner over the State Pension Credit + # qualifying age; HB's own regime switch then chooses the schedule. + over_qualifying_age = bool( + ( + simulation.calculate("is_claimant_or_partner", YEAR) + & simulation.calculate( + "has_attained_state_pension_credit_qualifying_age", YEAR + ) + ).any() + ) + pension_regulations = bool( + simulation.calculate("housing_benefit_pension_age_regulations_apply", YEAR)[0] + ) premium = get("pension_age_severe_disability_premium") - if pension_age: + if over_qualifying_age: weekly_rate = float( simulation.tax_benefit_system.parameters( YEAR @@ -232,9 +244,13 @@ def get(variable): assert ( abs(premium - get("severe_disability_minimum_guarantee_addition")) < 0.005 ) - expected = premium + get("carer_premium") else: assert premium == 0 + # Reg 5 needs the qualifying age, so the pension-age schedule implies it. + assert over_qualifying_age or not pension_regulations + if pension_regulations: + expected = premium + get("carer_premium") + else: expected = sum( get(variable) for variable in [ diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py index a560722817..b3195ffc2e 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_amount.py @@ -53,5 +53,7 @@ def formula(benunit, period, parameters): ], ) personal_allowance = personal_allowance_weekly * WEEKS_IN_YEAR - premiums = benunit("benefits_premiums", period) + # IS Regs 1987 Sch 2 Part III: the pension-age HB and CTR schedules + # never apply to Income Support. + premiums = benunit("working_age_benefits_premiums", period) return personal_allowance + premiums diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py index 6c3ba4e089..9b4f657c29 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py @@ -48,5 +48,11 @@ def formula(benunit, period, parameters): + couple * couple_personal_allowance + lone_parent * lone_parent_personal_allowance ) * WEEKS_IN_YEAR - premiums = benunit("benefits_premiums", period) + # The pension-age schedules have only the severe disability and carer + # premiums; the same pensioner test chooses the schedule. + premiums = where( + pensioner, + benunit("pension_age_benefits_premiums", period), + benunit("working_age_benefits_premiums", period), + ) return personal_allowance + premiums diff --git a/policyengine_uk/variables/household/demographic/pension_age_severe_disability_premium.py b/policyengine_uk/variables/household/demographic/pension_age_severe_disability_premium.py index 7921327326..5a41077d36 100644 --- a/policyengine_uk/variables/household/demographic/pension_age_severe_disability_premium.py +++ b/policyengine_uk/variables/household/demographic/pension_age_severe_disability_premium.py @@ -7,14 +7,15 @@ class pension_age_severe_disability_premium(Variable): label = "Severe disability premium for pension-age Housing Benefit and CTR" documentation = ( "The severe disability premium in the Housing Benefit and Council Tax " - "Reduction applicable amount of a family over State Pension age. " + "Reduction applicable amount of a family whose claimant or partner has " + "reached the qualifying age for State Pension Credit. " "HB(SPC) Regs 2006 Sch 3 para 6 sets the same conditions as the " "working-age premium (HB Regs 2006 Sch 3 para 14): the qualifying " "benefits, both partners qualifying unless the other is blind, no " "non-dependant aged 18 or over with the same exceptions (para 6(6), " "reg 3), and no carer benefit paid for caring for the claimant or " "partner. Para 12 sets the same weekly amounts. So this is " - "severe_disability_premium for a family over State Pension age, with " + "severe_disability_premium for such a family, with " "its amounts and its documented assumptions: where only one partner " "qualifies and the other is blind, the qualifying partner is taken to " "claim, as a couple may agree under HB(SPC) reg 63(1) and the CTR " @@ -41,5 +42,15 @@ class pension_age_severe_disability_premium(Variable): ) def formula(benunit, period, parameters): - pension_age = benunit.any(benunit.members("is_SP_age", period)) - return where(pension_age, benunit("severe_disability_premium", period), 0) + # HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests turn on a + # claimant or partner having attained the qualifying age for State + # Pension Credit; each programme's own switch then chooses the + # schedule (benefits_premiums, council_tax_reduction_applicable_amount). + person = benunit.members + over_qualifying_age = benunit.any( + person("is_claimant_or_partner", period) + & person("has_attained_state_pension_credit_qualifying_age", period) + ) + return where( + over_qualifying_age, benunit("severe_disability_premium", period), 0 + ) diff --git a/policyengine_uk/variables/household/income/benefits_premiums.py b/policyengine_uk/variables/household/income/benefits_premiums.py index 17780055eb..8bfdff9448 100644 --- a/policyengine_uk/variables/household/income/benefits_premiums.py +++ b/policyengine_uk/variables/household/income/benefits_premiums.py @@ -18,16 +18,32 @@ ] -class benefits_premiums(Variable): +class working_age_benefits_premiums(Variable): value_type = float entity = BenUnit - label = "Value of premiums for disability and carer status" + label = "Premiums in the working-age schedules" documentation = ( - "Premiums in the Housing Benefit, Council Tax Reduction and Income " - "Support applicable amounts. A family with a member over State " - "Pension age uses the pension-age schedules: the severe disability " - "premium and the carer premium. Those schedules have no adult disability or " - "enhanced disability premium." + "The disability, enhanced disability, severe disability and carer " + "premiums, as in the working-age Housing Benefit and Council Tax " + "Reduction schedules and the Income Support schedule." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2", + ) + adds = WORKING_AGE_PREMIUMS + + +class pension_age_benefits_premiums(Variable): + value_type = float + entity = BenUnit + label = "Premiums in the pension-age schedules" + documentation = ( + "The severe disability and carer premiums, as in the pension-age " + "Housing Benefit and Council Tax Reduction schedules, which have no " + "adult disability or enhanced disability premium." ) definition_period = YEAR unit = GBP @@ -37,17 +53,37 @@ class benefits_premiums(Variable): "https://www.legislation.gov.uk/ssi/2012/319/schedule/1", "https://www.legislation.gov.uk/wsi/2013/3035/schedule/2", "https://www.legislation.gov.uk/nisr/2006/406/schedule/4", + ) + adds = PENSION_AGE_PREMIUMS + + +class benefits_premiums(Variable): + value_type = float + entity = BenUnit + label = "Value of premiums for disability and carer status" + documentation = ( + "Premiums in the Housing Benefit applicable amount. Where the claim " + "falls under the pension-age regulations " + "(housing_benefit_pension_age_regulations_apply), the pension-age " + "schedule: the severe disability premium and the carer premium. " + "Otherwise the working-age schedule. This is the switch the personal " + "allowance uses. Council Tax Reduction chooses between the same two " + "schedules with council_tax_reduction_pensioner, and Income Support " + "uses the working-age schedule." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/214/regulation/5", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3", "https://www.legislation.gov.uk/uksi/2006/213/schedule/3", - "https://www.legislation.gov.uk/uksi/1987/1967/schedule/2", + "https://www.legislation.gov.uk/nisr/2006/406/schedule/4", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/4", ) def formula(benunit, period, parameters): - # Same switch as the aged personal allowance in the Housing Benefit - # and Council Tax Reduction applicable amounts. Income Support is not - # paid to a family with a member over State Pension age. - pension_age = benunit.any(benunit.members("is_SP_age", period)) return where( - pension_age, - add(benunit, period, PENSION_AGE_PREMIUMS), - add(benunit, period, WORKING_AGE_PREMIUMS), + benunit("housing_benefit_pension_age_regulations_apply", period), + benunit("pension_age_benefits_premiums", period), + benunit("working_age_benefits_premiums", period), ) From a4b9f73a21c31448d406d2776221c9a799f9824f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 08:16:09 -0400 Subject: [PATCH 11/13] Apply the Income Support age test to the claimant, not the whole family SSCBA 1992 s.124(1)(aa) requires that the claimant has not attained the qualifying age for State Pension Credit, and either member of a couple can claim, so a mixed-age couple can claim through the younger partner. The test barred the family if any member had attained the qualifying age. s.124(1)(g) separately bars Income Support where the other member is entitled to State Pension Credit; reading the Pension Credit amount would be circular, so the couple is taken to be on it where it meets the Pension Credit age conditions and would claim it. Adds three YAML cases: the 2017 cohort couple with and without a Pension Credit claim, and a mixed-age couple in 2026. Co-Authored-By: Claude Opus 5.5 --- .../spc-qualifying-age-class-4-spa.fixed.md | 2 +- .../state_pension_credit_qualifying_age.yaml | 80 +++++++++++++++++++ .../gov/dwp/income_support_eligible.py | 29 +++++-- 3 files changed, 105 insertions(+), 6 deletions(-) diff --git a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md index 9c89eec154..5d075f5232 100644 --- a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md +++ b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md @@ -1 +1 @@ -Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). Housing Benefit and Council Tax Reduction choose their pension-age or working-age allowances, means tests and premium schedules with their own statutory tests (HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests), which exclude families on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance; the pension-age severe disability premium follows the qualifying age. Income Support uses its own working-age premiums. +Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). Housing Benefit and Council Tax Reduction choose their pension-age or working-age allowances, means tests and premium schedules with their own statutory tests (HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests), which exclude families on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance; the pension-age severe disability premium follows the qualifying age. Income Support uses its own working-age premiums, and its age test applies to the claimant, so a mixed-age couple can claim it through the younger partner unless the couple is on Pension Credit (SSCBA 1992 s.124(1)(aa) and (g)). diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml index d16bdce73b..d60f841d14 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -483,3 +483,83 @@ savings: 20_000 output: pension_credit_assessable_capital: 0 + +- name: Case 21, a mixed-age couple can claim Income Support through the younger partner. + # SSCBA 1992 s.124(1)(aa) restricts the claimant, and either member of a + # couple can claim. She is a carer under the qualifying age with an + # existing award; he was born 6 June 1953 and reached the qualifying age on + # 6 March 2017, though his own pensionable age is 6 June 2018. They have not + # claimed Pension Credit. + period: 2017 + input: + people: + carer: + age: 50 + is_carer_for_benefits: true + income_support_reported: 1_000 + man: + age: 64 + months_since_last_birthday: 4 + is_male: true + benunits: + benunit: + members: [carer, man] + would_claim_pc: false + households: + household: + members: [carer, man] + output: + has_attained_state_pension_credit_qualifying_age: [false, true] + pension_credit: 0 + income_support_eligible: true + +- name: Case 22, the same couple cannot have Income Support while on Pension Credit. + # s.124(1)(g): the other member of the couple is not entitled to State + # Pension Credit. Before 15 May 2019 the older member could claim it for the + # couple; the model treats a couple that meets the Pension Credit age + # conditions and would claim it as on Pension Credit. + period: 2017 + input: + people: + carer: + age: 50 + is_carer_for_benefits: true + income_support_reported: 1_000 + man: + age: 64 + months_since_last_birthday: 4 + is_male: true + benunits: + benunit: + members: [carer, man] + would_claim_pc: true + households: + household: + members: [carer, man] + output: + meets_pension_credit_age_conditions: true + pension_credit: 12_649 + income_support_eligible: false + +- name: Case 23, a mixed-age couple after May 2019 can keep Income Support through the younger partner. + # Pension Credit needs both members over the qualifying age from 15 May 2019 + # (SPCA 2002 s.4(1A)) unless the SI 2019/37 saving applies, which these + # inputs do not give. + period: 2026 + input: + people: + carer: + age: 50 + is_carer_for_benefits: true + income_support_reported: 1_000 + partner: + age: 70 + benunits: + benunit: + members: [carer, partner] + households: + household: + members: [carer, partner] + output: + pension_credit: 0 + income_support_eligible: true diff --git a/policyengine_uk/variables/gov/dwp/income_support_eligible.py b/policyengine_uk/variables/gov/dwp/income_support_eligible.py index 10cbd902b5..998a815ab2 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_eligible.py +++ b/policyengine_uk/variables/gov/dwp/income_support_eligible.py @@ -6,7 +6,10 @@ class income_support_eligible(Variable): entity = BenUnit label = "Whether eligible for Income Support" definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/1987/1967/schedule/1B" + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/1B", + "https://www.legislation.gov.uk/ukpga/1992/4/section/124", + ) def formula(benunit, period, parameters): IS = parameters(period).gov.dwp.income_support @@ -20,17 +23,33 @@ def formula(benunit, period, parameters): lone_parent_with_young_child = lone_parent & youngest_child_5_or_under has_carers = add(benunit, period, ["is_carer_for_benefits"]) > 0 # SSCBA 1992 s.124(1)(aa): the claimant has not attained the qualifying - # age for State Pension Credit. - none_over_qualifying_age = ~benunit.any( - benunit.members("has_attained_state_pension_credit_qualifying_age", period) + # age for State Pension Credit. Either member of a couple can claim, so + # a mixed-age couple can claim through the younger one. + person = benunit.members + claimant_under_qualifying_age = benunit.any( + person("is_claimant_or_partner", period) + & ~person("has_attained_state_pension_credit_qualifying_age", period) ) + # s.124(1)(g): the other member of a couple is not entitled to State + # Pension Credit. Entitlement needs a claim (SSAA 1992 s.1). Reading the + # Pension Credit amount here would be circular (Pension Credit income + # counts working tax credit, whose income counts Income Support), so a + # couple is taken to be on Pension Credit where it meets the Pension + # Credit age conditions and would claim it. That leaves out Pension + # Credit's means test: a couple whose income is too high for Pension + # Credit is treated as on it, which bars an Income Support award only + # where its income is below the lower Income Support applicable amount. + on_pension_credit = benunit( + "meets_pension_credit_age_conditions", period + ) & benunit("would_claim_pc", period) has_esa_income = benunit("esa_income", period) > 0 already_claiming = add(benunit, period, ["income_support_reported"]) > 0 capital = benunit("income_support_assessable_capital", period) limit = IS.means_test.capital.limit return ( (has_carers | lone_parent_with_young_child) - & none_over_qualifying_age + & claimant_under_qualifying_age + & ~on_pension_credit & ~has_esa_income & already_claiming & (capital <= limit) From 32a6c17547cf9f273d1eb33e4126a0c77b404802 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 08:34:45 -0400 Subject: [PATCH 12/13] Require the Income Support claimant to meet both the age and category tests The age test and the carer test could be met by different members of a couple: the younger partner under the qualifying age and the older one a carer. SSCBA 1992 s.124(1)(aa) and (e) and IS Regs 1987 Sch 1B require the same claimant to meet both, so the formula now finds a claimant or partner who is under the qualifying age and a carer (or the lone parent of a young child). Carers who are neither claimant nor partner no longer count. Adds Case 24 (only the older partner is a carer: ineligible) and Cases 25-26, which show the Pension Credit proxy barring Income Support for a couple whose income is too high for Pension Credit, and that Income Support is nil for that couple anyway. Co-Authored-By: Claude Opus 5.5 --- .../spc-qualifying-age-class-4-spa.fixed.md | 2 +- .../state_pension_credit_qualifying_age.yaml | 73 +++++++++++++++++++ .../gov/dwp/income_support_eligible.py | 35 +++++---- 3 files changed, 95 insertions(+), 15 deletions(-) diff --git a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md index 5d075f5232..9394083175 100644 --- a/changelog.d/spc-qualifying-age-class-4-spa.fixed.md +++ b/changelog.d/spc-qualifying-age-class-4-spa.fixed.md @@ -1 +1 @@ -Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). Housing Benefit and Council Tax Reduction choose their pension-age or working-age allowances, means tests and premium schedules with their own statutory tests (HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests), which exclude families on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance; the pension-age severe disability premium follows the qualifying age. Income Support uses its own working-age premiums, and its age test applies to the claimant, so a mixed-age couple can claim it through the younger partner unless the couple is on Pension Credit (SSCBA 1992 s.124(1)(aa) and (g)). +Pension Credit, Universal Credit, Housing Benefit, Council Tax Reduction, Income Support, the benefit cap (which reaches only working-age Housing Benefit and Universal Credit) and Winter Fuel Payment now use the qualifying age for State Pension Credit (State Pension Credit Act 2002 s.1(6)) instead of each person's own State Pension age. For a man born before 6 December 1953 that is the State Pension age of a woman born on the same day, not 65, so these programmes change only in 2018-19 and earlier. National Insurance and State Pension keep using the person's own State Pension age; Class 4 now reads it at the start of the tax year (see the Class 4 entry). Housing Benefit and Council Tax Reduction choose their pension-age or working-age allowances, means tests and premium schedules with their own statutory tests (HB(SPC) Regs 2006 reg 5 and the CTR pensioner tests), which exclude families on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance; the pension-age severe disability premium follows the qualifying age. Income Support uses its own working-age premiums, and its age test applies to the claimant, who must also be the one in a prescribed category such as carer, so a mixed-age couple can claim it through the younger partner unless the couple is on Pension Credit (SSCBA 1992 s.124(1)(aa), (e) and (g)). diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml index d60f841d14..ddbdc9255a 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -563,3 +563,76 @@ output: pension_credit: 0 income_support_eligible: true + +- name: Case 24, the claimant must meet both the age and the carer conditions. + # SSCBA 1992 s.124(1)(aa) and (e), IS Regs 1987 Sch 1B para 4: the younger + # partner is under the qualifying age but is not a carer, and the older + # partner is a carer but over the qualifying age, so neither can claim. + period: 2026 + input: + people: + younger: + age: 50 + income_support_reported: 1_000 + older: + age: 70 + is_carer_for_benefits: true + benunits: + benunit: + members: [younger, older] + households: + household: + members: [younger, older] + output: + income_support_eligible: false + +- name: Case 25, a couple that would claim Pension Credit is treated as on it even when its income is too high. + # The s.124(1)(g) bar reads the Pension Credit age conditions and take-up, + # not the Pension Credit means test (see income_support_eligible). Here + # Pension Credit is nil, yet Income Support is barred. + period: 2017 + input: + people: + carer: + age: 50 + is_carer_for_benefits: true + income_support_reported: 1_000 + man: + age: 64 + months_since_last_birthday: 4 + is_male: true + private_pension_income: 30_000 + benunits: + benunit: + members: [carer, man] + would_claim_pc: true + households: + household: + members: [carer, man] + output: + pension_credit: 0 + income_support_eligible: false + +- name: Case 26, the same couple without a Pension Credit claim is eligible for Income Support, but it is nil at that income. + period: 2017 + input: + people: + carer: + age: 50 + is_carer_for_benefits: true + income_support_reported: 1_000 + man: + age: 64 + months_since_last_birthday: 4 + is_male: true + private_pension_income: 30_000 + benunits: + benunit: + members: [carer, man] + would_claim_pc: false + households: + household: + members: [carer, man] + output: + income_support_eligible: true + income_support: 0 diff --git a/policyengine_uk/variables/gov/dwp/income_support_eligible.py b/policyengine_uk/variables/gov/dwp/income_support_eligible.py index 998a815ab2..83bbc69f3d 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_eligible.py +++ b/policyengine_uk/variables/gov/dwp/income_support_eligible.py @@ -19,16 +19,24 @@ def formula(benunit, period, parameters): benunit("youngest_child_age_for_legacy_benefits", period) <= IS.eligibility.lone_parent_youngest_child_age_limit ) - lone_parent = benunit("is_lone_parent", period) - lone_parent_with_young_child = lone_parent & youngest_child_5_or_under - has_carers = add(benunit, period, ["is_carer_for_benefits"]) > 0 - # SSCBA 1992 s.124(1)(aa): the claimant has not attained the qualifying - # age for State Pension Credit. Either member of a couple can claim, so - # a mixed-age couple can claim through the younger one. person = benunit.members - claimant_under_qualifying_age = benunit.any( - person("is_claimant_or_partner", period) - & ~person("has_attained_state_pension_credit_qualifying_age", period) + # SSCBA 1992 s.124(1)(aa) and (e): the claimant has not attained the + # qualifying age for State Pension Credit and is in a prescribed + # category (Sch 1B). Either member of a couple can claim, so a + # mixed-age couple can claim through the younger one, but the same + # person must meet both conditions. + could_claim = person("is_claimant_or_partner", period) & ~person( + "has_attained_state_pension_credit_qualifying_age", period + ) + # Sch 1B para 1: a lone parent of a young child, who is the only + # claimant or partner in the benefit unit. + lone_parent = benunit("is_lone_parent", period) + lone_parent_with_young_child = ( + lone_parent & youngest_child_5_or_under & benunit.any(could_claim) + ) + # Sch 1B para 4: a carer. + carer_claimant = benunit.any( + could_claim & person("is_carer_for_benefits", period) ) # s.124(1)(g): the other member of a couple is not entitled to State # Pension Credit. Entitlement needs a claim (SSAA 1992 s.1). Reading the @@ -36,9 +44,9 @@ def formula(benunit, period, parameters): # counts working tax credit, whose income counts Income Support), so a # couple is taken to be on Pension Credit where it meets the Pension # Credit age conditions and would claim it. That leaves out Pension - # Credit's means test: a couple whose income is too high for Pension - # Credit is treated as on it, which bars an Income Support award only - # where its income is below the lower Income Support applicable amount. + # Credit's means test: a couple the means test would refuse Pension + # Credit is still treated as on it, so Income Support is barred where it + # could be payable but Pension Credit would not be. on_pension_credit = benunit( "meets_pension_credit_age_conditions", period ) & benunit("would_claim_pc", period) @@ -47,8 +55,7 @@ def formula(benunit, period, parameters): capital = benunit("income_support_assessable_capital", period) limit = IS.means_test.capital.limit return ( - (has_carers | lone_parent_with_young_child) - & claimant_under_qualifying_age + (carer_claimant | lone_parent_with_young_child) & ~on_pension_credit & ~has_esa_income & already_claiming From 43a52e2598b396c6018438eb087bf9d2ebbbe8c8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 08:59:43 -0400 Subject: [PATCH 13/13] Apply the savings-credit-only tariff exemption only under the pension-age regulations HB(SPC) Regs 2006 reg 27(5) lifts the tariff income rule for a savings-credit-only award, within the pension-age regulations. The main merge gated #1945's income and capital branches on housing_benefit_pension_age_regulations_apply, but the tariff branch merged automatically without it. Adds a component routing case that fails without the gate. Co-Authored-By: Claude Opus 5.5 --- ...ousing_benefit_pension_age_means_test.yaml | 27 +++++++++++++++++++ .../housing_benefit_tariff_income.py | 6 ++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml index 9e098b60f5..4e721cbe6b 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml @@ -151,3 +151,30 @@ housing_benefit_assessable_capital: 0 housing_benefit_tariff_income: 0 housing_benefit_applicable_income: 0 + +- name: Synthetic component routing keeps the working-age tariff income for savings-credit-only status under working-age flags + # This fixes the scheme flags and savings-credit-only status to test their + # consumer; it does not assert a lawful award. HB (SPC) reg 27(5) lifts the + # tariff income rule only within the pension-age regulations. Working HB + # reg 52 gives (12,000 - 6,000) / 250 * 52 = 1,248 tariff income here. + period: 2017 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 64 + months_since_last_birthday: 4 + is_male: true + benunits: + benunit: + members: [claimant] + housing_benefit_pension_age_regulations_apply: false + in_receipt_of_savings_credit_only: true + guarantee_credit: 0 + households: + household: + members: [claimant] + savings: 12_000 + output: + housing_benefit_assessable_capital: 12_000 + housing_benefit_tariff_income: 1_248 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py index 7321e3a69f..9280adbd13 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py @@ -47,6 +47,10 @@ def formula(benunit, period, parameters): # Pension Credit is savings credit only, Housing Benefit's tariff # income rule does not apply; the Secretary of State's assessment of # income already counts Pension Credit deemed income from capital. - savings_credit_only = benunit("in_receipt_of_savings_credit_only", period) + # That regulation is in the pension-age regulations, so it applies only + # where they do. + savings_credit_only = pension_age_regulations & benunit( + "in_receipt_of_savings_credit_only", period + ) tariff_income = where(savings_credit_only, 0, tariff_income) return where(guarantee_credit, 0, tariff_income)