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..779d5befa6 --- /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 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/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.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 new file mode 100644 index 0000000000..9394083175 --- /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 (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/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 9f2c1d8b09..ed74f0f54f 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -102,6 +102,35 @@ 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 (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 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. 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`. + 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/docs/book/programs/gov/hmrc/national-insurance.ipynb b/docs/book/programs/gov/hmrc/national-insurance.ipynb index 20425241ec..9b15f48f22 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, charged on `ni_class_4_profits`. Those are the trade profits chargeable to income tax under ITTOIA 2005 Part 2 Chapter 2 (`ni_class_4_profits_before_losses`: self-employment profit after capital allowances and the trading allowance), less trading losses as SSCBA 1992 Schedule 2 paragraph 3 allows (`ni_class_4_loss_relief`). A loss reduces only trade profits for Class 4: each year's losses (those brought forward, then the year's own loss, `ni_class_4_trading_loss`) are set against that year's trade profits, and the rest is carried forward until used (`ni_class_4_losses_brought_forward`, `ni_class_4_losses_carried_forward`). A year's own loss is the `trading_loss` entered for that year, so a loss entered once is relieved once: it is not counted again in the later years that `trading_loss` carries into. Carry-back claims and the paragraph 3(5) deductions for trade interest and royalties are not modelled. 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, charged on `ni_class_4_profits`. Those are the trade profits chargeable to income tax under ITTOIA 2005 Part 2 Chapter 2 (`ni_class_4_profits_before_losses`: self-employment profit after capital allowances and the trading allowance), less trading losses as SSCBA 1992 Schedule 2 paragraph 3 allows (`ni_class_4_loss_relief`). A loss reduces only trade profits for Class 4: each year's losses (those brought forward, then the year's own loss, `ni_class_4_trading_loss`) are set against that year's trade profits, and the rest is carried forward until used (`ni_class_4_losses_brought_forward`, `ni_class_4_losses_carried_forward`). A year's own loss is the `trading_loss` entered for that year, so a loss entered once is relieved once: it is not counted again in the later years that `trading_loss` carries into. Carry-back claims and the paragraph 3(5) deductions for trade interest and royalties are not modelled. 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 \u2014 `ni_class_1_employee + ni_class_2 + ni_class_3 + ni_class_4` \u2014 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/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/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..4e721cbe6b --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_pension_age_means_test.yaml @@ -0,0 +1,180 @@ +# 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 + +- 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/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..ddbdc9255a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -0,0 +1,638 @@ +# 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 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. +# 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. 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. 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: + 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_mixed_age_couple: false + meets_pension_credit_age_conditions: 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 + +- 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 + +- 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/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/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..377d0dd60e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_liable.yaml @@ -0,0 +1,146 @@ +# 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 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: + 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/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 diff --git a/policyengine_uk/tests/test_carer_overlapping_benefits_properties.py b/policyengine_uk/tests/test_carer_overlapping_benefits_properties.py index 3c17abe12a..8bb3314a77 100644 --- a/policyengine_uk/tests/test_carer_overlapping_benefits_properties.py +++ b/policyengine_uk/tests/test_carer_overlapping_benefits_properties.py @@ -37,12 +37,14 @@ for a pensioner below the personal allowance, Pension Credit income is State Pension plus private pension plus the payable carer benefit. -The test takes these conventions from the model, not from the law: -- State Pension age in 2026 is 66 for both genders. The model's annual - approximation places the old/new cutoff at 75 for men and 74 for women: - the 2016 pension ages (65 and 64) plus the ten elapsed years. The reference - computes the type from these thresholds without reading the model's - calculated state_pension_type. +The reference uses the statutory pension cutoff and these model conventions: +- Whole ages with the default six months since the last birthday infer + 6 April birthdays. At 6 October 2026, pensionable age has been attained + from age 66; the old State Pension starts at 76 for men and 74 for women. + Pensions Act 2014 s. 1(2) requires attainment before 6 April 2016: a man + aged 75 was born on 6 April 1951 and attains pensionable age on that + cutoff day, so has a new State Pension. The reference computes the type + independently of the model's calculated state_pension_type. - A State Pension supplied directly, with no components, overlaps in full. - A reported old State Pension is split at the full basic rate, and the excess is treated as additional pension, which does not overlap. Basic @@ -77,10 +79,14 @@ "incapacity_benefit_reported", "maternity_allowance_reported", ] -# From state_pension/age/{male,female}.yaml and the 2016 activation in -# state_pension/new_state_pension/active.yaml; annual model conventions. +# Whole ages with the six-month default imply 6 April birthdays. These are +# the attainment and old-pension thresholds at 6 October 2026, independently +# of the model's calculated type. Pensions Act 2014 s.1(2) says "before +# 6 April 2016": men aged 75 attain on that date, whereas those aged 76 +# attained on 6 April 2015. Women born on 6 April 1952 attained on 6 May 2014; +# those born on 6 April 1953 attained on 6 July 2016 (1995 Act Sch 4 table 1). STATE_PENSION_AGE = 66 -OLD_STATE_PENSION_AGE = {"MALE": 65 + YEAR - 2016, "FEMALE": 64 + YEAR - 2016} +OLD_STATE_PENSION_AGE = {"MALE": 76, "FEMALE": 74} class CarerOverlapSimulation(Simulation): @@ -291,10 +297,13 @@ def pinned(age, country, mode, pension, additional=0.0, gender="MALE"): pinned(80, "SCOTLAND", "components", 2_000, 5_000, "FEMALE"), pinned(68, "ENGLAND", "components", 2_000, 1_000, "FEMALE"), pinned(68, "SCOTLAND", "components", 2_000, 1_000), - # The same age belongs to different systems for men and women in - # the model's annual pension-age approximation. + # The same inferred birthday belongs to different systems for men + # and women under the statutory timetable. pinned(74, "ENGLAND", "components", 2_000, 5_000), pinned(74, "SCOTLAND", "components", 2_000, 5_000, "FEMALE"), + # Attainment on 6 April 2016 means new pension; before it means old. + pinned(75, "ENGLAND", "components", 2_000, 5_000), + pinned(76, "ENGLAND", "components", 2_000, 5_000), pinned(66, "WALES", "components", 2_000, 1_000), ] ) @@ -308,7 +317,7 @@ def test_overlap_bounds_and_reference(people): ref_ca_pre, ref_csp_pre, ref_overlap = reference(people, sim) # Components are assigned using age and gender, independently of the - # model's calculated type. Check the model's annual thresholds as well. + # model's calculated type. Check the default-birthday reference as well. pension_type = sim.calculate("state_pension_type", YEAR) if hasattr(pension_type, "decode_to_str"): pension_type = pension_type.decode_to_str() 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/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..d537262b94 --- /dev/null +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -0,0 +1,288 @@ +"""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, timedelta +from functools import lru_cache + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +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, + grid_months, + grid_months_to_date, + 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) +# 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}, + } + + +@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 + 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 + ) + + +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, 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) + 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 = 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] + ) + 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) + 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, + suppress_health_check=[HealthCheck.too_slow], +) +@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, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + births=st.lists( + 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, + ), + 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 5 December 1953, in 2015-16 to 2018-19.""" + assert_consumers_follow_the_qualifying_age(births, True, year) + + +@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)), + 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 = 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) + 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/dates.py b/policyengine_uk/utils/dates.py index 2320bd9f59..ec08d2b94e 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/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 7474b01981..da8034ccf0 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 @@ -37,7 +37,9 @@ def formula(benunit, period, parameters): members = person("is_claimant_or_partner", period) | person( "is_child_or_young_person_for_legacy_benefits", period ) - any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) + pension_age_regulations = benunit( + "housing_benefit_pension_age_regulations_apply", period + ) BENUNIT_MEANS_TESTED_BENEFITS = [ "child_benefit", "income_support", @@ -103,14 +105,22 @@ def formula(benunit, period, parameters): ) # SI 2006/214 reg 27 (NI: SR 2006/406 reg 25): where the award of # Pension Credit is savings credit only, the Secretary of State's - # assessment of income is used instead, plus the savings credit. - savings_credit_only = benunit("in_receipt_of_savings_credit_only", period) + # assessment of income is used instead, plus the savings credit. 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 + ) applicable_income = where( savings_credit_only, benunit("housing_benefit_savings_credit_only_income", period), income_under_general_rules, ) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + # 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 + ) # SI 2006/213 Sch 5 para 4 (NI: SR 2006/405 Sch 6 para 4) disregards # "the whole of his income" where a claimant is on income support, an # income-based jobseeker's allowance or an income-related employment 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 c15edabc14..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 @@ -8,24 +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_SP_age = benunit.any(benunit.members("is_SP_age", period)) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + # 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 = pension_age_regulations & ( + benunit("guarantee_credit", period) > 0 + ) p = parameters(period).gov.dwp.housing_benefit.means_test.capital threshold = where( - any_over_SP_age, + pension_age_regulations, p.pension_age.tariff_income.threshold, p.working_age.tariff_income.threshold, ) step = where( - any_over_SP_age, + pension_age_regulations, p.pension_age.tariff_income.step, p.working_age.tariff_income.step, ) amount = where( - any_over_SP_age, + pension_age_regulations, p.pension_age.tariff_income.amount, p.working_age.tariff_income.amount, ) @@ -36,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) 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 e5e84f8d97..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,35 +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_SP_age = benunit.any(benunit.members("is_SP_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_SP_age - o_18 = (eldest_age >= younger_age_threshold) * ~any_over_SP_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_SP_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_SP_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_SP_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 757f427c16..69a6c23fbe 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 @@ -34,7 +34,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)) + 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) claimant_or_partner = person("is_claimant_or_partner", period) @@ -60,14 +62,22 @@ def formula(benunit, period, parameters): # Pension Credit is savings credit only, the Secretary of State's # calculation of capital is used, and the £16,000 limit applies to it. # The recalculation when capital rises above £16,000 during an - # assessed income period (reg 27(7) and (8)) is not modelled. - savings_credit_only = benunit("in_receipt_of_savings_credit_only", period) + # assessed income period (reg 27(7) and (8)) is not modelled. 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 + ) capital = where( savings_credit_only, benunit("pension_credit_assessable_capital", period), household_capital_proxy + person_capital, ) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + # 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 + ) # SI 2006/213 Sch 6 para 5 (NI: SR 2006/405 Sch 7 para 5) disregards # "the whole of his capital" where a claimant is on income support, an # income-based jobseeker's allowance or an income-related employment 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 f3955f6b5a..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,7 +27,6 @@ class housing_benefit_eligible(Variable): def formula(benunit, period, parameters): person = benunit.members - sp_age = person("is_SP_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 @@ -51,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_SP_age = benunit.any(sp_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_SP_age, + pension_age_regulations, hb_capital.pension_age.limit, hb_capital.working_age.limit, ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py index 34b106fba2..d9bc07fec4 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py @@ -22,7 +22,7 @@ class housing_benefit_pension_age_regulations_apply(Variable): "working-age regulations. The model has no lawful such awards (tax " "credit migrants under SI 2014/1230 reg 60A are not modelled), and its " "Universal Credit eligibility test also needs a claimant or partner " - "under State Pension age, so the pension-age regulations are the right " + "under the qualifying age, so the pension-age regulations are the right " "result." ) definition_period = YEAR @@ -35,9 +35,11 @@ class housing_benefit_pension_age_regulations_apply(Variable): def formula(benunit, period, parameters): person = benunit.members claimant_or_partner = person("is_claimant_or_partner", period) - # is_SP_age stands in for the qualifying age for State Pension Credit, - # as elsewhere in the model. - over_qualifying_age = person("is_SP_age", period) + # HB (SPC) Regs 2006 reg 5(1): "has attained the qualifying age for + # state pension credit"; reg 5(2) excludes the benefits below. + over_qualifying_age = person( + "has_attained_state_pension_credit_qualifying_age", period + ) attained_qualifying_age = benunit.any(claimant_or_partner & over_qualifying_age) # Universal Credit needs a claimant or partner under the qualifying age # (WRA 2012 s.4(1)(b); UC Regs 2013 reg 3(2)(a)), except for tax credit 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/dwp/income_support_eligible.py b/policyengine_uk/variables/gov/dwp/income_support_eligible.py index c0f6715ffa..83bbc69f3d 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 @@ -16,17 +19,44 @@ def formula(benunit, period, parameters): benunit("youngest_child_age_for_legacy_benefits", period) <= IS.eligibility.lone_parent_youngest_child_age_limit ) + person = benunit.members + # 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 - has_carers = add(benunit, period, ["is_carer_for_benefits"]) > 0 - none_SP_age = ~benunit.any(benunit.members("is_SP_age", 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 + # 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 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) 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 + (carer_claimant | lone_parent_with_young_child) + & ~on_pension_credit & ~has_esa_income & already_claiming & (capital <= limit) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py index c27e13c39d..6746386523 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py @@ -9,10 +9,7 @@ class is_benefit_cap_exempt_earnings(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 person = benunit.members - over_pension_age = person("is_SP_age", period) - has_pensioner = benunit.any(over_pension_age) # UC-specific exemptions # Limited capability for work and work-related activity diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py index ee5bbbba9b..72c0d1a847 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py @@ -9,10 +9,7 @@ class is_benefit_cap_exempt_health_disability(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 person = benunit.members - over_pension_age = person("is_SP_age", period) - has_pensioner = benunit.any(over_pension_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 3a1110e892..6e7e36dfbd 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): @@ -16,11 +16,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/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/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..8d077a2379 --- /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; under the statutory timetable, 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/meets_pension_credit_age_conditions.py b/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py index 1df5294698..ec299bceea 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py @@ -26,8 +26,14 @@ def formula(benunit, period, parameters): person = benunit.members claimant_or_partner = person("is_claimant_or_partner", period) count = benunit.sum(claimant_or_partner) + # SPCA 2002 s.1(2)(b): "has attained the qualifying age"; + # s.4(1A) excludes a partner who "has not attained the qualifying age". all_pension_age = (count > 0) & ( - benunit.sum(claimant_or_partner & person("is_SP_age", period)) == count + benunit.sum( + claimant_or_partner + & person("has_attained_state_pension_credit_qualifying_age", period) + ) + == count ) # Parameters are read at 30 April of the model year # (convert_to_fiscal_year_parameters), so model year 2019 (2019-20) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py b/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py index 57b3d9b4e7..8b516b2b53 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/has_mixed_age_couple_pension_credit_saving.py @@ -48,7 +48,7 @@ def reported(variables): pension_age_member_hb = ( benunit.sum( person("housing_benefit_reported", period) - * person("is_SP_age", period) + * person("has_attained_state_pension_credit_qualifying_age", period) * claimant_or_partner ) > 0 @@ -69,7 +69,7 @@ def reported(variables): # then. older_member_qualified = benunit.any( claimant_or_partner - & person("is_SP_age", period) + & person("has_attained_state_pension_credit_qualifying_age", period) & (person("birth_year", period) <= p.saving_latest_birth_year) ) return ( diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/is_mixed_age_couple.py b/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/is_mixed_age_couple.py index 353036cb5f..d1a12ca25e 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/is_mixed_age_couple.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/mixed_age_couples/is_mixed_age_couple.py @@ -20,6 +20,7 @@ def formula(benunit, period, parameters): # The claimant and partner (SPCA 2002 s.17), so a pensioner with an # 18 or 19 year old dependant is not a couple here. claimant_or_partner = person("is_claimant_or_partner", period) - pension_age = person("is_SP_age", period) + # SPCA 2002 s.4(1A): the other member "has not attained the qualifying age". + pension_age = person("has_attained_state_pension_credit_qualifying_age", period) n_pension_age = benunit.sum(claimant_or_partner & pension_age) return (benunit.sum(claimant_or_partner) == 2) & (n_pension_age == 1) 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 e3a462a6c9..5803c56af2 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 @@ -34,10 +34,18 @@ def formula(benunit, period, parameters): benunit.sum(person(source, period) * claimant_or_partner) for source in p.person_sources ) - any_pension_age = benunit.any(person("is_SP_age", period)) - benunit_pension_age_adults = benunit.sum(person("is_SP_age", period)) + any_pension_age = benunit.any( + person("has_attained_state_pension_credit_qualifying_age", period) + ) + benunit_pension_age_adults = benunit.sum( + person("has_attained_state_pension_credit_qualifying_age", period) + ) 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/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/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..a4590c7e8a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/state_pension_credit_qualifying_age.py @@ -0,0 +1,44 @@ +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 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 + 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 214f762c85..dab00df84c 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 @@ -26,7 +26,11 @@ def formula(benunit, period, parameters): limit = parameters(period).gov.dwp.universal_credit.means_test.capital.limit claimant = benunit.members("is_uc_claimant", period) meets_minimum_age = benunit.members("meets_uc_minimum_age_condition", period) - pension_age = benunit.members("is_SP_age", period) + # WRA 2012 s.4(1)(b): "has not reached the qualifying age for state + # pension credit"; s.4(4) imports SPCA 2002 s.1(6). + pension_age = benunit.members( + "has_attained_state_pension_credit_qualifying_age", period + ) has_qualifying_claimant = benunit.any( claimant & meets_minimum_age & ~pension_age ) diff --git a/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py b/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py index f158779646..5792f3b07d 100644 --- a/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py +++ b/policyengine_uk/variables/gov/dwp/winter_fuel_allowance.py @@ -11,7 +11,18 @@ 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) + # 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 = ( add( @@ -27,12 +38,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 + age_eligible & ( taxable_income < wfp.eligibility.taxable_income_test.maximum_taxable_income @@ -45,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(is_SP_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_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): 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 d1d81de624..0657f27c97 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..bb61d2b274 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/national_insurance/class_4/ni_class_4_liable.py @@ -0,0 +1,45 @@ +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): + 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. 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 = ( + "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. 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". + # 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 + ) + 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 74b467363e..8ba64c53b3 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 8d64c045ab..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 @@ -8,42 +8,51 @@ 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_SP_age = benunit.any(benunit.members("is_SP_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_SP_age - o_18 = (eldest_age >= younger_age_threshold) & ~any_over_SP_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_SP_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_SP_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_SP_age * p.lone_parent.aged + + pensioner * p.lone_parent.aged ) personal_allowance = ( single * single_personal_allowance + 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/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 a9da7f9aa3..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 = ( @@ -28,9 +29,12 @@ def formula(benunit, period, parameters): person = benunit.members claimant_or_partner = person("is_claimant_or_partner", period) # Either member of a couple can claim, so the older one can claim as a - # pensioner. is_SP_age stands in for the qualifying age for State - # Pension Credit, as elsewhere in the model. - over_qualifying_age = person("is_SP_age", period) + # pensioner: "has attained the qualifying age for state pension credit" + # (SI 2012/2885 reg 3(1)(a)(i), WSI 2013/3029 reg 3(1)(a)(i), + # SSI 2012/319 reg 12(1)). The benefit conditions still apply. + over_qualifying_age = person( + "has_attained_state_pension_credit_qualifying_age", period + ) attained_qualifying_age = benunit.any(claimant_or_partner & over_qualifying_age) income_related_benefit = benunit( "council_tax_reduction_relevant_income_based_benefit", period 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), )