diff --git a/changelog.d/mixed-age-uc-benefit-cap-ctr.fixed.md b/changelog.d/mixed-age-uc-benefit-cap-ctr.fixed.md new file mode 100644 index 0000000000..0757e315c3 --- /dev/null +++ b/changelog.d/mixed-age-uc-benefit-cap-ctr.fixed.md @@ -0,0 +1 @@ +Stop exempting mixed-age couples on Universal Credit from the benefit cap (UC Regs 2013 regs 79, 82 and 83 have no age exception), and stop treating them as pensioners for Council Tax Reduction (SI 2012/2885 reg 3, WSI 2013/3029 reg 3, SSI 2021/249 reg 3). The cap's age exemption now follows which Housing Benefit regulations apply (HB Regs 2006 reg 5), through the new `housing_benefit_pension_age_regulations_apply`; the CTR pensioner test is the new `council_tax_reduction_pensioner`. Both read the claimant and partner (`is_claimant_or_partner`), and a Universal Credit award counts only where one of them is under State Pension age, so a pensioner whose only younger member is an 18- or 19-year-old dependant keeps pensioner treatment, with or without `is_parent`. CTR amounts change only in England, where such couples move to the local working-age scheme; in Wales and Scotland CTR changes only through the capped Universal Credit counted as income. Outside the five English councils the model simulates (Newham, Merton, Kingston upon Thames, Westminster and Oxford), the working-age scheme uses reported CTR, so a household calculation for a mixed-age couple on Universal Credit in the rest of England now shows CTR of £0 unless a reported amount is entered. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml index c88a124cb8..456c6dbf9a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml @@ -78,12 +78,16 @@ uc_income_reduction: 9_000 # 14,243.64 - 9,000. Before this fix the award was 14,243.64. universal_credit: 5_243.64 - # The pension-age CTR income is State Pension plus UC: - # 9,000 + 5,243.64 = 14,243.64, below the applicable amount of - # 383.35 x 52 = 19,934.20, so council tax is met in full. Before this fix - # the inflated UC gave an income of 23,243.64 and CTR of - # 1,500 - 20% x (23,243.64 - 19,934.20) = 838.11. - council_tax_benefit: 1_500 + # A couple with an award of Universal Credit is not a pensioner for + # Council Tax Reduction, though one member is over the qualifying age + # (SI 2012/2885 reg 3(1)(a)(ii)(bb)), so it claims under the council's + # working-age scheme, not the national pensioner scheme. The household is + # in England (region defaults to London) and in Maidstone (the default + # local authority), whose scheme the model does not simulate, so it keeps + # reported Council Tax Reduction: none here. The pensioner scheme would + # have met the 1,500 in full. + council_tax_reduction_pensioner: false + council_tax_benefit: 0 - name: Same couple with the same amount of private pension instead gets the same UC period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml new file mode 100644 index 0000000000..b0232bd974 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml @@ -0,0 +1,266 @@ +# The benefit cap and State Pension age. +# +# The Universal Credit cap (UC Regs 2013 regs 79, 82 and 83) turns on earnings +# and benefit receipt, not age, so it applies to a mixed-age couple's joint +# award (reg 3(2)(a)). The one age-based exception, SI 2014/1230 reg 60C, +# covers only claims where every claimant has reached the qualifying age. +# The Housing Benefit cap (HB Regs 2006 Part 8A) reaches only Housing Benefit +# under the working-age regulations, which by reg 5 cover a claimant or partner +# over the qualifying age for State Pension Credit only while either of them +# is on Universal Credit, Income Support, income-based JSA or income-related +# ESA. Housing Benefit under the pension-age regulations (SI 2006/214) has no +# cap. +# +# The Universal Credit and Housing Benefit amounts before the cap are inputs, +# so these cases test the cap alone. The couple cap in London is 25,323. + +- name: A mixed-age couple on Universal Credit is capped + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + child_1: + age: 4 + child_2: + age: 7 + benunits: + benunit: + members: [older, younger, child_1, child_2] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 3_000 + uc_deductions: 0 + households: + household: + members: [older, younger, child_1, child_2] + region: LONDON + output: + is_uc_entitled: true + housing_benefit_pension_age_regulations_apply: false + is_benefit_cap_exempt_other: false + is_benefit_cap_exempt: false + benefit_cap: 25_323 + # 30,000 + 3,000 - 25,323 + benefit_cap_reduction: 7_677 + universal_credit: 22_323 + +- name: The same couple is excepted when its earnings reach the reg 82 threshold + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + employment_income: 20_000 + child_1: + age: 4 + child_2: + age: 7 + benunits: + benunit: + members: [older, younger, child_1, child_2] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 3_000 + uc_deductions: 0 + households: + household: + members: [older, younger, child_1, child_2] + region: LONDON + output: + is_benefit_cap_exempt_other: false + is_benefit_cap_exempt_earnings: true + is_benefit_cap_exempt: true + benefit_cap_reduction: 0 + universal_credit: 30_000 + +- name: A mixed-age couple on Housing Benefit and no working-age benefit is not capped + # Neither member is on Universal Credit or a legacy income-related benefit, + # so the pension-age Housing Benefit regulations apply (HB Regs 2006 reg + # 5(3)); they have no benefit cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + would_claim_housing_benefit: true + housing_benefit_pre_benefit_cap: 30_000 + households: + household: + members: [older, younger] + region: LONDON + output: + is_uc_entitled: false + housing_benefit_pension_age_regulations_apply: true + is_benefit_cap_exempt_other: true + benefit_cap_reduction: 0 + housing_benefit: 30_000 + +- name: A mixed-age couple on income-based JSA has working-age Housing Benefit, which is capped + # HB Regs 2006 reg 5(1)(b): the working-age regulations, and so Part 8A, + # apply to a claimant over the qualifying age whose partner is on + # income-based JSA. The model stops paying income-based JSA from April 2026 + # (gov.dwp.JSA.income.active), so this case is in 2025-26. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + jsa_income_reported: 4_000 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + would_claim_housing_benefit: true + housing_benefit_pre_benefit_cap: 30_000 + households: + household: + members: [older, younger] + region: LONDON + output: + jsa_income: 4_000 + housing_benefit_pension_age_regulations_apply: false + is_benefit_cap_exempt_other: false + is_benefit_cap_exempt: false + benefit_cap: 25_323 + # 30,000 + 4,000 - 25,323 + benefit_cap_reduction: 8_677 + housing_benefit: 21_323 + +- name: A couple over State Pension age is not capped + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 72 + younger: + age: 68 + benunits: + benunit: + members: [older, younger] + would_claim_housing_benefit: true + housing_benefit_pre_benefit_cap: 30_000 + households: + household: + members: [older, younger] + region: LONDON + output: + is_uc_entitled: false + housing_benefit_pension_age_regulations_apply: true + is_benefit_cap_exempt_other: true + benefit_cap_reduction: 0 + housing_benefit: 30_000 + +- name: A working-age couple on Universal Credit is capped, as before + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 45 + younger: + age: 40 + child_1: + age: 4 + child_2: + age: 7 + benunits: + benunit: + members: [older, younger, child_1, child_2] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 3_000 + uc_deductions: 0 + households: + household: + members: [older, younger, child_1, child_2] + region: LONDON + output: + housing_benefit_pension_age_regulations_apply: false + is_benefit_cap_exempt: false + benefit_cap_reduction: 7_677 + universal_credit: 22_323 + +- name: A pensioner whose only younger adult is a qualifying young person is not capped + # Universal Credit needs a claimant or partner under the qualifying age (UC + # Regs 2013 reg 3(2)(a)), and the model's is_uc_eligible needs one too, so + # the award here is an input the family could not hold in law. The + # 18-year-old is a dependant, not a partner, so the award does not take the + # family out of the pension-age Housing Benefit rules, which have no cap. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 70 + is_parent: true + young_person: + age: 18 + current_education: UPPER_SECONDARY + child: + age: 9 + benunits: + benunit: + members: [parent, young_person, child] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 3_000 + uc_deductions: 0 + households: + household: + members: [parent, young_person, child] + region: LONDON + output: + is_claimant_or_partner: [true, false, false] + is_uc_entitled: true + housing_benefit_pension_age_regulations_apply: true + is_benefit_cap_exempt_other: true + benefit_cap_reduction: 0 + universal_credit: 30_000 + +- name: Without is_parent the young person is still a dependant, so the award is not capped + # is_claimant_or_partner presumes a member under 20 and at least 16 years + # younger than the claimant to be their child when relationships are not + # given, so the 18-year-old is not a partner under State Pension age and + # the result matches the case above. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 70 + young_person: + age: 18 + current_education: UPPER_SECONDARY + child: + age: 9 + benunits: + benunit: + members: [parent, young_person, child] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 3_000 + uc_deductions: 0 + households: + household: + members: [parent, young_person, child] + region: LONDON + output: + is_claimant_or_partner: [true, false, false] + housing_benefit_pension_age_regulations_apply: true + is_benefit_cap_exempt_other: true + is_benefit_cap_exempt: true + benefit_cap_reduction: 0 + universal_credit: 30_000 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 new file mode 100644 index 0000000000..444244a941 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml @@ -0,0 +1,343 @@ +# Who is a pensioner for Council Tax Reduction. +# +# England (SI 2012/2885 reg 3(1)) and Wales (SI 2013/3029 reg 3(1)): a +# pensioner has attained the qualifying age for State Pension Credit and is +# not, nor is any partner, on Income Support, income-based JSA or +# income-related ESA, or a person with an award of Universal Credit. Scotland +# applies the same test through SSI 2021/249 reg 3 and SSI 2012/319 reg 12. A +# mixed-age couple on Universal Credit therefore claims under the working-age +# scheme. +# +# In England the pensioner scheme is national and working-age schemes are +# local; the model simulates five local schemes and otherwise keeps reported +# Council Tax Reduction. + +- name: An English mixed-age couple on Universal Credit uses the local working-age scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + council_tax_benefit_reported: 700 + younger: + age: 50 + benunits: + benunit: + members: [older, younger] + universal_credit_pre_benefit_cap: 10_000 + households: + household: + members: [older, younger] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: false + council_tax_reduction_household_has_pensioner: false + council_tax_reduction_scheme_supported: false + simulated_council_tax_reduction_benunit: 0 + council_tax_benefit: 700 + +- name: An English mixed-age couple not on Universal Credit uses the pensioner scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + council_tax_benefit_reported: 700 + younger: + age: 50 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + households: + household: + members: [older, younger] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + is_uc_entitled: false + council_tax_reduction_pensioner: true + council_tax_reduction_household_has_pensioner: true + council_tax_reduction_scheme_supported: true + # No income, so the full liability. + council_tax_benefit: 2_000 + +- name: An English mixed-age couple on income-based JSA is not a pensioner couple + # The model stops paying income-based JSA from April 2026 + # (gov.dwp.JSA.income.active), so this case is in 2025-26. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + council_tax_benefit_reported: 700 + younger: + age: 50 + jsa_income_reported: 4_000 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + households: + household: + members: [older, younger] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + jsa_income: 4_000 + council_tax_reduction_pensioner: false + council_tax_reduction_household_has_pensioner: false + council_tax_benefit: 700 + +- name: An English couple over State Pension age is a pensioner couple + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 72 + younger: + age: 68 + benunits: + benunit: + members: [older, younger] + households: + household: + members: [older, younger] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: true + council_tax_reduction_household_has_pensioner: true + council_tax_reduction_scheme_supported: true + +- name: A Newham mixed-age couple on Universal Credit gets Newham's working-age scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + benunits: + benunit: + members: [older, younger] + universal_credit_pre_benefit_cap: 10_000 + households: + household: + members: [older, younger] + country: ENGLAND + local_authority: NEWHAM + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_household_has_pensioner: false + council_tax_reduction_scheme_supported: true + # Newham's scheme on the Universal Credit route. The pensioner scheme + # would give the full 2,000 (income 10,000 is below the pension-age + # applicable amount). + newham_council_tax_reduction: 900.91 + council_tax_benefit: 900.91 + +- name: Newham's working-age non-dependant deduction applies to a mixed-age couple on UC + # The couple (the claimant benefit unit, with the oldest member) lives with + # a working son in his own benefit unit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + son: + age: 30 + employment_income: 30_000 + benunits: + couple: + members: [older, younger] + universal_credit_pre_benefit_cap: 10_000 + son: + members: [son] + households: + household: + members: [older, younger, son] + country: ENGLAND + local_authority: NEWHAM + council_tax: 2_000 + savings: 0 + output: + benunit_contains_household_head: [true, false] + council_tax_reduction_pensioner: [false, false] + council_tax_reduction_household_has_pensioner: false + newham_council_tax_reduction_non_dep_deductions: [392.08, 0] + council_tax_benefit: [508.83, 0] + +- name: A pensioner whose only younger adult is a qualifying young person stays a pensioner + # Universal Credit before the cap is an input here. Universal Credit needs a + # claimant or partner under the qualifying age, and the 18-year-old is a + # dependant, so the award does not count against pensioner status. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + young_person: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent, young_person] + universal_credit_pre_benefit_cap: 10_000 + households: + household: + members: [parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + is_uc_entitled: true + council_tax_reduction_pensioner: true + council_tax_reduction_household_has_pensioner: true + council_tax_reduction_scheme_supported: true + +- name: A pensioner aged 70 with an 18-year-old in education and no is_parent keeps pensioner CTR + # is_claimant_or_partner presumes the 18-year-old to be the pensioner's + # child (under 20 and at least 16 years younger), so the family has no + # claimant or partner under State Pension age. The model gives it no + # Universal Credit, it is not capped, and it stays on the national + # pensioner scheme. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + young_person: + age: 18 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [pensioner, young_person] + households: + household: + members: [pensioner, young_person] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + is_claimant_or_partner: [true, false] + is_uc_eligible: false + is_uc_entitled: false + housing_benefit_pension_age_regulations_apply: true + is_benefit_cap_exempt: true + benefit_cap_reduction: 0 + council_tax_reduction_pensioner: true + council_tax_reduction_household_has_pensioner: true + council_tax_reduction_scheme_supported: true + # The pensioner scheme meets the 2,000 in full. + council_tax_benefit: 2_000 + +- name: A Welsh mixed-age couple on Universal Credit is not a pensioner couple + # Wales has one national scheme, so the scheme is still simulated. + period: 2026 + input: + people: + older: + age: 70 + younger: + age: 50 + benunits: + benunit: + members: [older, younger] + universal_credit_pre_benefit_cap: 10_000 + households: + household: + members: [older, younger] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: false + council_tax_reduction_household_has_pensioner: false + council_tax_reduction_scheme_supported: true + +- name: A Scottish mixed-age couple on Universal Credit is not a pensioner couple + period: 2026 + input: + people: + older: + age: 70 + younger: + age: 50 + benunits: + benunit: + members: [older, younger] + universal_credit_pre_benefit_cap: 10_000 + households: + household: + members: [older, younger] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: false + 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. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + younger: + age: 50 + child: + age: 9 + benunits: + benunit: + members: [older, younger, child] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 3_000 + uc_deductions: 0 + households: + household: + members: [older, younger, child] + country: WALES + local_authority: CARDIFF + region: WALES + council_tax: 2_000 + savings: 0 + output: + benefit_cap_reduction: 10_980 + universal_credit: 19_020 + council_tax_reduction_applicable_income: 22_020 + council_tax_benefit: 1_582.84 diff --git a/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py new file mode 100644 index 0000000000..ea0ece7cc3 --- /dev/null +++ b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py @@ -0,0 +1,352 @@ +"""Property tests: the benefit cap and Council Tax Reduction for families with a +member over State Pension age. + +The law: + +- The Universal Credit benefit cap turns on earnings and benefit receipt, not + age (UC Regs 2013 regs 79, 82 and 83). A mixed-age couple can claim + Universal Credit jointly (reg 3(2)(a)), and the cap applies to that award. + The one age-based exception, SI 2014/1230 reg 60C, covers only claims where + every claimant has reached the qualifying age for State Pension Credit. +- The Housing Benefit cap (HB Regs 2006 Part 8A) reaches only Housing Benefit + under the working-age regulations. By reg 5 those cover a claimant or + partner over the qualifying age for State Pension Credit only while either + of them is on Universal Credit, Income Support, income-based JSA or + income-related ESA. +- For Council Tax Reduction a "pensioner" has reached the qualifying age and + neither they nor any partner is on those legacy benefits or has an award of + Universal Credit (England SI 2012/2885 reg 3(1); Wales SI 2013/3029 reg + 3(1); Scotland SSI 2021/249 reg 3 and SSI 2012/319 reg 12). + +Universal Credit needs a claimant or partner under the qualifying age (UC +Regs 2013 reg 3(2)(a)). Claimant and partner are is_claimant_or_partner, which +presumes a member under 20 and at least 16 years younger than the claimant to +be their child, so a pensioner's qualifying young person is a dependant +whether or not is_parent is set; the draws cover both. "On Universal Credit" +below means an award before the cap with a claimant or partner under State +Pension age. Legacy income-related benefit is set as an input. + +Invariants, for every drawn population of families: + +P1 Both new tests hold exactly when a claimant or partner is over State + Pension age and the family is neither on Universal Credit nor on a legacy + income-related benefit. +P2 Every Universal Credit award is capped unless a non-age exception applies: + such a family has a finite benefit cap. +P3 Differential against the formulas before this change. A family with no + member over State Pension age, or one over it with neither Universal + Credit nor a legacy income-related benefit, gets exactly the same benefit + cap, Universal Credit, Housing Benefit, Council Tax Reduction and household + net income. +P4 The change only removes exemptions: exempt now implies exempt before, the + cap is never higher, and Universal Credit and Housing Benefit never rise. + A CTR pensioner now was a CTR pensioner before. +P5 The model's Welsh and Scottish CTR formulas do not route on pensioner + status, but they count Universal Credit as income. So their CTR changes + only through Universal Credit: unchanged where Universal Credit is + unchanged, and never lower where the cap has lowered Universal Credit. +P6 Metamorphic: with a Universal Credit award, adding a member over State + Pension age to a couple's ages (making it mixed-age) never exempts it from + the cap. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.reforms import Reform + +from policyengine_uk import Simulation +from policyengine_uk.model_api import * + +PERIOD = 2026 +PROPERTY_SETTINGS = settings( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +PENSION_AGE = st.integers(67, 95) +WORKING_AGE = st.integers(25, 60) +# Qualifying young people are 18: at 19 the UC definition also needs the +# course to have started before 19, which is a separate input. +YOUNG_PERSON = st.just(18) +SHAPES = { + "single_pension": [("adult", PENSION_AGE)], + "couple_pension": [("adult", PENSION_AGE), ("adult", PENSION_AGE)], + "mixed_age": [("adult", PENSION_AGE), ("adult", WORKING_AGE)], + "single_working": [("adult", WORKING_AGE)], + "couple_working": [("adult", WORKING_AGE), ("adult", WORKING_AGE)], + "pension_with_young_person": [("adult", PENSION_AGE), ("young", YOUNG_PERSON)], +} +# Weighted towards the families this change is about. +SHAPE_DRAWS = sorted(SHAPES) + ["mixed_age", "mixed_age", "pension_with_young_person"] +PLACES = [ + ("ENGLAND", "MAIDSTONE", "SOUTH_EAST"), + ("ENGLAND", "NEWHAM", "LONDON"), + ("ENGLAND", "WESTMINSTER", "LONDON"), + ("ENGLAND", "OXFORD", "SOUTH_EAST"), + ("WALES", "CARDIFF", "WALES"), + ("SCOTLAND", "GLASGOW_CITY", "SCOTLAND"), +] +TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] + + +# The two formulas as they were before this change, kept as the reference for +# the differential invariants. +class is_benefit_cap_exempt_other(Variable): + value_type = bool + entity = BenUnit + label = "Benefit cap exemption before this change" + definition_period = YEAR + + def formula(benunit, period, parameters): + has_pensioner = benunit.any(benunit.members("is_SP_age", period)) + afcs = benunit("afcs", period) > 0 + esa_support_component = benunit("esa_contrib", period) > 0 + return has_pensioner | afcs | esa_support_component + + +class council_tax_reduction_household_has_pensioner(Variable): + value_type = bool + entity = Household + label = "CTR pensioner test before this change" + definition_period = YEAR + + def formula(household, period, parameters): + person = household.members + claimant_benunit = person.benunit("benunit_contains_household_head", period) + return household.any(claimant_benunit & person("is_SP_age", period)) + + +class before_this_change(Reform): + def apply(self): + self.update_variable(is_benefit_cap_exempt_other) + self.update_variable(council_tax_reduction_household_has_pensioner) + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(SHAPE_DRAWS)) + country, local_authority, region = draw(st.sampled_from(PLACES)) + tenure = draw(st.sampled_from(TENURES)) + return dict( + shape=shape, + members=[(role, draw(age)) for role, age in SHAPES[shape]], + children=draw(st.integers(0, 4)), + country=country, + local_authority=local_authority, + region=region, + tenure=tenure, + rent=0 if tenure == "OWNED_OUTRIGHT" else draw(st.integers(0, 30_000)), + earnings=draw(st.sampled_from([0, 0, 5_000, 25_000])), + state_pension=draw(st.sampled_from([0, 9_000, 12_000])), + would_claim_uc=draw(st.sampled_from([True, True, False])), + hb_reported=draw(st.sampled_from([0, 0, 5_000])), + ctb_reported=draw(st.sampled_from([0, 0, 700])), + legacy=draw(st.sampled_from([0, 0, 0, 3_000])), + # Whether is_parent identifies the adult in a family with a young + # person, as the FRS datasets do; household inputs may leave it out. + parent_flag=draw(st.booleans()), + ) + + +def situation(units): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + has_young_person = any(role == "young" for role, _ in unit["members"]) + for j, (role, age) in enumerate(unit["members"]): + name = f"p{i}_{j}" + person = {"age": {PERIOD: age}} + if role == "young": + person["current_education"] = {PERIOD: "UPPER_SECONDARY"} + elif age >= 67: + person["state_pension_reported"] = {PERIOD: unit["state_pension"]} + else: + person["employment_income"] = {PERIOD: unit["earnings"]} + if role == "adult" and has_young_person and unit["parent_flag"]: + person["is_parent"] = {PERIOD: True} + if j == 0: + person["housing_benefit_reported"] = {PERIOD: unit["hb_reported"]} + person["council_tax_benefit_reported"] = {PERIOD: unit["ctb_reported"]} + people[name] = person + names.append(name) + for k in range(unit["children"]): + name = f"p{i}_c{k}" + people[name] = {"age": {PERIOD: 2 + 3 * k}} + names.append(name) + benunit = { + "members": names, + "would_claim_uc": {PERIOD: unit["would_claim_uc"]}, + # claims_all_entitled_benefits sums reported benefits across the + # whole simulation, so it is set per family. + "claims_all_entitled_benefits": { + PERIOD: unit["hb_reported"] == 0 and unit["ctb_reported"] == 0 + }, + } + if unit["legacy"]: + benunit["jsa_income"] = {PERIOD: unit["legacy"]} + benunits[f"b{i}"] = benunit + households[f"h{i}"] = { + "members": names, + "country": {PERIOD: unit["country"]}, + "local_authority": {PERIOD: unit["local_authority"]}, + "region": {PERIOD: unit["region"]}, + "tenure_type": {PERIOD: unit["tenure"]}, + "rent": {PERIOD: unit["rent"]}, + "council_tax": {PERIOD: 1_800}, + "savings": {PERIOD: 0}, + } + return {"people": people, "benunits": benunits, "households": households} + + +BENUNIT = [ + "is_uc_entitled", + "housing_benefit_pension_age_regulations_apply", + "council_tax_reduction_pensioner", + "is_benefit_cap_exempt", + "is_benefit_cap_exempt_health_disability", + "is_benefit_cap_exempt_earnings", + "is_benefit_cap_exempt_other", + "benefit_cap", + "universal_credit", + "housing_benefit", + "council_tax_benefit", + "afcs", + "esa_contrib", +] +HOUSEHOLD = ["council_tax_reduction_household_has_pensioner", "household_net_income"] +BEFORE = [ + "is_benefit_cap_exempt", + "benefit_cap", + "universal_credit", + "housing_benefit", + "council_tax_benefit", + "council_tax_reduction_household_has_pensioner", + "household_net_income", +] + + +def calculate(units, before=False): + simulation = Simulation(situation=situation(units)) + if before: + simulation.apply_reform(before_this_change) + names = BEFORE + else: + names = BENUNIT + HOUSEHOLD + return {name: np.asarray(simulation.calculate(name, PERIOD)) for name in names} + + +def has_pension_age_claimant(unit): + return any(role == "adult" and age >= 67 for role, age in unit["members"]) + + +def has_working_age_claimant(unit): + return any(role == "adult" and age < 67 for role, age in unit["members"]) + + +def close(a, b): + return (a == b) or abs(a - b) < 0.01 + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=10, max_size=30)) +def test_benefit_cap_and_ctr_pension_age_invariants(units): + now = calculate(units) + before = calculate(units, before=True) + for i, unit in enumerate(units): + on_uc = bool(now["is_uc_entitled"][i]) and has_working_age_claimant(unit) + legacy = unit["legacy"] > 0 + # P1 + expected = has_pension_age_claimant(unit) and not (on_uc or legacy) + assert now["housing_benefit_pension_age_regulations_apply"][i] == expected + assert now["council_tax_reduction_pensioner"][i] == expected, unit + # P2 + other_exception = ( + now["is_benefit_cap_exempt_health_disability"][i] + or now["is_benefit_cap_exempt_earnings"][i] + or now["afcs"][i] > 0 + or now["esa_contrib"][i] > 0 + ) + if on_uc and not other_exception: + assert not now["is_benefit_cap_exempt"][i], unit + assert np.isfinite(now["benefit_cap"][i]), unit + # P3 + if not has_pension_age_claimant(unit) or not (on_uc or legacy): + for name in BEFORE: + assert close(now[name][i], before[name][i]), (name, unit) + # P4 + if now["is_benefit_cap_exempt"][i]: + assert before["is_benefit_cap_exempt"][i], unit + assert now["benefit_cap"][i] <= before["benefit_cap"][i], unit + assert now["universal_credit"][i] <= before["universal_credit"][i] + 0.01 + assert now["housing_benefit"][i] <= before["housing_benefit"][i] + 0.01 + if now["council_tax_reduction_household_has_pensioner"][i]: + assert before["council_tax_reduction_household_has_pensioner"][i], unit + # P5 + if unit["country"] in ("WALES", "SCOTLAND"): + ctr_now, ctr_before = ( + now["council_tax_benefit"][i], + before["council_tax_benefit"][i], + ) + if close(now["universal_credit"][i], before["universal_credit"][i]): + assert close(ctr_now, ctr_before), unit + else: + assert ctr_now >= ctr_before - 0.01, unit + + +@PROPERTY_SETTINGS +@given( + st.lists( + st.fixed_dictionaries( + dict( + working_age=WORKING_AGE, + pension_age=PENSION_AGE, + children=st.integers(0, 4), + uc=st.integers(1, 40_000), + child_benefit=st.integers(0, 5_000), + london=st.booleans(), + ) + ), + min_size=1, + max_size=20, + ) +) +def test_a_pension_age_partner_never_exempts_a_uc_award(couples): + # P6: the same couple on the same Universal Credit award, first with two + # working-age members, then with the older member over State Pension age. + def run(older_is_pension_age): + people, benunits, households = {}, {}, {} + for i, c in enumerate(couples): + older = c["pension_age"] if older_is_pension_age else c["working_age"] + names = [f"p{i}_a", f"p{i}_b"] + [ + f"p{i}_c{k}" for k in range(c["children"]) + ] + people[names[0]] = {"age": {PERIOD: older}} + people[names[1]] = {"age": {PERIOD: c["working_age"]}} + for k, name in enumerate(names[2:]): + people[name] = {"age": {PERIOD: 2 + 3 * k}} + benunits[f"b{i}"] = { + "members": names, + "universal_credit_pre_benefit_cap": {PERIOD: c["uc"]}, + "child_benefit": {PERIOD: c["child_benefit"]}, + "uc_deductions": {PERIOD: 0}, + } + households[f"h{i}"] = { + "members": names, + "region": {PERIOD: "LONDON" if c["london"] else "NORTH_WEST"}, + } + simulation = Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + return { + name: np.asarray(simulation.calculate(name, PERIOD)) + for name in ["is_benefit_cap_exempt", "benefit_cap_reduction"] + } + + working, mixed = run(False), run(True) + for i, c in enumerate(couples): + assert mixed["is_benefit_cap_exempt"][i] == working["is_benefit_cap_exempt"][i] + assert close( + mixed["benefit_cap_reduction"][i], working["benefit_cap_reduction"][i] + ), c 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 new file mode 100644 index 0000000000..34b106fba2 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py @@ -0,0 +1,60 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_pension_age_regulations_apply(Variable): + value_type = bool + entity = BenUnit + label = "Housing Benefit falls under the pension-age regulations" + documentation = ( + "Whether a Housing Benefit claim by this family falls under the Housing " + "Benefit (Persons who have attained the qualifying age for state " + "pension credit) Regulations 2006 rather than the Housing Benefit " + "Regulations 2006. That is the case where the claimant or partner has " + "reached the qualifying age for State Pension Credit and neither of " + "them is on Universal Credit, Income Support, income-based Jobseeker's " + "Allowance or income-related Employment and Support Allowance. The " + "benefit cap (Part 8A of the working-age regulations) does not reach " + "Housing Benefit under the pension-age regulations. A Universal Credit " + "award counts only where a claimant or partner is under the qualifying " + "age. That is right for the cap, which SI 2014/1230 reg 60C disapplies " + "for a claim where every claimant has reached that age, but not for " + "reg 5 itself, under which such an award would bring in the " + "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 " + "result." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/5", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/5", + ) + + 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) + 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 + # migrants (SI 2014/1230 reg 60A), whose award is exempt from the cap + # (reg 60C). An award counts here only with such a claimant or partner. + # A dependant is neither: is_claimant_or_partner presumes a member under + # 20 and at least 16 years younger than the claimant to be their child, + # so a pensioner's 18- or 19-year-old does not make the award a + # working-age one, whether or not is_parent identifies the pensioner as + # the parent. The award is read before the benefit cap, which depends + # on this variable. + working_age_claimant = benunit.any(claimant_or_partner & ~over_qualifying_age) + on_universal_credit = benunit("is_uc_entitled", period) & working_age_claimant + # Reg 5 names the same three benefits as the CTR pensioner test. + on_income_related_benefit = benunit( + "council_tax_reduction_relevant_income_based_benefit", period + ) + return ( + attained_qualifying_age & ~on_universal_credit & ~on_income_related_benefit + ) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py index 0fb1547506..df02432607 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py @@ -6,50 +6,28 @@ class is_benefit_cap_exempt_other(Variable): entity = BenUnit label = "Whether exempt from the benefits cap for non-health/disability reasons" definition_period = YEAR - reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" + reference = ( + "https://www.gov.uk/benefit-cap/when-youre-not-affected", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/83", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60C", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75A", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", + ) 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 - has_lcwra = benunit.any(person("uc_limited_capability_for_WRA", period)) - - # Carer element in UC indicates caring for someone with disability - gets_uc_carer_element = benunit("uc_carer_element", period) > 0 - - # Earnings exemption for UC (£846/month = £10,152/year) - # Note: Only check earned income, not UC amount itself to avoid circular dependency - uc_earned = benunit.sum( - benunit.members("employment_income", period) - + benunit.members("self_employment_income", period) - - benunit.members("income_tax", period) - - benunit.members("national_insurance", period) + # The benefit cap applies to a Universal Credit award whatever the + # claimants' ages (UC Regs 2013 regs 79, 82 and 83), including a + # mixed-age couple's joint award (reg 3(2)(a)). The one age-based + # exception, SI 2014/1230 reg 60C (NI: SR 2016/226 reg 61C), covers + # only claims where every claimant has reached the qualifying age for + # State Pension Credit. The cap also applies to Housing Benefit under + # the working-age regulations (HB Regs 2006 Part 8A). Housing Benefit + # under the pension-age regulations, which never apply to a family on + # Universal Credit, has no cap. + pension_age_housing_benefit = benunit( + "housing_benefit_pension_age_regulations_apply", period ) - earnings_threshold = 10_152 - meets_earnings_test = uc_earned >= earnings_threshold - - # Disability and carer benefits that exempt from cap - QUAL_PERSONAL_BENEFITS = [ - "attendance_allowance", - "carers_allowance", - "dla", # Disability Living Allowance (includes components) - "pip_dl", # PIP daily living component - "pip_m", # PIP mobility component - "iidb", # Industrial injuries disability benefit - ] - - # ESA and Working Tax Credit - QUAL_BENUNIT_BENEFITS = [ - "esa_income", # Income-based ESA - "working_tax_credit", # If getting WTC, likely working enough - ] - - qualifying_personal_benefits = add(benunit, period, QUAL_PERSONAL_BENEFITS) - qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) # Check for Armed Forces Compensation Scheme payments afcs = benunit("afcs", period) > 0 @@ -57,4 +35,4 @@ def formula(benunit, period, parameters): # ESA contribution-based with support component esa_support_component = benunit("esa_contrib", period) > 0 - return has_pensioner | afcs | esa_support_component + return pension_age_housing_benefit | afcs | esa_support_component diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py index 555231a5ab..26931bda3c 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_household_has_pensioner.py @@ -4,10 +4,12 @@ class council_tax_reduction_household_has_pensioner(Variable): value_type = bool entity = Household - label = "CTR claimant benefit unit has a pension-age member" + label = "CTR claimant benefit unit is a pensioner" definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2012/2885/regulation/3" def formula(household, period, parameters): person = household.members claimant_benunit = person.benunit("benunit_contains_household_head", period) - return household.any(claimant_benunit & person("is_SP_age", period)) + pensioner = person.benunit("council_tax_reduction_pensioner", period) + return household.any(claimant_benunit & pensioner) 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 new file mode 100644 index 0000000000..a9da7f9aa3 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py @@ -0,0 +1,58 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_pensioner(Variable): + value_type = bool + entity = BenUnit + label = "Pensioner for Council Tax Reduction" + documentation = ( + "Whether the family's claim falls under the pension-age Council Tax " + "Reduction rules: the claimant or partner has reached the qualifying " + "age for State Pension Credit, and neither of them is on " + "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." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/3", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/3", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/3", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/12", + "https://www.legislation.gov.uk/ssi/2012/303/regulation/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) + attained_qualifying_age = benunit.any(claimant_or_partner & over_qualifying_age) + income_related_benefit = benunit( + "council_tax_reduction_relevant_income_based_benefit", period + ) + # The award before the benefit cap: the cap reduces an award + # (UC Regs 2013 reg 81) rather than removing it. An award counts only + # where a claimant or partner is under the qualifying age, which + # Universal Credit needs (UC Regs 2013 reg 3(2)(a)). A dependant is + # neither: is_claimant_or_partner presumes a member under 20 and at + # least 16 years younger than the claimant to be their child, so a + # pensioner's 18- or 19-year-old does not count, whether or not + # is_parent identifies the pensioner as the parent. + # This also stands in for the rules that disregard an award held after + # both members reach the qualifying age (SI 2012/2885 reg 3(2); + # WSI 2013/3029 reg 3(2); SSI 2021/249 reg 3(2)). England, and Wales + # for schemes from 2026-27, also disregard a tax credit migrant's award + # (UC (TP) Regs 2014 reg 60A) and Scotland does not; the model does not + # model those migrants. + working_age_claimant = benunit.any(claimant_or_partner & ~over_qualifying_age) + universal_credit_award = ( + benunit("is_uc_entitled", period) & working_age_claimant + ) + return ( + attained_qualifying_age & ~income_related_benefit & ~universal_credit_award + )