From bd9a5a76a74ad7fcadb0a313b2214f15d77fec1c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 13:28:51 -0400 Subject: [PATCH 01/11] Give Universal Credit and Housing Benefit their own benefit cap and LHA shared-accommodation rules One benefit cap (rate, exceptions, reduction) and one LHA category served both schemes although the law differs. Each scheme now has its own: - UC: uc_benefit_cap (reg 80A), is_uc_benefit_cap_single_claimant_rate (a reg 3(3) single claim is single), is_uc_benefit_cap_exempt_earnings (reg 82), is_uc_benefit_cap_exempt_specified_benefit (reg 83, "a claimant"), uc_benefit_cap_reduction (reg 81, less the childcare costs element). LHA_category / is_lha_shared_accommodation_rate_specified_renter are UC's (Sch 4 paras 27-29). - HB: housing_benefit_benefit_cap (reg 75CA), single claimant per reg 2(1), exceptions 75E (WTC) and 75F (claimant or partner), no cap under the pension-age regulations (reg 5), housing_benefit_benefit_cap_reduction (75D, keeping the reg 75 minimum). housing_benefit_LHA_category, is_housing_benefit_young_individual and housing_benefit_LHA_rate follow reg 13D(2)(a)(i), with names and helpers shared with #2006. - universal_credit and housing_benefit read their own reduction; benefit_cap_reduction is their sum. The claims_legacy_benefits stopgap from #2084 is gone. Co-Authored-By: Claude Opus 5.5 --- .../shared_accommodation_age_threshold.yaml | 6 +- ...ared_accommodation_exception_benefits.yaml | 7 +- .../minimum_weekly_amount.yaml | 12 ++ .../qualifying_benefits.yaml | 16 ++ .../family/LHA_claimant_composition.yaml | 29 ++- .../housing_benefit/housing_benefit.yaml | 4 +- .../claimant_child_element.yaml | 6 +- .../claimant_relationships.yaml | 12 +- .../claims_as_single_person.yaml | 70 +++++-- .../universal_credit/uc_deductions.yaml | 12 +- .../policy/baseline/gov/dwp/benefit_cap.yaml | 176 ++++++++++++------ .../test_abolish_benefit_cap_scenario.py | 7 +- policyengine_uk/tests/test_uc_deductions.py | 4 +- .../tests/test_uc_single_claim_properties.py | 86 ++++++--- policyengine_uk/utils/benefit_cap.py | 31 +++ policyengine_uk/utils/benefit_unit.py | 20 ++ .../utils/uc_work_related_requirements.py | 20 -- policyengine_uk/variables/gov/dwp/LHA_cap.py | 9 +- .../variables/gov/dwp/LHA_category.py | 8 +- .../variables/gov/dwp/benefit_cap.py | 33 ---- .../gov/dwp/benefit_cap_reduction.py | 27 +-- .../gov/dwp/benefit_cap_welfare_benefits.py | 41 ++++ .../housing_benefit_benefit_cap.py | 33 ++++ .../housing_benefit_benefit_cap_reduction.py | 33 ++++ .../is_housing_benefit_benefit_cap_exempt.py | 30 +++ ...it_benefit_cap_exempt_specified_benefit.py | 54 ++++++ ...t_benefit_cap_exempt_working_tax_credit.py | 18 ++ ...enefit_benefit_cap_single_claimant_rate.py | 30 +++ .../dwp/housing_benefit/housing_benefit.py | 2 +- ...g_benefit_pension_age_regulations_apply.py | 47 +++++ ...nefit_severe_disability_premium_applies.py | 48 +++++ .../gov/dwp/housing_benefit_LHA_category.py | 62 ++++++ .../gov/dwp/housing_benefit_LHA_rate.py | 28 +++ .../gov/dwp/is_benefit_cap_exempt.py | 15 -- .../gov/dwp/is_benefit_cap_exempt_earnings.py | 60 ------ ...is_benefit_cap_exempt_health_disability.py | 85 --------- .../gov/dwp/is_benefit_cap_exempt_other.py | 71 ------- .../is_benefit_cap_single_claimant_rate.py | 41 ---- .../is_housing_benefit_young_individual.py | 23 +++ ...red_accommodation_rate_specified_renter.py | 47 +++-- ..._young_person_for_uc_or_housing_benefit.py | 17 +- .../gov/dwp/uncapped_BRMA_LHA_rate.py | 125 +++++++------ .../benefit_cap/is_uc_benefit_cap_exempt.py | 24 +++ .../is_uc_benefit_cap_exempt_earnings.py | 32 ++++ ...uc_benefit_cap_exempt_specified_benefit.py | 72 +++++++ .../is_uc_benefit_cap_single_claimant_rate.py | 37 ++++ .../benefit_cap/uc_benefit_cap.py | 29 +++ .../benefit_cap/uc_benefit_cap_reduction.py | 32 ++++ .../deductions/uc_deductions.py | 2 +- .../deductions/uc_has_deduction.py | 2 +- .../dwp/universal_credit/universal_credit.py | 2 +- 51 files changed, 1178 insertions(+), 559 deletions(-) create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/minimum_weekly_amount.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml create mode 100644 policyengine_uk/utils/benefit_cap.py delete mode 100644 policyengine_uk/variables/gov/dwp/benefit_cap.py create mode 100644 policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap_reduction.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_working_tax_credit.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_single_claimant_rate.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py delete mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py delete mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py delete mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py delete mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py delete mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py create mode 100644 policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_single_claimant_rate.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml index e47ddeb809..4e49290cc1 100644 --- a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_age_threshold.yaml @@ -6,7 +6,9 @@ metadata: unit: year label: Lha shared accommodation age threshold reference: - - title: The Housing Benefit Regulations 2006 - Schedule 4 - Paragraph 28 + - title: Universal Credit Regulations 2013 Sch 4 para 28(2)(a) href: https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28 + - title: Housing Benefit Regulations 2006 reg 2(1) ("young individual") + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/2 - title: LHA shared accommodation rate guidance - href: https://www.gov.uk/government/publications/local-housing-allowance-rates-applicable-from-april-2013 \ No newline at end of file + href: https://www.gov.uk/government/publications/local-housing-allowance-rates-applicable-from-april-2013 diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml index 26f65c2490..9f9765f986 100644 --- a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml @@ -1,12 +1,15 @@ -description: Disability benefits whose recipients under 35 are excepted from the shared accommodation rate. +description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. values: 2013-04-29: - attendance_allowance - dla_sc_middle_plus - pip_dl + - armed_forces_independence_payment metadata: unit: list - label: shared accommodation rate disability exceptions + label: Universal Credit shared accommodation rate disability exceptions reference: - title: Universal Credit Regulations 2013 Sch 4 para 29(5) href: https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/29 + - title: Universal Credit Regulations 2013 reg 2 ("attendance allowance" includes armed forces independence payment) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/2 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/minimum_weekly_amount.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/minimum_weekly_amount.yaml new file mode 100644 index 0000000000..24e98a4144 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/minimum_weekly_amount.yaml @@ -0,0 +1,12 @@ +description: Housing Benefit is not payable where the weekly amount would be less than this; where the benefit cap would reduce Housing Benefit below it, the reduction leaves this amount. +values: + 2006-03-06: 0.5 +metadata: + unit: currency-GBP + period: week + label: Housing Benefit minimum weekly amount + reference: + - title: Housing Benefit Regulations 2006 reg. 75 (minimum housing benefit) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/75 + - title: Housing Benefit Regulations 2006 reg. 75D(2) (reduction of housing benefit) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/75D diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml new file mode 100644 index 0000000000..82d79f1db6 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/severe_disability_premium/qualifying_benefits.yaml @@ -0,0 +1,16 @@ +description: Benefits whose receipt meets the qualifying-benefit condition of the Housing Benefit severe disability premium. +values: + 2013-04-08: + - attendance_allowance + - dla_sc_middle_plus + - pip_dl + - armed_forces_independence_payment +metadata: + unit: list + label: housing benefit severe disability premium qualifying benefits + reference: + - title: Housing Benefit Regulations 2006 Sch 3 para 14(2) + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/14 + - title: Housing Benefit (State Pension Credit) Regulations 2006 Sch 3 para 6 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6 + note: The Scottish benefits in the same list (adult disability payment, pension age disability payment, child disability payment and Scottish adult disability living allowance care components) have no separate variables in the model. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml index 75c99e4bb4..8adc343d4b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml @@ -1,7 +1,11 @@ -# UC Regulations 2013, Schedule 4 paragraphs 9 and 27-28: +# UC Regulations 2013, Schedule 4 paragraphs 9 and 27-29 (LHA_category): # https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28 +# HB Regulations 2006 reg 13D(2)(a)(i) and reg 2(1) "young individual" +# (housing_benefit_LHA_category): +# https://www.legislation.gov.uk/uksi/2006/213/regulation/13D # The existing bedroom calculation is unchanged: B = 1 bedroom, C = 2; # A is the shared accommodation rate, overriding the bedroom calculation. +# Where the schemes agree, each case asserts both categories. - name: Single 17-year-old renter is not their own dependent child period: 2025 input: @@ -13,6 +17,7 @@ is_lha_shared_accommodation_rate_specified_renter: true # Single; 17 < 35; no dependant or non-dependant: A (previously B). LHA_category: A + housing_benefit_LHA_category: A - name: Single 30-year-old renter remains subject to the shared rate period: 2025 @@ -23,6 +28,7 @@ is_lha_shared_accommodation_rate_specified_renter: true # Single; 30 < 35; no other occupants: A. LHA_category: A + housing_benefit_LHA_category: A - name: Single renter immediately below the shared rate age threshold period: 2025 @@ -32,6 +38,7 @@ is_lha_shared_accommodation_rate_specified_renter: true # 34 < 35: A. LHA_category: A + housing_benefit_LHA_category: A - name: Single renter at the shared rate age threshold period: 2025 @@ -41,6 +48,7 @@ is_lha_shared_accommodation_rate_specified_renter: false # 35 is not below 35; 1 bedroom: B. LHA_category: B + housing_benefit_LHA_category: B - name: A 17-year-old apprentice in the renter's benefit unit is a non-dependant period: 2025 @@ -65,6 +73,7 @@ is_lha_shared_accommodation_rate_specified_renter: false # No UC QYP, but a non-dependant blocks A. Existing bedroom formula = 1: B. LHA_category: B + housing_benefit_LHA_category: B - name: Lone parent with an 18-year-old qualifying young person period: 2025 @@ -89,8 +98,9 @@ is_lha_shared_accommodation_rate_specified_renter: false # Responsibility for a UC QYP blocks A. Existing bedroom formula = 1: B. LHA_category: B + housing_benefit_LHA_category: B -- name: Renter aged 30 whose partner is 17 claims as a single person and is a specified renter +- name: Renter aged 30 whose partner is 17 claims UC as a single person and is a specified renter; for HB they are a couple period: 2025 input: people: @@ -118,6 +128,11 @@ uc_member_of_couple_claims_as_single_person: true is_lha_shared_accommodation_rate_specified_renter: true LHA_category: A + # HB has no single claim by a member of a couple: a claimant with a + # partner is not a "single claimant", so not a young individual (HB reg + # 2(1)); the couple's one bedroom (reg 13D(3)(a)) gives B. + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B - name: Couple aged 30 and 17 where the partner is within reg. 8 is not a specified renter period: 2025 @@ -143,6 +158,7 @@ is_lha_shared_accommodation_rate_specified_renter: false # Joint renters have one bedroom (Sch 4 para 10(1)(a)): B. LHA_category: B + housing_benefit_LHA_category: B - name: A 17-year-old claimant in another benefit unit blocks the shared rate period: 2025 @@ -168,6 +184,7 @@ # Other unit's claimant blocks A; 1 + 1 occupant aged 16+ outside = 2: C. # Renter previously A because the other claimant was under 18. LHA_category: [C, C] + housing_benefit_LHA_category: [C, C] - name: A 17-year-old student claimant in another unit also blocks the shared rate period: 2025 @@ -191,6 +208,7 @@ # Other claimant blocks A even if their age/education meets the QYP test. # 1 + 1 occupant aged 16+ outside = 2: C (renter previously A). LHA_category: [C, C] + housing_benefit_LHA_category: [C, C] - name: Rent liability of another benefit unit does not exempt its claimant period: 2025 @@ -215,6 +233,7 @@ lha_renter_has_non_dependant: [true, true] # Conservative proxy retained: 1 + 1 occupant aged 16+ outside = 2: C. LHA_category: [C, C] + housing_benefit_LHA_category: [C, C] - name: Occupants in a different household do not block the shared rate period: 2025 @@ -239,6 +258,7 @@ # Single, 30 < 35, nobody else in this household: A. # Other renter 40 >= 35; 1 bedroom: B. LHA_category: [A, B] + housing_benefit_LHA_category: [A, B] - name: Actual shared accommodation retains its separate route to category A period: 2025 @@ -249,6 +269,7 @@ is_lha_shared_accommodation_rate_specified_renter: false # 40 >= 35, but actual shared accommodation independently gives A. LHA_category: A + housing_benefit_LHA_category: A - name: A disabled 17-year-old renter is excepted from the shared accommodation rate # UC Sch 4 para 29(5)(c): a renter under 35 receiving the PIP daily living @@ -260,6 +281,7 @@ output: is_lha_shared_accommodation_rate_specified_renter: false LHA_category: B + housing_benefit_LHA_category: B - name: A disabled 16-year-old renter is excepted from the shared accommodation rate # UC Sch 4 para 29(5)(a): the DLA care component at the middle or highest @@ -271,6 +293,7 @@ output: is_lha_shared_accommodation_rate_specified_renter: false LHA_category: B + housing_benefit_LHA_category: B - name: A 16-year-old renter without a disability benefit gets the shared rate period: 2025 @@ -279,6 +302,7 @@ output: is_lha_shared_accommodation_rate_specified_renter: true LHA_category: A + housing_benefit_LHA_category: A - name: A renter under 35 on attendance allowance is excepted from the shared rate period: 2025 @@ -288,3 +312,4 @@ output: is_lha_shared_accommodation_rate_specified_renter: false LHA_category: B + housing_benefit_LHA_category: B diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit.yaml index 9b0473a2ed..2aadfd747b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit.yaml @@ -3,7 +3,7 @@ absolute_error_margin: 100 input: housing_benefit_pre_benefit_cap: 1_000 - benefit_cap_reduction: 400 + housing_benefit_benefit_cap_reduction: 400 would_claim_housing_benefit: true output: housing_benefit: 600 @@ -13,7 +13,7 @@ absolute_error_margin: 100 input: housing_benefit_pre_benefit_cap: 1_000 - benefit_cap_reduction: 3_000 + housing_benefit_benefit_cap_reduction: 3_000 would_claim_housing_benefit: true output: housing_benefit: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml index 2778098821..6ed4fd7ae8 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml @@ -16,7 +16,7 @@ members: [person1] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1] @@ -50,7 +50,7 @@ members: [person1, person2] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2] @@ -86,7 +86,7 @@ members: [person1, person2, person3, person4] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2, person3, person4] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml index e9eee500eb..bc017468ab 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_relationships.yaml @@ -24,7 +24,7 @@ members: [person1, person2] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2] @@ -56,7 +56,7 @@ members: [person1, person2] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2] @@ -91,7 +91,7 @@ is_married: false uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2, person3] @@ -116,7 +116,7 @@ is_married: false uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2] @@ -173,7 +173,7 @@ members: [person1, person2] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2] @@ -203,7 +203,7 @@ members: [person1, person2] uc_reported_capital: 0 uc_latent_deduction_rate: 0 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: [person1, person2] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml index 8a18d6a46a..3fce83d844 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml @@ -443,14 +443,18 @@ benunits: benunit: members: [claimant, partner, child] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: household: members: [claimant, partner, child] region: NORTH_EAST output: - is_benefit_cap_single_claimant_rate: false - benefit_cap: 22_020 + is_uc_benefit_cap_single_claimant_rate: false + uc_benefit_cap: 22_020 + # HB: a couple with a child, so not a single claimant either. + is_housing_benefit_benefit_cap_single_claimant_rate: false + housing_benefit_benefit_cap: 22_020 - name: Reg. 80A(2)(c) - a single claimant with no child has the single cap period: 2026 @@ -466,14 +470,19 @@ benunits: benunit: members: [claimant, partner] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: household: members: [claimant, partner] region: NORTH_EAST output: - is_benefit_cap_single_claimant_rate: true - benefit_cap: 14_753 + is_uc_benefit_cap_single_claimant_rate: true + uc_benefit_cap: 14_753 + # HB has no single claim by a member of a couple: the claimant has a + # partner, so is not a single claimant (HB reg 2(1)); reg. 75CA(2)(d). + is_housing_benefit_benefit_cap_single_claimant_rate: false + housing_benefit_benefit_cap: 22_020 - name: Reg. 83(1)(g) - the other member's PIP does not lift the cap period: 2026 @@ -495,8 +504,12 @@ members: [claimant, partner] region: NORTH_EAST output: - is_benefit_cap_exempt_health_disability: false - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt_specified_benefit: false + is_uc_benefit_cap_exempt: false + # HB reg. 75F(1)(ea): the claimant's partner receiving PIP lifts the HB + # cap; HB has no single claim by a member of a couple. + is_housing_benefit_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt: true - name: Reg. 83(1)(g) - joint claimants, either receiving PIP, are not capped (control) period: 2026 @@ -517,7 +530,8 @@ members: [claimant, partner] region: NORTH_EAST output: - is_benefit_cap_exempt: true + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt: true - name: Reg. 83(1)(g) - the claimant's own PIP lifts the cap on a single claim period: 2026 @@ -539,7 +553,8 @@ members: [claimant, partner] region: NORTH_EAST output: - is_benefit_cap_exempt: true + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt: true - name: Reg. 82(1)(a) - the couple's combined earned income, the other member's included, meets the earnings exception period: 2026 @@ -566,7 +581,11 @@ # wage of 12.71 (NMW Regs 2015 reg. 4 from 1 April 2026): # 12.71 x 16 x 52 / 12 = 881.23 a month. The model uses a fixed # 10,152 a year; 11,000 clears both. - is_benefit_cap_exempt_earnings: true + is_uc_benefit_cap_exempt_earnings: true + # Housing Benefit has no earnings exception (HB Regs 2006 regs. + # 75E-75F); with no working tax credit the HB cap applies. + working_tax_credit: 0 + is_housing_benefit_benefit_cap_exempt: false - name: Sch 4 para 28(2) - a renter under 35 claiming as a single person is a specified renter, whatever the partner's age period: 2026 @@ -592,6 +611,10 @@ lha_renter_has_non_dependant: false is_lha_shared_accommodation_rate_specified_renter: true LHA_category: A + # HB: a claimant with a partner is not a young individual (HB reg 2(1)); + # a couple has one bedroom (reg. 13D(3)(a)): B. + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B - name: Sch 4 para 28(2) - a renter aged 35 or over claiming as a single person is not a specified renter period: 2026 @@ -615,6 +638,8 @@ output: is_lha_shared_accommodation_rate_specified_renter: false LHA_category: B + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B - name: Sch 4 para 29(5) - only the renter's own disability benefit excepts them from the shared rate period: 2026 @@ -659,7 +684,7 @@ is_uc_single_or_joint_claimant: false is_uc_eligible: false -- name: A family claiming legacy benefits keeps the couple rules Housing Benefit shares, while the UC award is a single claim +- name: A single claim is single for UC's cap and shared rate and a couple for HB's, whether or not the family claims legacy benefits period: 2026 absolute_error_margin: 0.01 input: @@ -675,7 +700,8 @@ benunits: benunit: members: [renter, partner] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: household: members: [renter, partner] @@ -684,10 +710,18 @@ output: claims_legacy_benefits: true uc_member_of_couple_claims_as_single_person: true - # Housing Benefit has no single claim by a member of a couple. - is_benefit_cap_single_claimant_rate: false - benefit_cap: 22_020 - is_lha_shared_accommodation_rate_specified_renter: false - LHA_category: B + # Each scheme applies its own rules, so the family's legacy claim + # changes neither. UC: a single claimant (reg. 80A(2)(c); Sch 4 para + # 28(2)). + is_uc_benefit_cap_single_claimant_rate: true + uc_benefit_cap: 14_753 + is_lha_shared_accommodation_rate_specified_renter: true + LHA_category: A + # Housing Benefit has no single claim by a member of a couple + # (reg. 75CA(2)(d); reg. 13D(3)(a)). + is_housing_benefit_benefit_cap_single_claimant_rate: false + housing_benefit_benefit_cap: 22_020 + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B # 424.90 x 12 uc_standard_allowance: 5_098.80 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml index 1840c13910..8069859343 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_deductions.yaml @@ -10,7 +10,7 @@ members: person would_claim_uc: true universal_credit_pre_benefit_cap: 6_000 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: person @@ -30,7 +30,7 @@ members: person would_claim_uc: true universal_credit_pre_benefit_cap: 6_000 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 uc_latent_deduction_rate: 0.25 uc_deduction_combination: ADVANCE_ONLY households: @@ -54,7 +54,7 @@ members: person would_claim_uc: true universal_credit_pre_benefit_cap: 6_000 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 uc_latent_deduction_rate: 0.25 uc_deduction_combination: ADVANCE_ONLY households: @@ -77,7 +77,7 @@ members: person would_claim_uc: true universal_credit_pre_benefit_cap: 6_000 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 uc_latent_deduction_rate: 0.28 uc_deduction_combination: GOVERNMENT_ONLY households: @@ -100,7 +100,7 @@ members: person would_claim_uc: true universal_credit_pre_benefit_cap: 500 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 uc_latent_deduction_rate: 0.25 uc_deduction_combination: ADVANCE_ONLY households: @@ -124,7 +124,7 @@ members: person would_claim_uc: true universal_credit_pre_benefit_cap: 6_000 - benefit_cap_reduction: 0 + uc_benefit_cap_reduction: 0 households: household: members: person diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml index 19ed1f92a6..29cb586127 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml @@ -1,27 +1,37 @@ # UC Regulations 2013, reg 80A(2), and HB Regulations 2006, reg 75CA(2): # annual single / family caps are 14,753 / 22,020 outside London and -# 16,967 / 25,323 in London. These cases isolate rate selection from -# exemptions and do not assert eligibility to claim UC or HB. +# 16,967 / 25,323 in London. Each scheme decides who is a single claimant: +# UC a single claimant (including a member of a couple claiming as a single +# person, reg 3(3)) not responsible for a child or qualifying young person; +# HB a claimant with no partner who is not a lone parent (reg 2(1)). These +# cases isolate rate selection from exemptions and do not assert eligibility +# to claim UC or HB. - name: Single claimant aged 30 receives the outside-London single cap. period: 2025 input: age: 30 region: SOUTH_EAST - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false output: - is_benefit_cap_single_claimant_rate: true - benefit_cap: 14_753 + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 - name: Single claimant aged 30 receives the London single cap. period: 2025 input: age: 30 region: LONDON - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false output: - is_benefit_cap_single_claimant_rate: true - benefit_cap: 16_967 + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true + uc_benefit_cap: 16_967 + housing_benefit_benefit_cap: 16_967 - name: Single claimant aged 17 does not count as their own child outside London. period: 2025 @@ -29,14 +39,17 @@ age: 17 is_benunit_head: true region: SOUTH_EAST - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false output: is_claimant_or_partner: true is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # Old age counts selected 22,020; no partner or dependant means 14,753. # Change = 14,753 - 22,020 = -7,267 per year. - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 - name: Single claimant aged 17 does not count as their own child in London. period: 2025 @@ -44,11 +57,14 @@ age: 17 is_benunit_head: true region: LONDON - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false output: - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # London change = 16,967 - 25,323 = -8,356 per year. - benefit_cap: 16_967 + uc_benefit_cap: 16_967 + housing_benefit_benefit_cap: 16_967 - name: A 17-year-old apprentice in the claimant's unit does not give the family cap. period: 2025 @@ -66,7 +82,8 @@ benunits: family: members: [claimant, apprentice] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, apprentice] @@ -74,10 +91,12 @@ output: is_claimant_or_partner: [true, false] is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # UC reg 5(3) excludes education under an employment contract. # Single cap: 14,753, down 22,020 - 14,753 = 7,267 from age-only counts. - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 - name: A 17-year-old outside education or approved training does not give the family cap. period: 2025 @@ -95,17 +114,20 @@ benunits: family: members: [claimant, young_person] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, young_person] region: SOUTH_EAST output: is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # UC reg 5(1)(b): no qualifying education/training after the age-16 route. # Change = 14,753 - 22,020 = -7,267 per year. - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 - name: A 17-year-old receiving benefits in their own right does not give the family cap. period: 2025 @@ -123,17 +145,20 @@ benunits: family: members: [claimant, young_person] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, young_person] region: SOUTH_EAST output: is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # UC reg 5(5) excludes own-right UC/ESA/JSA recipients from QYP status. # Change = 14,753 - 22,020 = -7,267 per year. - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 - name: A looked-after foster child does not give the claimant the family cap. period: 2025 @@ -149,18 +174,21 @@ benunits: family: members: [claimant, child] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, child] region: SOUTH_EAST output: is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # UC reg 4(6)(a): this foster child is excluded from responsibility. # Assume no respite or adoption placement exception under reg 4A. # Change = 14,753 - 22,020 = -7,267 per year. - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 - name: Parent of a 17-year-old qualifying student keeps the family cap. period: 2025 @@ -177,7 +205,8 @@ benunits: family: members: [parent, student] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [parent, student] @@ -185,9 +214,11 @@ output: is_couple: false is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true - is_benefit_cap_single_claimant_rate: false + is_uc_benefit_cap_single_claimant_rate: false + is_housing_benefit_benefit_cap_single_claimant_rate: false # Responsibility under UC regs 4-5 gives the 22,020 annual family limit. - benefit_cap: 22_020 + uc_benefit_cap: 22_020 + housing_benefit_benefit_cap: 22_020 - name: Lone parent of an 18-year-old qualifying student keeps the London family cap. period: 2025 @@ -204,7 +235,8 @@ benunits: family: members: [parent, student] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [parent, student] @@ -213,11 +245,13 @@ is_claimant_or_partner: [true, false] is_couple: false is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true - is_benefit_cap_single_claimant_rate: false + is_uc_benefit_cap_single_claimant_rate: false + is_housing_benefit_benefit_cap_single_claimant_rate: false # Same 25,323 as before, now because of responsibility rather than two 18+ members. - benefit_cap: 25_323 + uc_benefit_cap: 25_323 + housing_benefit_benefit_cap: 25_323 -- name: Couple aged 30 and 17 claims as a single person, so the single cap applies without counting the partner as a child. +- name: Couple aged 30 and 17 claims UC as a single person, so the UC single cap applies; HB caps them as a couple. period: 2025 input: people: @@ -231,7 +265,8 @@ benunits: family: members: [claimant, partner] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, partner] @@ -243,11 +278,16 @@ # the claimant claims as a single person (reg. 3(3)(a); ADM E2017). uc_is_ineligible_partner: [false, true] uc_member_of_couple_claims_as_single_person: true - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true # Reg. 80A(2)(c): 14,753 for a single claimant outside Greater London # who is not responsible for a child or qualifying young person (ADM # E5007 note 3 applies it to a member of a couple claiming as single). - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + # HB has no single claim by a member of a couple: the claimant has a + # partner, so is not a "single claimant" (HB reg 2(1)), and reg. + # 75CA(2)(d) gives 22,020 outside Greater London. + is_housing_benefit_benefit_cap_single_claimant_rate: false + housing_benefit_benefit_cap: 22_020 - name: Couple aged 30 and 17 where the partner is within reg. 8 claim jointly and receive the couple cap. period: 2025 @@ -266,7 +306,8 @@ benunits: family: members: [claimant, partner] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, partner] @@ -274,9 +315,11 @@ output: uc_is_ineligible_partner: [false, false] uc_member_of_couple_claims_as_single_person: false - is_benefit_cap_single_claimant_rate: false + is_uc_benefit_cap_single_claimant_rate: false + is_housing_benefit_benefit_cap_single_claimant_rate: false # Reg. 80A(2)(d)(i): 22,020 for joint claimants outside Greater London. - benefit_cap: 22_020 + uc_benefit_cap: 22_020 + housing_benefit_benefit_cap: 22_020 - name: Explicitly identified 19-year-old non-dependant does not imply a partner. period: 2025 @@ -292,7 +335,8 @@ benunits: family: members: [claimant, non_dependant] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: home: members: [claimant, non_dependant] @@ -300,29 +344,47 @@ output: is_couple: false is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: true # Old two-adult count gave 22,020. With explicit roles, no partner or QYP: # change = 14,753 - 22,020 = -7,267 per year. - benefit_cap: 14_753 + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 14_753 -- name: Benefit-cap exemption still overrides the selected single rate. +- name: A UC benefit-cap exemption still overrides the selected single rate. period: 2025 input: age: 30 region: SOUTH_EAST - is_benefit_cap_exempt: true + is_uc_benefit_cap_exempt: true universal_credit_pre_benefit_cap: 100_000 output: - is_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_single_claimant_rate: true # Exemption makes the cap infinite: max(100,000 - infinity, 0) = 0. # Assert the resulting reduction because the YAML runner cannot compare infinities. + uc_benefit_cap_reduction: 0 benefit_cap_reduction: 0 -- name: A Housing Benefit young person keeps the family cap though not a UC qualifying young person +- name: An HB benefit-cap exemption still overrides the selected single rate. + period: 2025 + input: + age: 30 + region: SOUTH_EAST + is_housing_benefit_benefit_cap_exempt: true + housing_benefit_pre_benefit_cap: 100_000 + would_claim_uc: false + output: + is_housing_benefit_benefit_cap_single_claimant_rate: true + housing_benefit_benefit_cap_reduction: 0 + benefit_cap_reduction: 0 + +- name: A Housing Benefit young person gives the HB family cap but not the UC one # The 19-year-old started non-advanced education at 17 and is past UC's # terminal date, so is not a UC qualifying young person (UC reg 5), but is a # Child Benefit qualifying young person, which Housing Benefit uses (HB reg - # 19). One cap serves both, so the family rate applies: 22,020. + # 19). HB: a lone parent, so the family rate (reg 75CA(2)(d)): 22,020. UC: a + # single claimant not responsible for a child or qualifying young person + # (reg 80A(2)(c)): 14,753. period: 2025 input: people: @@ -337,7 +399,8 @@ benunits: benunit: members: [parent, student] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: household: members: [parent, student] @@ -345,11 +408,16 @@ output: is_qualifying_young_person_for_universal_credit: [false, false] is_qualifying_young_person_for_child_benefit: [false, true] - benefit_cap: 22_020 + is_uc_benefit_cap_single_claimant_rate: true + is_housing_benefit_benefit_cap_single_claimant_rate: false + uc_benefit_cap: 14_753 + housing_benefit_benefit_cap: 22_020 -- name: A 16-year-old outside education keeps the family cap (UC reg 5(1)(a) window) +- name: A 16-year-old outside education keeps the family cap (UC reg 5(1)(a), Child Benefit regs 5 and 7) # Every 16-year-old is a UC qualifying young person until the 1 September - # after their 16th birthday; annual ages cannot place that date. + # after their 16th birthday, and a school leaver stays a Child Benefit + # qualifying young person (HB reg 19) until a terminal date; annual ages + # cannot place those dates, so both schemes give the family rate. period: 2025 input: people: @@ -362,10 +430,12 @@ benunits: benunit: members: [parent, teenager] - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false households: household: members: [parent, teenager] region: SOUTH_EAST output: - benefit_cap: 22_020 + uc_benefit_cap: 22_020 + housing_benefit_benefit_cap: 22_020 diff --git a/policyengine_uk/tests/test_abolish_benefit_cap_scenario.py b/policyengine_uk/tests/test_abolish_benefit_cap_scenario.py index f3afea7826..8546e4ba19 100644 --- a/policyengine_uk/tests/test_abolish_benefit_cap_scenario.py +++ b/policyengine_uk/tests/test_abolish_benefit_cap_scenario.py @@ -32,7 +32,7 @@ def test_removes_benefit_cap_reduction(self): baseline_reduction = baseline.calculate("benefit_cap_reduction", 2026) baseline_uc = baseline.calculate("universal_credit", 2026) - reformed_cap = reformed.calculate("benefit_cap", 2026) + reformed_cap = reformed.calculate("uc_benefit_cap", 2026) reformed_reduction = reformed.calculate("benefit_cap_reduction", 2026) reformed_uc = reformed.calculate("universal_credit", 2026) reformed_uc_pre_cap = reformed.calculate( @@ -40,8 +40,11 @@ def test_removes_benefit_cap_reduction(self): ) assert baseline_reduction[0] > 0 - assert np.isfinite(baseline.calculate("benefit_cap", 2026)[0]) + assert np.isfinite(baseline.calculate("uc_benefit_cap", 2026)[0]) assert np.isinf(reformed_cap[0]) + # The scenario lifts the Housing Benefit cap too: both schemes read + # the same annual limits. + assert np.isinf(reformed.calculate("housing_benefit_benefit_cap", 2026)[0]) assert reformed_reduction.tolist() == [0.0] assert reformed_uc.tolist() == reformed_uc_pre_cap.tolist() assert reformed_uc[0] > baseline_uc[0] diff --git a/policyengine_uk/tests/test_uc_deductions.py b/policyengine_uk/tests/test_uc_deductions.py index ffab86dfe2..a06e740ba4 100644 --- a/policyengine_uk/tests/test_uc_deductions.py +++ b/policyengine_uk/tests/test_uc_deductions.py @@ -15,7 +15,7 @@ def make_situation(**benunit_overrides): "members": ["person"], "would_claim_uc": {YEAR: True}, "universal_credit_pre_benefit_cap": {YEAR: 6_000}, - "benefit_cap_reduction": {YEAR: 0}, + "uc_benefit_cap_reduction": {YEAR: 0}, **benunit_overrides, } return { @@ -103,7 +103,7 @@ def test_protected_floor_limits_combined_reductions(self): # of the standard allowance; the floor limits the combined # reductions to (1 - 0.85) x standard allowance. situation = make_situation( - benefit_cap_reduction={YEAR: 2_500}, + uc_benefit_cap_reduction={YEAR: 2_500}, uc_latent_deduction_rate={YEAR: 0.25}, uc_deduction_combination={YEAR: "ADVANCE_ONLY"}, ) diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 6d6328543a..c38bb77a68 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -27,10 +27,14 @@ reg. 90(3)(b) for this case) is not engaged. 4. Capital (reg. 18(2)): the single claim's capital equals the joint claimants', the other member's Lifetime ISA included. -5. Benefit cap: the single-claimant rate applies exactly when no child is in - the family (reg. 80A(2)); the other member's disability benefits, LCWRA, - caring, AFCS and contributory ESA lift no cap (reg. 83(1)), so the health - and disability exception equals the joint claimants' with those removed. +5. Benefit cap: the Universal Credit single-claimant rate applies exactly + when no child is in the family (reg. 80A(2)); the other member's + disability benefits, LCWRA, caring, AFCS and contributory ESA lift no + Universal Credit cap (reg. 83(1)), so the exceptions equal the joint + claimants' with those removed. Housing Benefit has no single claim by a + member of a couple, so its cap rate, exceptions and shared accommodation + test (HB Regs 2006 regs. 2(1), 13D(2), 75CA, 75E, 75F) and its LHA + category are the joint claimants' whatever the flag. 6. Regulation 3(3)(a): with the flag left to its formula, a member of a couple under 18 is the ineligible partner exactly when none of the generated regulation 8(1) circumstances applies to them (limited @@ -38,8 +42,9 @@ child under 16 in the family), or they are under 16; the other member then claims as a single person. 7. A flag that marks no single claim (a single adult, or both members of a - couple) leaves every rule Housing Benefit shares (the cap rate, the cap - exceptions, the shared accommodation test) as it was without the flag. + couple) leaves the Universal Credit cap rate, cap exceptions and shared + accommodation test, and their Housing Benefit counterparts, as they were + without the flag. Each property also runs on EXAMPLE_FAMILIES, built so that the cases the properties are about occur: an other member with LCWRA, caring, PIP, AFCS @@ -253,10 +258,14 @@ def situation(families, year, mode): "uc_housing_costs_element", "is_uc_eligible", "is_uc_work_allowance_eligible", - "is_benefit_cap_single_claimant_rate", - "is_benefit_cap_exempt_health_disability", - "is_benefit_cap_exempt_other", + "is_uc_benefit_cap_single_claimant_rate", + "is_uc_benefit_cap_exempt_specified_benefit", + "is_uc_benefit_cap_exempt", "is_lha_shared_accommodation_rate_specified_renter", + "is_housing_benefit_benefit_cap_single_claimant_rate", + "is_housing_benefit_benefit_cap_exempt_specified_benefit", + "is_housing_benefit_benefit_cap_exempt", + "is_housing_benefit_young_individual", ] @@ -266,6 +275,8 @@ def calculate(families, year, mode, extra=()): values["claimant_type"] = np.asarray( sim.calculate("uc_standard_allowance_claimant_type", year) ).astype(str) + for v in ["LHA_category", "housing_benefit_LHA_category"]: + values[v] = np.asarray(sim.calculate(v, year)).astype(str) for v in extra: values[v] = np.asarray(sim.calculate(v, year)) values["parameters"] = sim.tax_benefit_system.parameters(f"{year}-01-01") @@ -392,6 +403,7 @@ def test_capital_includes_the_other_members(families, year, balances): def test_benefit_cap_rate_and_exceptions(families, year): single = calculate(families, year, "single") stripped = calculate(families, year, "stripped") + joint = calculate(families, year, "joint") # Reg. 80A(2): the single-claimant limit unless responsible for a child # or qualifying young person. A 16 or 17-year-old is a qualifying young # person only in qualifying education, which the generator leaves @@ -401,16 +413,32 @@ def test_benefit_cap_rate_and_exceptions(families, year): [all(age < 16 for age in family["children"]) for family in families] ) np.testing.assert_array_equal( - single["is_benefit_cap_single_claimant_rate"][under_16_only], + single["is_uc_benefit_cap_single_claimant_rate"][under_16_only], no_children[under_16_only], ) for variable in [ - "is_benefit_cap_exempt_health_disability", - "is_benefit_cap_exempt_other", + "is_uc_benefit_cap_exempt_specified_benefit", + "is_uc_benefit_cap_exempt", ]: np.testing.assert_array_equal( single[variable], stripped[variable], err_msg=f"{variable}: {families}" ) + # Housing Benefit has no single claim by a member of a couple: its rules + # see the couple whatever the Universal Credit flag says. + for variable in [ + "is_housing_benefit_benefit_cap_single_claimant_rate", + "is_housing_benefit_benefit_cap_exempt_specified_benefit", + "is_housing_benefit_benefit_cap_exempt", + "is_housing_benefit_young_individual", + "housing_benefit_LHA_category", + ]: + np.testing.assert_array_equal( + single[variable], joint[variable], err_msg=f"{variable}: {families}" + ) + # HB reg. 2(1): a claimant with a partner is never a single claimant or a + # young individual. + assert not np.any(single["is_housing_benefit_benefit_cap_single_claimant_rate"]) + assert not np.any(single["is_housing_benefit_young_individual"]) @PROPERTY_SETTINGS @@ -455,12 +483,18 @@ def test_a_partner_under_18_outside_regulation_8_cannot_claim_jointly( @PROPERTY_SETTINGS @given(families=populations, year=st.sampled_from(YEARS)) @example(families=example_families(), year=2026) -def test_a_flag_marking_no_single_claim_leaves_shared_rules_alone(families, year): +def test_a_flag_marking_no_single_claim_leaves_cap_and_lha_rules_alone(families, year): shared = [ - "is_benefit_cap_single_claimant_rate", - "is_benefit_cap_exempt_health_disability", - "is_benefit_cap_exempt_other", + "is_uc_benefit_cap_single_claimant_rate", + "is_uc_benefit_cap_exempt_specified_benefit", + "is_uc_benefit_cap_exempt", "is_lha_shared_accommodation_rate_specified_renter", + "LHA_category", + "is_housing_benefit_benefit_cap_single_claimant_rate", + "is_housing_benefit_benefit_cap_exempt_specified_benefit", + "is_housing_benefit_benefit_cap_exempt", + "is_housing_benefit_young_individual", + "housing_benefit_LHA_category", ] both = calculate(families, year, "both") joint = calculate(families, year, "joint") @@ -494,13 +528,23 @@ def test_examples_reach_the_cases(): assert single["uc_LCWRA_element"][second] > 0 assert single["uc_carer_element"][second] > 0 # Reg. 83(1): the other member's benefits lift the joint claimants' cap - # only. - assert not single["is_benefit_cap_exempt_health_disability"][first] - assert joint["is_benefit_cap_exempt_health_disability"][first] + # only. HB reg. 75F(1): the partner's benefits lift the HB cap either way. + assert not single["is_uc_benefit_cap_exempt_specified_benefit"][first] + assert joint["is_uc_benefit_cap_exempt_specified_benefit"][first] + assert single["is_housing_benefit_benefit_cap_exempt_specified_benefit"][first] # Reg. 80A(2) and Sch 4 para 28(2): single rate and shared accommodation. - assert single["is_benefit_cap_single_claimant_rate"][first] - assert not joint["is_benefit_cap_single_claimant_rate"][first] + # HB regs. 2(1), 13D(2) and 75CA(2): a couple either way. + assert single["is_uc_benefit_cap_single_claimant_rate"][first] + assert not joint["is_uc_benefit_cap_single_claimant_rate"][first] + assert not single["is_housing_benefit_benefit_cap_single_claimant_rate"][first] assert single["is_lha_shared_accommodation_rate_specified_renter"][first] assert not joint["is_lha_shared_accommodation_rate_specified_renter"][first] + assert single["LHA_category"][first] == "A" + assert not single["is_housing_benefit_young_individual"][first] + assert ( + single["housing_benefit_LHA_category"][first] + == joint["housing_benefit_LHA_category"][first] + == "B" + ) # Reg. 22(3): the other member's LCW gives the work allowance either way. assert single["is_uc_work_allowance_eligible"][first] diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py new file mode 100644 index 0000000000..4bd89d9b9d --- /dev/null +++ b/policyengine_uk/utils/benefit_cap.py @@ -0,0 +1,31 @@ +"""The annual limits of the benefit cap. + +Universal Credit and Housing Benefit apply the same four annual limits (UC +Regs 2013 reg. 80A(2); HB Regs 2006 reg. 75CA(2)), each scheme deciding for +itself who is a single claimant. Both place a claimant in Greater London by +the home they occupy (reg. 80A(3); reg. 75CA(3)). +""" + +from policyengine_core.model_api import select + + +def benefit_cap_annual_limit(benunit, period, parameters, single_claimant_rate): + """The applicable annual limit for the single-claimant or other rate.""" + household_region = benunit.members.household("region", period) + region = benunit.value_from_first_person(household_region) + in_london = region == household_region.possible_values.LONDON + cap = parameters(period).gov.dwp.benefit_cap + return select( + [ + single_claimant_rate & in_london, + single_claimant_rate & ~in_london, + ~single_claimant_rate & in_london, + ~single_claimant_rate & ~in_london, + ], + [ + cap.single.in_london, + cap.single.outside_london, + cap.non_single.in_london, + cap.non_single.outside_london, + ], + ) diff --git a/policyengine_uk/utils/benefit_unit.py b/policyengine_uk/utils/benefit_unit.py index 853726105f..8eb9a9fd35 100644 --- a/policyengine_uk/utils/benefit_unit.py +++ b/policyengine_uk/utils/benefit_unit.py @@ -19,3 +19,23 @@ def add_for_members(benunit, period, variables, members): else: total = total + benunit(name, period) return total + + +def has_sixteen_year_old_dependant(benunit, period): + """Whether a member aged 16 who is not the claimant or partner is present. + + Such a member is a dependant for part of the year under either scheme, + whatever their education: Universal Credit makes them a qualifying young + person up to the 1 September after their 16th birthday (UC Regs 2013 + reg. 5(1)(a)), and Housing Benefit takes the Child Benefit qualifying + young person (HB Regs 2006 reg. 19(1)), which a school leaver remains + until a terminal date or the end of an extension period (Child Benefit + (General) Regulations 2006 regs. 5 and 7). Annual ages cannot place + those dates, so the family rules that turn on responsibility for a child + or young person count such a member for the year. + """ + person = benunit.members + age = person("age", period) + return benunit.any( + (age >= 16) & (age < 17) & ~person("is_claimant_or_partner", period) + ) diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index c44804de19..175ee17968 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -39,23 +39,3 @@ def other_member_of_single_claim(person, period): return person("uc_is_ineligible_partner", period) & person.benunit( "uc_member_of_couple_claims_as_single_person", period ) - - -def single_claim_in_rules_shared_with_legacy_benefits(benunit, period): - """Whether a reg. 3(3) single claim sets the rules the model shares. - - The benefit cap rate and exceptions and the LHA shared accommodation - test serve Housing Benefit as well as Universal Credit. Housing Benefit - and the other legacy benefits have no single claim by a member of a - couple, so a family claiming legacy benefits keeps the couple rules. - """ - return benunit("uc_member_of_couple_claims_as_single_person", period) & ~benunit( - "claims_legacy_benefits", period - ) - - -def other_member_of_single_claim_in_shared_rules(person, period): - """`other_member_of_single_claim`, for the rules shared with legacy benefits.""" - return other_member_of_single_claim(person, period) & ~person.benunit( - "claims_legacy_benefits", period - ) diff --git a/policyengine_uk/variables/gov/dwp/LHA_cap.py b/policyengine_uk/variables/gov/dwp/LHA_cap.py index 090fcd23fb..1afa72f4da 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_cap.py +++ b/policyengine_uk/variables/gov/dwp/LHA_cap.py @@ -12,14 +12,19 @@ class LHA_cap(Variable): label = "Maximum rent (LHA)" documentation = ( "The Housing Benefit maximum rent (LHA): the Local Housing Allowance " - "rate for the benefit unit, or its rent where that is lower (SI " + "rate for the benefit unit's Housing Benefit category " + "(housing_benefit_LHA_rate), or its rent where that is lower (SI " "2006/213 and 2006/214 reg 13D(5): where the LHA exceeds the cap " "rent, the maximum rent (LHA) is the cap rent)." ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/13D", + ) def formula(benunit, period, parameters): rent = benunit("benunit_rent", period) - cap = benunit("BRMA_LHA_rate", period) + cap = benunit("housing_benefit_LHA_rate", period) return min_(rent, cap) diff --git a/policyengine_uk/variables/gov/dwp/LHA_category.py b/policyengine_uk/variables/gov/dwp/LHA_category.py index f0f2d4eb76..4c0c69460b 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_category.py +++ b/policyengine_uk/variables/gov/dwp/LHA_category.py @@ -123,11 +123,15 @@ def find_freeze_anchor(freeze_parameter: Parameter, period: str) -> str: MONTHLY_MAXIMUM_FIRST_YEAR = 2020 -def category_maximum(benunit, period, node_name: str): +def category_maximum( + benunit, period, node_name: str, category_variable: str = "LHA_category" +): """Per-category national maximum, read at the determination year. Frozen rates are held at the level last determined, so the maximum in force then is the one that binds, not the current year's. + ``category_variable`` names the category to look up: the Universal + Credit one by default, or the Housing Benefit one. """ lha = benunit.simulation.tax_benefit_system.parameters.gov.dwp.LHA @@ -137,6 +141,6 @@ def category_maximum(benunit, period, node_name: str): determination_period = str(period.start.year) node = getattr(lha, node_name) - category = benunit("LHA_category", period).decode_to_str() + category = benunit(category_variable, period).decode_to_str() caps = {cat: node.children[cat](determination_period) for cat in node.children} return pd.Series(category).map(caps).to_numpy(dtype=float) diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap.py b/policyengine_uk/variables/gov/dwp/benefit_cap.py deleted file mode 100644 index f67e218041..0000000000 --- a/policyengine_uk/variables/gov/dwp/benefit_cap.py +++ /dev/null @@ -1,33 +0,0 @@ -from policyengine_uk.model_api import * - - -class benefit_cap(Variable): - value_type = float - entity = BenUnit - label = "Benefit cap for the family" - definition_period = YEAR - unit = GBP - - def formula(benunit, period, parameters): - single_claimant = benunit("is_benefit_cap_single_claimant_rate", period) - household_region = benunit.members.household("region", period) - region = benunit.value_from_first_person(household_region) - regions = household_region.possible_values - in_london = region == regions.LONDON - cap = parameters(period).gov.dwp.benefit_cap - rate = select( - [ - single_claimant & in_london, - single_claimant & ~in_london, - ~single_claimant & in_london, - ~single_claimant & ~in_london, - ], - [ - cap.single.in_london, - cap.single.outside_london, - cap.non_single.in_london, - cap.non_single.outside_london, - ], - ) - exempt = benunit("is_benefit_cap_exempt", period) - return where(exempt, np.inf, rate) diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py index bb0fddc2a4..6e9de19409 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py @@ -3,26 +3,15 @@ class benefit_cap_reduction(Variable): label = "benefit cap reduction" + documentation = ( + "The reduction the benefit cap makes to the family's Universal Credit " + "(uc_benefit_cap_reduction, UC Regs 2013 reg. 81) or Housing Benefit " + "(housing_benefit_benefit_cap_reduction, HB Regs 2006 reg. 75D). Each " + "scheme applies its own cap; the model pays a family one or the " + "other, never both. A family with neither award has no reduction." + ) entity = BenUnit definition_period = YEAR value_type = float unit = GBP - - def formula(benunit, period, parameters): - CAPPED_BENEFITS = [ - "child_benefit", - "child_tax_credit", - "jsa_income", - "income_support", - "esa_income", - "universal_credit_pre_benefit_cap", - "housing_benefit_pre_benefit_cap", - "jsa_contrib", - "incapacity_benefit", - "esa_contrib", - "sda", - ] - return max_( - add(benunit, period, CAPPED_BENEFITS) - benunit("benefit_cap", period), - 0, - ) + adds = ["uc_benefit_cap_reduction", "housing_benefit_benefit_cap_reduction"] diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py new file mode 100644 index 0000000000..6616dfc545 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + +# Welfare benefits (Welfare Reform Act 2012 s. 96(10)) the benefit cap counts. +# Universal Credit counts Housing Benefit as nil (UC Regs 2013 reg. 80(2A)) +# and Housing Benefit counts its own amount before the cap (HB Regs 2006 +# reg. 75C(2)(b)). The model pays a family Universal Credit or Housing +# Benefit, never both, so one total serves both schemes. +CAPPED_BENEFITS = [ + "child_benefit", + "child_tax_credit", + "jsa_income", + "income_support", + "esa_income", + "universal_credit_pre_benefit_cap", + "housing_benefit_pre_benefit_cap", + "jsa_contrib", + "incapacity_benefit", + "esa_contrib", + "sda", +] + + +class benefit_cap_welfare_benefits(Variable): + value_type = float + entity = BenUnit + label = "Welfare benefits counted by the benefit cap" + documentation = ( + "The total of the welfare benefits the benefit cap compares with the " + "Universal Credit or Housing Benefit cap (UC Regs 2013 regs. 79(1) " + "and 80; HB Regs 2006 regs. 75A and 75C)." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/96", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/80", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75C", + ) + + def formula(benunit, period, parameters): + return add(benunit, period, CAPPED_BENEFITS) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap.py new file mode 100644 index 0000000000..bc00f3d429 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.benefit_cap import benefit_cap_annual_limit + + +class housing_benefit_benefit_cap(Variable): + value_type = float + entity = BenUnit + label = "Housing Benefit benefit cap" + documentation = ( + "The applicable annual limit on the welfare benefits of a Housing " + "Benefit claimant or couple (HB Regs 2006 reg. 75CA(2)), or infinity " + "where the cap does not apply: an exception in regulation 75E or 75F " + "(reg. 75A), or a claim under the pension-age regulations, which have " + "no benefit cap. The weekly relevant amount (the annual limit divided " + "by 52, rounded to the nearest penny: reg. 75CA(1)) is not rounded: " + "the model applies the annual limit." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75A", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75CA", + ) + + def formula(benunit, period, parameters): + limit = benefit_cap_annual_limit( + benunit, + period, + parameters, + benunit("is_housing_benefit_benefit_cap_single_claimant_rate", period), + ) + exempt = benunit("is_housing_benefit_benefit_cap_exempt", period) + return where(exempt, np.inf, limit) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap_reduction.py new file mode 100644 index 0000000000..a145d92c72 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/housing_benefit_benefit_cap_reduction.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_benefit_cap_reduction(Variable): + value_type = float + entity = BenUnit + label = "Housing Benefit benefit cap reduction" + documentation = ( + "The reduction the benefit cap makes to Housing Benefit: the excess " + "of the welfare benefits of the claimant or couple over the Housing " + "Benefit cap, limited so that the claimant keeps the minimum amount " + "of Housing Benefit, 50 pence a week (HB Regs 2006 regs. 75 and " + "75D)." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75", + ) + + def formula(benunit, period, parameters): + excess = max_( + benunit("benefit_cap_welfare_benefits", period) + - benunit("housing_benefit_benefit_cap", period), + 0, + ) + entitlement = benunit("housing_benefit_pre_benefit_cap", period) + minimum = ( + parameters(period).gov.dwp.housing_benefit.minimum_weekly_amount + * WEEKS_IN_YEAR + ) + return min_(excess, max_(entitlement - minimum, 0)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt.py new file mode 100644 index 0000000000..38b0f86562 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * + + +class is_housing_benefit_benefit_cap_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Housing Benefit benefit cap" + documentation = ( + "Whether the benefit cap does not apply to the family's Housing " + "Benefit: regulation 75E (working tax credit) or 75F (specified " + "benefits) of the HB Regs 2006 applies (reg. 75A), or the claim falls " + "under the Housing Benefit (Persons who have attained the qualifying " + "age for state pension credit) Regulations 2006 (reg. 5), which have " + "no benefit cap. Housing Benefit has no earnings exception and no " + "exception for the Universal Credit LCWRA or carer elements." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75A", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75E", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75F", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", + ) + + def formula(benunit, period, parameters): + return ( + benunit("is_housing_benefit_benefit_cap_exempt_working_tax_credit", period) + | benunit("is_housing_benefit_benefit_cap_exempt_specified_benefit", period) + | benunit("housing_benefit_pension_age_regulations_apply", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py new file mode 100644 index 0000000000..b884be826b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py @@ -0,0 +1,54 @@ +from policyengine_uk.model_api import * + +# Benefits whose receipt (or entitlement) by the claimant or partner, or for +# some a child or young person, lifts the cap: industrial injuries benefit +# (HB Regs 2006 reg. 75F(1)(b)), attendance allowance (c), disability living +# allowance (e), personal independence payment (ea), carer's allowance (h) +# and carer support payment (ha). +SPECIFIED_PERSONAL_BENEFITS = [ + "attendance_allowance", + "carers_allowance", + "carer_support_payment", + "dla", + "pip_dl", + "pip_m", + "iidb", +] + + +class is_housing_benefit_benefit_cap_exempt_specified_benefit(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Housing Benefit benefit cap through a specified benefit" + documentation = ( + "Whether the claimant, partner or (for some benefits) a child or " + "young person receives or is entitled to a benefit listed in HB Regs " + "2006 reg. 75F(1): employment and support allowance with the support " + "component, industrial injuries benefit, attendance allowance, a war " + "pension or armed forces compensation, disability living allowance, " + "personal independence payment, carer's allowance or carer support " + "payment. Housing Benefit has no claim by a member of a couple as a " + "single person, so the partner's benefits always count. The Universal " + "Credit LCWRA and carer elements are not Housing Benefit exceptions. " + "Regulation 75F(1)(g) (a claimant receiving Universal Credit) never " + "applies: the model pays a family Universal Credit or Housing " + "Benefit, not both." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75F", + "https://www.gov.uk/benefit-cap/when-youre-not-affected", + ) + + def formula(benunit, period, parameters): + # Reg. 75F(1)(a): an employment and support allowance which includes + # a support component (contributory or income-related allowances + # stand in for it). + esa_support_component = (benunit("esa_income", period) > 0) | ( + add(benunit, period, ["esa_contrib"]) > 0 + ) + specified_benefits = add(benunit, period, SPECIFIED_PERSONAL_BENEFITS) > 0 + # Reg. 75F(1)(d) and (2)(a): a war pension, including a guaranteed + # income payment under the Armed Forces Compensation Scheme. + afcs = add(benunit, period, ["afcs"]) > 0 + return esa_support_component | specified_benefits | afcs diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_working_tax_credit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_working_tax_credit.py new file mode 100644 index 0000000000..ce55bfb428 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_working_tax_credit.py @@ -0,0 +1,18 @@ +from policyengine_uk.model_api import * + + +class is_housing_benefit_benefit_cap_exempt_working_tax_credit(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Housing Benefit benefit cap through working tax credit" + documentation = ( + "Whether the claimant is, or the claimant and partner are jointly, " + "entitled to working tax credit (HB Regs 2006 reg. 75E(2)). The " + "39-week grace period after work ends (reg. 75E(3)-(5)) is not " + "modelled." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/75E" + + def formula(benunit, period, parameters): + return benunit("working_tax_credit", period) > 0 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_single_claimant_rate.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_single_claimant_rate.py new file mode 100644 index 0000000000..2153f6137e --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_single_claimant_rate.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.benefit_unit import has_sixteen_year_old_dependant + + +class is_housing_benefit_benefit_cap_single_claimant_rate(Variable): + value_type = bool + entity = BenUnit + label = "Housing Benefit benefit cap single claimant rate applies" + documentation = ( + "Whether the Housing Benefit claimant is a single claimant (HB Regs " + "2006 reg. 75CA(2)(a) and (c)): a claimant who neither has a partner " + "nor is a lone parent, that is, responsible for a child or young " + "person in the same household (reg. 2(1)). Housing Benefit has no " + "claim by a member of a couple as a single person, so a couple has " + "the other rate even where the partner could not be a joint claimant " + "for Universal Credit. A 16-year-old member counts as a young person " + "for the year (reg. 19(1); Child Benefit (General) Regulations 2006 " + "regs. 5 and 7)." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/75CA", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + ) + + def formula(benunit, period, parameters): + return benunit("is_single_person", period) & ~has_sixteen_year_old_dependant( + benunit, period + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit.py index 2b02130f71..088ecbf5b9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit.py @@ -11,5 +11,5 @@ class housing_benefit(Variable): def formula(benunit, period, parameters): housing_benefit_entitlement = benunit("housing_benefit_pre_benefit_cap", period) - benefit_cap_reduction = benunit("benefit_cap_reduction", period) + benefit_cap_reduction = benunit("housing_benefit_benefit_cap_reduction", period) return max_(0, housing_benefit_entitlement - benefit_cap_reduction) 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..d843bf2708 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py @@ -0,0 +1,47 @@ +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 (HB Regs " + "2006 reg. 5). 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: Universal Credit needs one " + "(Welfare Reform Act 2012 s. 4(1)(b)) except for tax credit migrants " + "(SI 2014/1230 reg. 60A), whom the model does not have. Taken from " + "PolicyEngine-UK #1944." + ) + 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) + # 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 + on_income_related_benefit = ( + add(benunit, period, ["income_support", "jsa_income", "esa_income"]) > 0 + ) + return ( + attained_qualifying_age & ~on_universal_credit & ~on_income_related_benefit + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py new file mode 100644 index 0000000000..b48ce595e1 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_severe_disability_premium_applies.py @@ -0,0 +1,48 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_severe_disability_premium_applies(Variable): + value_type = bool + entity = BenUnit + label = "Severe disability premium conditions met (Housing Benefit)" + documentation = ( + "Whether the claimant is a severely disabled person for the Housing " + "Benefit severe disability premium (HB Regs 2006 Sch 3 para 14): the " + "claimant (and any partner) receives a qualifying disability benefit, " + "and no non-dependant aged 18 or over normally resides with them, " + "other than one who receives a qualifying benefit. Within the family " + "a non-dependant is an adult who is neither the claimant nor partner " + "nor a child or young person; outside it, the household composition " + "proxy of lha_renter_has_non_dependant (another benefit unit's " + "claimant or partner). Not modelled: who receives carer's allowance, " + "carer support payment or the UC carer element for caring for the " + "claimant, and the treatment of a blind partner. The legacy " + "severe_disability_premium variable uses the tax credit definition of " + "severe disability, which is narrower." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/14", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/6", + ) + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.housing_benefit.severe_disability_premium + person = benunit.members + qualifying = add(person, period, p.qualifying_benefits) > 0 + claimant_or_partner = person("is_claimant_or_partner", period) + # Sch 3 para 14(2)(a)(i), (b)(i)-(ii): every claimant or partner + # receives a qualifying benefit. + all_qualify = benunit.all(qualifying | ~claimant_or_partner) & benunit.any( + claimant_or_partner + ) + # Para 14(2)(a)(ii), (b)(iii) with (4): no non-dependant aged 18 or + # over, disregarding one who receives a qualifying benefit. + age = person("age", period) + adult_non_dependant = (age >= 18) & ~qualifying + young_person = person("is_child_or_young_person_for_legacy_benefits", period) + within = benunit.any(adult_non_dependant & ~claimant_or_partner & ~young_person) + other_claimants = benunit.max( + person.household.sum(adult_non_dependant & claimant_or_partner) + ) - benunit.sum(adult_non_dependant & claimant_or_partner) + return all_qualify & ~within & ~(other_claimants > 0) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py new file mode 100644 index 0000000000..91f037644a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_category.py @@ -0,0 +1,62 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.LHA_category import LHACategory + + +class housing_benefit_LHA_category(Variable): + value_type = Enum + entity = BenUnit + label = "LHA category of dwelling (Housing Benefit)" + documentation = ( + "The Housing Benefit category of dwelling (HB Regs 2006 reg. " + "13D(2)). The shared accommodation rate applies to a young individual " + "with no non-dependant to whom the severe disability premium does not " + "apply, and, as for Universal Credit, in shared accommodation. " + "Otherwise the category follows the number of bedrooms in the size " + "criteria, up to four. Housing Benefit has no claim by a member of a " + "couple as a single person, so a couple is never a young individual. " + "Universal Credit has its own category: see LHA_category." + ) + definition_period = YEAR + possible_values = LHACategory + default_value = LHACategory.C + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B", + ) + + def formula(benunit, period, parameters): + rooms = benunit("LHA_allowed_bedrooms", period.this_year) + household = benunit.members.household + is_shared = benunit.any(household("is_shared_accommodation", period.this_year)) + # HB Regs 2006 reg 13D(2)(a)(i): a young individual who has no + # non-dependant residing with them and to whom Sch 3 para 14 (severe + # disability premium) does not apply. A Universal Credit qualifying + # young person who is not a Housing Benefit young person is a + # non-dependant (reg. 3), which the household composition proxy does + # not identify, so responsibility under either scheme also blocks it. + young_individual = ( + benunit("is_housing_benefit_young_individual", period) + & ~benunit("lha_renter_has_non_dependant", period) + & ~benunit( + "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", + period, + ) + & ~benunit("housing_benefit_severe_disability_premium_applies", period) + ) + return select( + [ + young_individual | is_shared, + rooms == 1, + rooms == 2, + rooms == 3, + rooms > 3, + ], + [ + LHACategory.A, + LHACategory.B, + LHACategory.C, + LHACategory.D, + LHACategory.E, + ], + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py new file mode 100644 index 0000000000..b21e8c6e4d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_rate.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.LHA_category import category_maximum +from policyengine_uk.variables.gov.dwp.uncapped_BRMA_LHA_rate import ( + lha_rate_for_category, +) + + +class housing_benefit_LHA_rate(Variable): + value_type = float + entity = BenUnit + label = "LHA rate (Housing Benefit)" + documentation = ( + "The Local Housing Allowance for the Housing Benefit category of " + "dwelling: the Broad Rental Market Area rate, capped at the weekly " + "national maximum for the category." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", + "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B", + ) + + def formula(benunit, period, parameters): + category = "housing_benefit_LHA_category" + rate = lha_rate_for_category(benunit, period, category) + maximum = category_maximum(benunit, period, "maximum", category) + return min_(rate, maximum * 52) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py deleted file mode 100644 index 563b340854..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py +++ /dev/null @@ -1,15 +0,0 @@ -from policyengine_uk.model_api import * - - -class is_benefit_cap_exempt(Variable): - value_type = bool - entity = BenUnit - label = "Whether exempt from the benefits cap because of health or disability" - definition_period = YEAR - reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" - - def formula(benunit, period, parameters): - exempt_health = benunit("is_benefit_cap_exempt_health_disability", period) - exempt_other = benunit("is_benefit_cap_exempt_other", period) - exempt_earnings = benunit("is_benefit_cap_exempt_earnings", period) - return exempt_health | exempt_earnings | exempt_other 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 deleted file mode 100644 index c27e13c39d..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ /dev/null @@ -1,60 +0,0 @@ -from policyengine_uk.model_api import * - - -class is_benefit_cap_exempt_earnings(Variable): - value_type = bool - 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" - - 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) - ) - 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 - - # ESA contribution-based with support component - esa_support_component = benunit("esa_contrib", period) > 0 - - return meets_earnings_test 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 deleted file mode 100644 index 4637e8812c..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ /dev/null @@ -1,85 +0,0 @@ -from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim_in_shared_rules, -) -from policyengine_uk.utils.benefit_unit import add_for_members - - -class is_benefit_cap_exempt_health_disability(Variable): - value_type = bool - entity = BenUnit - label = "Whether exempt from the benefits cap because of health or disability" - definition_period = YEAR - 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) - - # The exceptions read "a claimant" (UC Regs 2013 reg. 83(1)). A - # partner who cannot be a joint claimant, so that the other member - # claims Universal Credit as a single person (reg. 3(3)), is not a - # claimant: their own disability benefits and limited capability do - # not lift the cap. - not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) - - # UC-specific exemptions - # Limited capability for work and work-related activity - has_lcwra = benunit.any( - person("uc_limited_capability_for_WRA", period) & ~not_a_claimant - ) - - # 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) - ) - 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", - "carer_support_payment", - "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_for_members( - benunit, period, QUAL_PERSONAL_BENEFITS, ~not_a_claimant - ) - qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) - - # Check for Armed Forces Compensation Scheme payments - afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 - - # ESA contribution-based with support component - esa_support_component = ( - add_for_members(benunit, period, ["esa_contrib"], ~not_a_claimant) > 0 - ) - - return ( - has_lcwra - | gets_uc_carer_element - | (qualifying_personal_benefits > 0) - | (qualifying_benunit_benefits > 0) - | afcs - | esa_support_component - ) 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 deleted file mode 100644 index b4524710f9..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +++ /dev/null @@ -1,71 +0,0 @@ -from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim_in_shared_rules, -) -from policyengine_uk.utils.benefit_unit import add_for_members - - -class is_benefit_cap_exempt_other(Variable): - value_type = bool - 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" - - 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) - ) - 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) - - # The AFCS and ESA exceptions read "a claimant" (UC Regs 2013 - # reg. 83(1)(a) and (e)); a partner who cannot be a joint claimant - # (reg. 3(3)) is not one. - not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) - - # Check for Armed Forces Compensation Scheme payments - afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 - - # ESA contribution-based with support component - esa_support_component = ( - add_for_members(benunit, period, ["esa_contrib"], ~not_a_claimant) > 0 - ) - - return has_pensioner | afcs | esa_support_component diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py deleted file mode 100644 index 4e57669bfd..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py +++ /dev/null @@ -1,41 +0,0 @@ -from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - single_claim_in_rules_shared_with_legacy_benefits, -) - - -class is_benefit_cap_single_claimant_rate(Variable): - value_type = bool - entity = BenUnit - label = "Benefit cap single claimant rate applies" - documentation = ( - "Whether the claimant is a single claimant, not responsible for a " - "child or young person. The model uses one benefit cap for Universal " - "Credit and Housing Benefit, so the family rate applies if either " - "scheme's child or young-person test is met. Couple status stands for " - "joint-claim status, except that a member of a couple who claims " - "Universal Credit as a single person (regulation 3(3)) is a single " - "claimant: regulation 80A sets the limits by single claimant and " - "joint claimants (ADM E5007 note 3). The welfare benefits capped are " - "still the couple's (regulations 78(2) and 79(1)). Housing Benefit " - "has no such single claim, so a family claiming legacy benefits " - "(`claims_legacy_benefits`) keeps the couple rate." - ) - definition_period = YEAR - reference = ( - "https://www.legislation.gov.uk/uksi/2013/376/regulation/80A", - "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", - "https://www.legislation.gov.uk/uksi/2013/376/regulation/78", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/75CA", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", - ) - - def formula(benunit, period, parameters): - single_claimant = ~benunit( - "is_couple", period - ) | single_claim_in_rules_shared_with_legacy_benefits(benunit, period) - return single_claimant & ~benunit( - "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", - period, - ) diff --git a/policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py b/policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py new file mode 100644 index 0000000000..f66e7af8ee --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_housing_benefit_young_individual.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class is_housing_benefit_young_individual(Variable): + value_type = bool + entity = BenUnit + label = "Young individual (Housing Benefit)" + documentation = ( + "A single claimant (no partner and not a lone parent) under the " + "shared accommodation age threshold. The exceptions for a housing " + "association landlord, care leavers, former hostel residents, " + "offenders under multi-agency management, people who need overnight " + "care, qualifying parents or carers, victims of domestic violence " + "and of modern slavery are not identified." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/2" + + def formula(benunit, period, parameters): + threshold = parameters(period).gov.dwp.LHA.shared_accommodation_age_threshold + return benunit("is_single_person", period) & ( + benunit("eldest_claimant_or_partner_age", period) < threshold + ) diff --git a/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py index 68db1b2923..e858bd8826 100644 --- a/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py +++ b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py @@ -1,7 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim_in_shared_rules, - single_claim_in_rules_shared_with_legacy_benefits, + other_member_of_single_claim, ) @@ -10,31 +9,27 @@ class is_lha_shared_accommodation_rate_specified_renter(Variable): entity = BenUnit label = "LHA renter restricted to the shared accommodation rate" documentation = ( - "The modelled specified-renter conditions: no partner, or a member of " - "a couple who claims Universal Credit as a single person (UC " - "regulation 3(3); Schedule 4 paragraph 28(2)), below the shared " - "accommodation age threshold, not responsible for a child or young " - "person under the UC or Housing Benefit rules, no non-dependant under " - "the household composition proxy, and not excepted by a disability " - "benefit (UC Schedule 4 paragraph 29(5)). The age and the exception " - "are the renter's own: the other member of a couple claiming as a " - "single person is neither a renter nor a non-dependant (paragraph " - "9(2)(b)). Other paragraph 29 exceptions are not modelled. The same " - "category serves Housing Benefit, whose young-individual definition " - "(HB regulation 2(1)) has neither the disability exception nor a " - "single claim by a member of a couple. The model applies the " - "disability exception to Housing Benefit too, and the single-claim " - "rule unless the family claims legacy benefits " - "(`claims_legacy_benefits`)." + "The Universal Credit specified-renter conditions (UC Regs 2013 Sch 4 " + "paras 27 and 28): a single person, or a member of a couple claiming " + "as a single person (reg. 3(3)), who is under the shared " + "accommodation age threshold and not excepted by a disability benefit " + "(para 29(5); armed forces independence payment counts as attendance " + "allowance, reg. 2); not responsible for a child or qualifying young " + "person; and with no non-dependant under the household composition " + "proxy. The age and the exception are the renter's own: the other " + "member of a couple claiming as a single person is neither the " + "renter nor a non-dependant (para 9(2)(b)). Other paragraph 29 " + "exceptions are not modelled. Housing Benefit has its own test: see " + "is_housing_benefit_young_individual and housing_benefit_LHA_category." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/27", "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28", "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/29", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/2", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", ) def formula(benunit, period, parameters): @@ -45,10 +40,10 @@ def formula(benunit, period, parameters): # and (b) is not an excepted person". Where a member of a couple # claims as a single person (reg. 3(3)), the renter is that member: # the other member's age and benefits do not count. - claims_as_single_person = single_claim_in_rules_shared_with_legacy_benefits( - benunit, period + claims_as_single_person = benunit( + "uc_member_of_couple_claims_as_single_person", period ) - other_member = other_member_of_single_claim_in_shared_rules(person, period) + other_member = other_member_of_single_claim(person, period) renter = person("is_claimant_or_partner", period) & ~other_member renter_age = benunit.max(where(renter, person("age", period), -np.inf)) # UC Sch 4 para 29(5): a renter under 35 receiving @@ -63,6 +58,10 @@ def formula(benunit, period, parameters): return ( (~benunit("is_couple", period) | claims_as_single_person) & (renter_age < p.shared_accommodation_age_threshold) + # Para 28(3)-(4). A Housing Benefit young person who is not a + # Universal Credit qualifying young person is a non-dependant + # (para 9), which the household composition proxy does not + # identify; either way the renter is not a specified renter. & ~benunit( "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", period, diff --git a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py index 2897b52f3a..c734ca21a3 100644 --- a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py +++ b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py @@ -4,12 +4,17 @@ class is_responsible_for_child_or_young_person_for_uc_or_housing_benefit(Variable): """Responsible for a child or young person under UC or Housing Benefit rules. - The benefit cap and the LHA category each serve both Universal Credit and - Housing Benefit in this model. UC counts a child or qualifying young person - (UC Regs 2013 regs 4-5); Housing Benefit counts a child or young person, - whose young-person test follows Child Benefit (HB Regs 2006 regs 2, 19). - The family rates apply if either scheme's test is met, so neither scheme's - families are moved onto the single rate by the other's narrower definition. + The LHA shared accommodation tests read this. UC counts a child or + qualifying young person (UC Regs 2013 regs 4-5); Housing Benefit counts a + child or young person, whose young-person test follows Child Benefit (HB + Regs 2006 regs 2, 19). A member who is a dependant under one scheme only + is, under the other, a non-dependant (UC Sch 4 para 9; HB reg 3), which + the household composition proxy (lha_renter_has_non_dependant) does not + identify. Either way the renter is not limited to the shared + accommodation rate (UC Sch 4 para 28(3)-(4); HB reg 13D(2)(a)(i)), so each + scheme's test reads responsibility under either scheme. The benefit cap + rates, where a non-dependant does not matter, read each scheme's own + responsibility test. UC reg 5(1)(a) also makes every 16-year-old a qualifying young person until the 1 September after their 16th birthday, whatever their education. Annual diff --git a/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py b/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py index 6ccef71675..27658fd46e 100644 --- a/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py +++ b/policyengine_uk/variables/gov/dwp/uncapped_BRMA_LHA_rate.py @@ -10,6 +10,72 @@ warnings.filterwarnings("ignore") +def lha_rate_for_category(benunit, period, category_variable="LHA_category"): + """The Broad Rental Market Area LHA rate (annual) for a category. + + ``category_variable`` names the category: the Universal Credit one by + default, or the Housing Benefit one. + """ + brma = benunit.value_from_first_person( + benunit.members.household("brma", period).decode_to_str() + ) + category = benunit(category_variable, period).decode_to_str() + + from policyengine_uk.parameters.gov.dwp.LHA import lha_list_of_rents + + parameters = benunit.simulation.tax_benefit_system.parameters + lha = parameters.gov.dwp.LHA + + # We first need to know what time period to collect rents from. If LHA is frozen, we need to look earlier + # than the current time period. + + frozen = lha.freeze(period) + if frozen: + # Rates are held at the level last determined, so every input to + # the determination is read at that year, not the current one. + freeze_anchor = find_freeze_anchor(lha.freeze, period.start) + lha_period = int(freeze_anchor[:4]) # Get year + else: + lha_period = int(period.start.year) + + determination_period = str(lha_period) + + private_rent_index = parameters.gov.indices.private_rent_index + lha_list_of_rents = time_shift_dataset( + lha_list_of_rents.copy(), lha_period, private_rent_index + ) + + percentile = lha.percentile(determination_period) + + lha_rates = lha_list_of_rents.groupby( + ["brma", "lha_category"] + ).weekly_rent.quantile(percentile) + + # Convert MultiIndex Series to DataFrame for merge + lha_rates_df = lha_rates.reset_index() + lha_rates_df.columns = ["brma", "lha_category", "weekly_rent"] + + # Determined rates are rounded to the nearest penny, half up + # (Schedule 3B paragraph 2(10)); np.round is half-even. Pence are + # snapped to 6dp first, because an exact half such as 298.835 is + # held as 29883.499999999996 once scaled and would round down. + lha_rates_df.weekly_rent = ( + np.floor(np.round(lha_rates_df.weekly_rent * 100, 6) + 0.5) / 100 + ) + + lha_lookup_table = pd.DataFrame( + { + "brma": brma, + "lha_category": category, + } + ) + # Use merge instead of row-by-row apply for vectorised lookup + lha_lookup_table = lha_lookup_table.merge( + lha_rates_df, on=["brma", "lha_category"], how="left" + ) + return lha_lookup_table.weekly_rent.values * 52 + + class uncapped_BRMA_LHA_rate(Variable): value_type = float entity = BenUnit @@ -20,61 +86,4 @@ class uncapped_BRMA_LHA_rate(Variable): reference = "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B" def formula(benunit, period, parameters): - brma = benunit.value_from_first_person( - benunit.members.household("brma", period).decode_to_str() - ) - category = benunit("LHA_category", period).decode_to_str() - - from policyengine_uk.parameters.gov.dwp.LHA import lha_list_of_rents - - parameters = benunit.simulation.tax_benefit_system.parameters - lha = parameters.gov.dwp.LHA - - # We first need to know what time period to collect rents from. If LHA is frozen, we need to look earlier - # than the current time period. - - frozen = lha.freeze(period) - if frozen: - # Rates are held at the level last determined, so every input to - # the determination is read at that year, not the current one. - freeze_anchor = find_freeze_anchor(lha.freeze, period.start) - lha_period = int(freeze_anchor[:4]) # Get year - else: - lha_period = int(period.start.year) - - determination_period = str(lha_period) - - private_rent_index = parameters.gov.indices.private_rent_index - lha_list_of_rents = time_shift_dataset( - lha_list_of_rents.copy(), lha_period, private_rent_index - ) - - percentile = lha.percentile(determination_period) - - lha_rates = lha_list_of_rents.groupby( - ["brma", "lha_category"] - ).weekly_rent.quantile(percentile) - - # Convert MultiIndex Series to DataFrame for merge - lha_rates_df = lha_rates.reset_index() - lha_rates_df.columns = ["brma", "lha_category", "weekly_rent"] - - # Determined rates are rounded to the nearest penny, half up - # (Schedule 3B paragraph 2(10)); np.round is half-even. Pence are - # snapped to 6dp first, because an exact half such as 298.835 is - # held as 29883.499999999996 once scaled and would round down. - lha_rates_df.weekly_rent = ( - np.floor(np.round(lha_rates_df.weekly_rent * 100, 6) + 0.5) / 100 - ) - - lha_lookup_table = pd.DataFrame( - { - "brma": brma, - "lha_category": category, - } - ) - # Use merge instead of row-by-row apply for vectorised lookup - lha_lookup_table = lha_lookup_table.merge( - lha_rates_df, on=["brma", "lha_category"], how="left" - ) - return lha_lookup_table.weekly_rent.values * 52 + return lha_rate_for_category(benunit, period) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt.py new file mode 100644 index 0000000000..c47f3fd20d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class is_uc_benefit_cap_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Universal Credit benefit cap" + documentation = ( + "Whether regulation 82 (earnings) or 83 (entitlement to or receipt of " + "certain benefits) of the UC Regs 2013 lifts the benefit cap " + "(reg. 79(1)). The Universal Credit cap has no exception for age: a " + "mixed-age couple's joint award is capped like any other." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/83", + ) + + def formula(benunit, period, parameters): + return benunit("is_uc_benefit_cap_exempt_earnings", period) | benunit( + "is_uc_benefit_cap_exempt_specified_benefit", period + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py new file mode 100644 index 0000000000..9f416a7881 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py @@ -0,0 +1,32 @@ +from policyengine_uk.model_api import * + + +class is_uc_benefit_cap_exempt_earnings(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Universal Credit benefit cap because of earnings" + documentation = ( + "Whether the claimant's earned income, or a couple's combined earned " + "income, reaches the earnings threshold (UC Regs 2013 reg. 82(1)(a)). " + "For a member of a couple claiming as a single person the couple's " + "earnings combine. The grace period (reg. 82(1)(b)) is not modelled. " + "Housing Benefit has no earnings exception: its work exception is " + "entitlement to working tax credit (HB Regs 2006 reg. 75E)." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + "https://www.gov.uk/benefit-cap/when-youre-not-affected", + ) + + def formula(benunit, period, parameters): + # 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) + ) + earnings_threshold = 10_152 + return uc_earned >= earnings_threshold diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py new file mode 100644 index 0000000000..4bfa81bf4b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py @@ -0,0 +1,72 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim, +) + +# Benefits whose receipt by "a claimant" (or, for some, a child or qualifying +# young person the claimant is responsible for) lifts the cap: industrial +# injuries benefit (UC Regs 2013 reg. 83(1)(b)), attendance allowance (c), +# disability living allowance (f), personal independence payment (g), carer's +# allowance (i) and carer support payment (ia). +SPECIFIED_PERSONAL_BENEFITS = [ + "attendance_allowance", + "carers_allowance", + "carer_support_payment", + "dla", + "pip_dl", + "pip_m", + "iidb", +] + + +class is_uc_benefit_cap_exempt_specified_benefit(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Universal Credit benefit cap through a specified benefit or element" + documentation = ( + "Whether the award includes the LCWRA or carer element, or a " + "claimant receives a benefit listed in UC Regs 2013 reg. 83(1): " + "employment and support allowance with the support component, " + "industrial injuries benefit, attendance allowance, a war pension or " + "armed forces compensation, disability living allowance, personal " + "independence payment, carer's allowance or carer support payment. " + "A partner who cannot be a joint claimant, so that the other member " + "claims as a single person (reg. 3(3)), is not a claimant: their own " + "benefits and limited capability lift no cap. Working tax credit, " + "which cannot be paid with Universal Credit, is Housing Benefit's " + "exception (HB Regs 2006 reg. 75E), not this one." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/83", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + "https://www.gov.uk/benefit-cap/when-youre-not-affected", + ) + + def formula(benunit, period, parameters): + person = benunit.members + # The exceptions read "a claimant" (reg. 83(1)); the other member of a + # single claim is not one. + claimant_side = ~other_member_of_single_claim(person, period) + + # Reg. 83(1)(a): the LCWRA element is included in the award, or the + # claimant receives an employment and support allowance that includes + # the support component (contributory or income-related allowances + # stand in for it). + lcwra = benunit.any( + person("uc_limited_capability_for_WRA", period) & claimant_side + ) + esa_support_component = (benunit("esa_income", period) > 0) | ( + add_for_members(benunit, period, ["esa_contrib"], claimant_side) > 0 + ) + # Reg. 83(1)(j): the carer element is included in the award. + carer_element = benunit("uc_carer_element", period) > 0 + specified_benefits = ( + add_for_members(benunit, period, SPECIFIED_PERSONAL_BENEFITS, claimant_side) + > 0 + ) + # Reg. 83(1)(d)-(e): a war pension, which includes Armed Forces + # Compensation Scheme payments. + afcs = add_for_members(benunit, period, ["afcs"], claimant_side) > 0 + + return lcwra | esa_support_component | carer_element | specified_benefits | afcs diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_single_claimant_rate.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_single_claimant_rate.py new file mode 100644 index 0000000000..4303ae1f24 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_single_claimant_rate.py @@ -0,0 +1,37 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.benefit_unit import has_sixteen_year_old_dependant + + +class is_uc_benefit_cap_single_claimant_rate(Variable): + value_type = bool + entity = BenUnit + label = "Universal Credit benefit cap single claimant rate applies" + documentation = ( + "Whether the Universal Credit claimant is a single claimant who is " + "not responsible for a child or qualifying young person (UC Regs 2013 " + "reg. 80A(2)(a) and (c)). Joint claimants, and a single claimant " + "responsible for a child or qualifying young person, have the other " + "rate. A member of a couple who claims as a single person because the " + "other member cannot be a joint claimant (reg. 3(3)) is a single " + "claimant (ADM E5007 note 3), although the welfare benefits capped " + "are still the couple's (regs. 78(2) and 79(1)). A 16-year-old member " + "counts as a qualifying young person for the year (reg. 5(1)(a))." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/80A", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/78", + ) + + def formula(benunit, period, parameters): + single_claimant = ~benunit("is_couple", period) | benunit( + "uc_member_of_couple_claims_as_single_person", period + ) + responsible = benunit( + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", + period, + ) | has_sixteen_year_old_dependant(benunit, period) + return single_claimant & ~responsible diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap.py new file mode 100644 index 0000000000..7244045032 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.benefit_cap import benefit_cap_annual_limit + + +class uc_benefit_cap(Variable): + value_type = float + entity = BenUnit + label = "Universal Credit benefit cap" + documentation = ( + "The applicable annual limit on the welfare benefits of a Universal " + "Credit claimant or couple (UC Regs 2013 reg. 80A(2)), or infinity " + "where regulation 82 or 83 lifts the cap (reg. 79(1))." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/80A", + ) + + def formula(benunit, period, parameters): + limit = benefit_cap_annual_limit( + benunit, + period, + parameters, + benunit("is_uc_benefit_cap_single_claimant_rate", period), + ) + exempt = benunit("is_uc_benefit_cap_exempt", period) + return where(exempt, np.inf, limit) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py new file mode 100644 index 0000000000..47fa993130 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -0,0 +1,32 @@ +from policyengine_uk.model_api import * + + +class uc_benefit_cap_reduction(Variable): + value_type = float + entity = BenUnit + label = "Universal Credit benefit cap reduction" + documentation = ( + "The reduction the benefit cap makes to a Universal Credit award: the " + "excess of the welfare benefits of the single person or couple over " + "the Universal Credit cap, minus the childcare costs element, and " + "nothing where the childcare costs element is greater than the " + "excess (UC Regs 2013 reg. 81). The cap reduces an award of Universal " + "Credit (reg. 78(1)), so there is no reduction without one." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/78", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/81", + ) + + def formula(benunit, period, parameters): + excess = max_( + benunit("benefit_cap_welfare_benefits", period) + - benunit("uc_benefit_cap", period), + 0, + ) + reduction = max_(excess - benunit("uc_childcare_element", period), 0) + has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 + return where(has_award, reduction, 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py index 0ef750fec5..0daf7d74ab 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_deductions.py @@ -26,7 +26,7 @@ def formula(benunit, period, parameters): standard_allowance = benunit("uc_standard_allowance", period) award = max_( benunit("universal_credit_pre_benefit_cap", period) - - benunit("benefit_cap_reduction", period), + - benunit("uc_benefit_cap_reduction", period), 0, ) minimum_payable = p.minimum_payable * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_has_deduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_has_deduction.py index 10af0ade40..332f99b1cb 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_has_deduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/deductions/uc_has_deduction.py @@ -36,7 +36,7 @@ def formula(benunit, period, parameters): adjusted_incidence = clip(incidence * p.region_incidence_factor[region], 0, 1) award = max_( benunit("universal_credit_pre_benefit_cap", period) - - benunit("benefit_cap_reduction", period), + - benunit("uc_benefit_cap_reduction", period), 0, ) on_uc = benunit("would_claim_uc", period) & (award > 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py index f30621334a..b7d2c0eb91 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py @@ -11,7 +11,7 @@ class universal_credit(Variable): def formula(benunit, period, parameters): uc_max_entitlement = benunit("universal_credit_pre_benefit_cap", period) - benefit_cap_reduction = benunit("benefit_cap_reduction", period) + benefit_cap_reduction = benunit("uc_benefit_cap_reduction", period) deductions = benunit("uc_deductions", period) # A protected minimum floor (zero under current law) caps combined # benefit cap reductions and deductions at (1 - floor) x the standard From e0d8b245b7cd86cdfca53b37c3fd041d2b908fee Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 13:33:19 -0400 Subject: [PATCH 02/11] Add hand-computed YAML tests for each scheme's cap exceptions, reductions and LHA category Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_lha_category.yaml | 188 ++++++++++++ .../gov/dwp/benefit_cap_by_scheme.yaml | 286 ++++++++++++++++++ 2 files changed, 474 insertions(+) create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml new file mode 100644 index 0000000000..8cb2de6a55 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml @@ -0,0 +1,188 @@ +# Housing Benefit's own LHA category (HB Regs 2006 reg 13D(2)), beside +# Universal Credit's (UC Regs 2013 Sch 4 paras 27-29, LHA_category). +# +# HB reg 13D(2)(a)(i): shared accommodation where "the claimant is a young +# individual who has no non-dependant residing with him and to whom paragraph +# 14 of Schedule 3 (severe disability premium) does not apply". Reg 2(1): +# "young individual" means a single claimant under 35 (with exceptions); +# "single claimant" means a claimant who neither has a partner nor is a lone +# parent. Sch 3 para 14(2)(a): a single claimant is severely disabled if (i) +# in receipt of attendance allowance, DLA care at the highest or middle rate, +# the PIP daily living component or armed forces independence payment +# (among others), (ii) no non-dependant aged 18 or over resides with them, +# and (iii) no one receives carer's allowance for caring for them (not +# observed). Para 14(2)(b): for a couple, both must qualify; para 14(4): a +# non-dependant who receives a qualifying benefit is disregarded. +# UC Sch 4 para 29(5)(b): attendance allowance, which UC reg 2 defines to +# include armed forces independence payment. + +- name: A renter under 35 receiving armed forces independence payment is excepted from both schemes' shared rate + period: 2025 + input: + age: 30 + armed_forces_independence_payment: 6_000 + output: + # UC Sch 4 para 29(5)(b) with reg 2: one bedroom, B. + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B + # HB: a young individual to whom Sch 3 para 14 applies, so not 13D(2)(a)(i). + is_housing_benefit_young_individual: true + housing_benefit_severe_disability_premium_applies: true + housing_benefit_LHA_category: B + +- name: A single claimant under 35 with no qualifying benefit gets the HB shared rate + period: 2025 + input: + age: 30 + output: + is_housing_benefit_young_individual: true + housing_benefit_severe_disability_premium_applies: false + housing_benefit_LHA_category: A + +- name: A single claimant aged 35 is not a young individual + period: 2025 + input: + age: 35 + output: + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B + +- name: Sch 3 para 14(2)(b) - a couple meets the severe disability premium condition only if both qualify + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + attendance_allowance: 4_000 + partner: + age: 30 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + housing_benefit_severe_disability_premium_applies: false + # A couple is never a young individual: one bedroom, B. + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B + +- name: Sch 3 para 14(2)(b) - both members of a couple qualifying meets the condition + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + attendance_allowance: 4_000 + partner: + age: 30 + is_claimant_or_partner: true + pip_dl: 3_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + housing_benefit_severe_disability_premium_applies: true + +- name: Sch 3 para 14(2)(a)(ii) - an adult non-dependant in the family defeats the severe disability premium + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + attendance_allowance: 4_000 + adult: + age: 25 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, adult] + households: + household: + members: [claimant, adult] + output: + housing_benefit_severe_disability_premium_applies: false + +- name: Sch 3 para 14(4)(a) - a non-dependant receiving a qualifying benefit is disregarded + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + attendance_allowance: 4_000 + adult: + age: 25 + is_claimant_or_partner: false + pip_dl: 3_000 + benunits: + benunit: + members: [claimant, adult] + households: + household: + members: [claimant, adult] + output: + housing_benefit_severe_disability_premium_applies: true + +- name: Another benefit unit's adult claimant in the household defeats the severe disability premium + period: 2025 + input: + people: + claimant: + age: 30 + attendance_allowance: 4_000 + lodger: + age: 40 + benunits: + claimant_unit: + members: [claimant] + lodger_unit: + members: [lodger] + households: + household: + members: [claimant, lodger] + output: + # The household composition proxy treats another family's claimant as a + # non-dependant, as lha_renter_has_non_dependant does. + housing_benefit_severe_disability_premium_applies: [false, false] + +- name: An HB young person who is not a UC qualifying young person blocks both shared rates + # The 19-year-old started non-advanced education at 17 and is past UC's + # terminal date, so is a Child Benefit (HB reg 19) young person but not a + # UC qualifying young person (UC reg 5). For UC the parent is responsible + # for no child or qualifying young person, but the 19-year-old is a + # non-dependant (Sch 4 para 9), so the parent is not a specified renter + # (para 28(4)). For HB the parent is a lone parent, not a single claimant, + # so not a young individual (reg 2(1)). One bedroom either way: B. + period: 2025 + input: + people: + parent: + age: 30 + is_parent: true + student: + age: 19 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + benunits: + benunit: + members: [parent, student] + households: + household: + members: [parent, student] + output: + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_responsible_for_child_or_young_person_for_legacy_benefits: true + is_lha_shared_accommodation_rate_specified_renter: false + is_housing_benefit_young_individual: false + LHA_category: B + housing_benefit_LHA_category: B diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml new file mode 100644 index 0000000000..4e6acfa70e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml @@ -0,0 +1,286 @@ +# Each scheme's own benefit cap exceptions and reduction. +# +# Universal Credit (UC Regs 2013): reg 79(1) "Unless regulation 82 or 83 +# applies, the benefit cap applies"; reg 82 earnings; reg 83(1) "a claimant" +# receiving listed benefits, or the LCWRA or carer element in the award; no +# exception for age; reg 81 reduction "the excess; minus any amount included +# in the award for the childcare costs element". +# Housing Benefit (HB Regs 2006): reg 75A "Unless regulation 75E or 75F +# applies"; reg 75E(2) "the claimant is, or the claimant and the claimant's +# partner are jointly, entitled to working tax credit"; reg 75F(1) the +# claimant or partner receiving listed benefits; reg 75D(2) the reduction +# leaves the reg 75 minimum (50 pence a week); reg 5 the HB Regs 2006, and so +# the cap, do not apply where the claimant or partner has reached the +# qualifying age for state pension credit unless on UC, IS, income-based JSA +# or income-related ESA. +# Annual limits (UC reg 80A(2), HB reg 75CA(2)): single 14,753 / family +# 22,020 outside London. + +- name: UC reg 83(1)(a) - the LCWRA element lifts the UC cap; HB has no LCWRA exception + period: 2025 + input: + age: 30 + region: NORTH_EAST + uc_limited_capability_for_WRA: true + esa_income: 0 + esa_contrib: 0 + working_tax_credit: 0 + output: + is_uc_benefit_cap_exempt_specified_benefit: true + is_uc_benefit_cap_exempt: true + # HB reg 75F(1)(a) needs an employment and support allowance with the + # support component; limited capability alone is not on the list. + is_housing_benefit_benefit_cap_exempt_specified_benefit: false + is_housing_benefit_benefit_cap_exempt: false + housing_benefit_benefit_cap: 14_753 + +- name: UC reg 83(1)(j) - the carer element lifts the UC cap; HB needs carer's allowance + period: 2025 + input: + age: 30 + care_hours: 35 + carers_allowance: 0 + carer_support_payment: 0 + working_tax_credit: 0 + output: + is_uc_benefit_cap_exempt_specified_benefit: true + is_uc_benefit_cap_exempt: true + # HB reg 75F(1)(h)-(ha): entitlement to carer's allowance or carer + # support payment, which this carer does not have. + is_housing_benefit_benefit_cap_exempt: false + +- name: UC reg 83(1)(i) and HB reg 75F(1)(h) - carer's allowance lifts both caps + period: 2025 + input: + age: 30 + care_hours: 35 + carers_allowance: 4_000 + working_tax_credit: 0 + output: + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt: true + +- name: UC reg 82(1)(a) - earnings lift the UC cap; HB has no earnings exception + period: 2025 + input: + age: 30 + region: NORTH_EAST + employment_income: 15_000 + working_tax_credit: 0 + output: + # 2025-26: income tax 20% x (15,000 - 12,570) = 486; Class 1 NI 8% x + # (15,000 - 12,570) = 194.40; earned income 14,319.60, above the model's + # 10,152 a year (846 a month). + is_uc_benefit_cap_exempt_earnings: true + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt: false + housing_benefit_benefit_cap: 14_753 + +- name: HB reg 75E(2) - working tax credit lifts the HB cap; it is no UC exception + period: 2025 + input: + age: 30 + region: NORTH_EAST + working_tax_credit: 1_000 + output: + is_housing_benefit_benefit_cap_exempt_working_tax_credit: true + is_housing_benefit_benefit_cap_exempt: true + is_uc_benefit_cap_exempt: false + uc_benefit_cap: 14_753 + +- name: HB reg 5 - a pension-age claimant's HB is under the pension-age regulations, which have no cap + period: 2025 + input: + age: 70 + working_tax_credit: 0 + output: + housing_benefit_pension_age_regulations_apply: true + is_housing_benefit_benefit_cap_exempt: true + # The UC cap has no age exception (regs 79, 82 and 83). + is_uc_benefit_cap_exempt: false + +- name: UC regs 79-83 - a mixed-age couple on UC is capped; under reg 5(1)(b) so is their HB + period: 2025 + input: + people: + older: + age: 70 + is_claimant_or_partner: true + younger: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + would_claim_uc: true + working_tax_credit: 0 + households: + household: + members: [older, younger] + region: NORTH_EAST + tenure_type: RENT_PRIVATELY + rent: 9_000 + output: + is_uc_entitled: true + is_uc_benefit_cap_exempt: false + uc_benefit_cap: 22_020 + # The younger member is on UC, so the HB Regs 2006 apply (reg 5(1)(b)). + housing_benefit_pension_age_regulations_apply: false + is_housing_benefit_benefit_cap_exempt: false + +- name: HB reg 5(3) - a mixed-age couple not on UC or an income-related benefit claims under the pension-age regulations + period: 2025 + input: + people: + older: + age: 70 + is_claimant_or_partner: true + younger: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + households: + household: + members: [older, younger] + region: NORTH_EAST + output: + housing_benefit_pension_age_regulations_apply: true + is_housing_benefit_benefit_cap_exempt: true + +- name: HB reg 5(1)(b) - a pension-age claimant whose partner is on income-based JSA claims under the working-age regulations + period: 2025 + input: + people: + older: + age: 70 + is_claimant_or_partner: true + younger: + age: 50 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + working_tax_credit: 0 + income_support: 0 + jsa_income: 1_000 + esa_income: 0 + households: + household: + members: [older, younger] + region: NORTH_EAST + output: + housing_benefit_pension_age_regulations_apply: false + # Income-based JSA is not a reg 75F exception. + is_housing_benefit_benefit_cap_exempt: false + housing_benefit_benefit_cap: 22_020 + +- name: UC reg 81(1) - the reduction is the excess less the childcare costs element + period: 2025 + input: + benefit_cap_welfare_benefits: 30_000 + uc_benefit_cap: 22_020 + uc_childcare_element: 3_000 + universal_credit_pre_benefit_cap: 15_000 + housing_benefit_pre_benefit_cap: 0 + output: + # Excess 30,000 - 22,020 = 7,980; less childcare 3,000 = 4,980. + uc_benefit_cap_reduction: 4_980 + housing_benefit_benefit_cap_reduction: 0 + benefit_cap_reduction: 4_980 + +- name: UC reg 81(2) - no reduction where the childcare costs element exceeds the excess + period: 2025 + input: + benefit_cap_welfare_benefits: 30_000 + uc_benefit_cap: 22_020 + uc_childcare_element: 9_000 + universal_credit_pre_benefit_cap: 15_000 + output: + # Excess 7,980 < childcare 9,000. + uc_benefit_cap_reduction: 0 + +- name: UC reg 78(1) - the cap reduces an award of UC, so no award means no reduction + period: 2025 + input: + benefit_cap_welfare_benefits: 30_000 + uc_benefit_cap: 22_020 + uc_childcare_element: 0 + universal_credit_pre_benefit_cap: 0 + housing_benefit_pre_benefit_cap: 0 + output: + uc_benefit_cap_reduction: 0 + benefit_cap_reduction: 0 + +- name: HB reg 75D(1) - Housing Benefit is reduced by the excess + period: 2025 + input: + benefit_cap_welfare_benefits: 30_000 + housing_benefit_benefit_cap: 22_020 + housing_benefit_pre_benefit_cap: 10_000 + would_claim_housing_benefit: true + universal_credit_pre_benefit_cap: 0 + output: + # Excess 7,980 is below 10,000 - 0.50 x 52 = 9,974. + housing_benefit_benefit_cap_reduction: 7_980 + housing_benefit: 2_020 + uc_benefit_cap_reduction: 0 + benefit_cap_reduction: 7_980 + +- name: HB reg 75D(2) - the reduction leaves the reg 75 minimum of 50 pence a week + period: 2025 + input: + benefit_cap_welfare_benefits: 30_000 + housing_benefit_benefit_cap: 22_020 + housing_benefit_pre_benefit_cap: 5_000 + would_claim_housing_benefit: true + output: + # min(7,980, 5,000 - 26) = 4,974, leaving 0.50 x 52 = 26. + housing_benefit_benefit_cap_reduction: 4_974 + housing_benefit: 26 + +- name: HB reg 75D(2) - Housing Benefit already below 50 pence a week is not reduced + period: 2025 + input: + benefit_cap_welfare_benefits: 30_000 + housing_benefit_benefit_cap: 22_020 + housing_benefit_pre_benefit_cap: 20 + would_claim_housing_benefit: true + output: + # max(20 - 26, 0) = 0. Reg 75 itself (no award below 50 pence a week) is + # not modelled. + housing_benefit_benefit_cap_reduction: 0 + housing_benefit: 20 + +- name: Reg 80A(2)(a) and reg 75CA(2)(b) - in London a member of a couple claiming UC as a single person has the single UC cap and the family HB cap + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + is_uc_benefit_cap_exempt: false + is_housing_benefit_benefit_cap_exempt: false + households: + household: + members: [claimant, partner] + region: LONDON + output: + uc_member_of_couple_claims_as_single_person: true + uc_benefit_cap: 16_967 + housing_benefit_benefit_cap: 25_323 From b6d61ef82c02fc377e0dbf3be170c31ca0bf0b5a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 13:40:16 -0400 Subject: [PATCH 03/11] Add differential and property tests for the split; HB tests pin the HB LHA rate; docs and changelog Co-Authored-By: Claude Opus 5.5 --- .../uc-hb-split-shared-cap-lha-rules.added.md | 1 + .../uc-hb-split-shared-cap-lha-rules.fixed.md | 1 + ...c-hb-split-shared-cap-lha-rules.removed.md | 1 + .../programs/gov/dwp/housing-benefit.ipynb | 4 +- .../programs/gov/dwp/universal-credit.ipynb | 2 +- docs/book/usage/scenarios.md | 4 +- .../housing_benefit_lha_order.yaml | 2 +- .../housing_benefit_pension_age_claims.yaml | 4 +- ...benefit_cap_lha_scheme_split_properties.py | 823 ++++++++++++++++++ ...st_housing_benefit_lha_order_properties.py | 12 +- 10 files changed, 840 insertions(+), 14 deletions(-) create mode 100644 changelog.d/uc-hb-split-shared-cap-lha-rules.added.md create mode 100644 changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md create mode 100644 changelog.d/uc-hb-split-shared-cap-lha-rules.removed.md create mode 100644 policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py diff --git a/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md b/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md new file mode 100644 index 0000000000..4c627c405d --- /dev/null +++ b/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md @@ -0,0 +1 @@ +Per-scheme benefit cap variables (`uc_benefit_cap`, `is_uc_benefit_cap_single_claimant_rate`, `is_uc_benefit_cap_exempt`, `is_uc_benefit_cap_exempt_earnings`, `is_uc_benefit_cap_exempt_specified_benefit`, `uc_benefit_cap_reduction`; `housing_benefit_benefit_cap`, `is_housing_benefit_benefit_cap_single_claimant_rate`, `is_housing_benefit_benefit_cap_exempt`, `is_housing_benefit_benefit_cap_exempt_working_tax_credit`, `is_housing_benefit_benefit_cap_exempt_specified_benefit`, `housing_benefit_benefit_cap_reduction`, `housing_benefit_pension_age_regulations_apply`; `benefit_cap_welfare_benefits`) and Housing Benefit LHA variables (`housing_benefit_LHA_category`, `housing_benefit_LHA_rate`, `is_housing_benefit_young_individual`, `housing_benefit_severe_disability_premium_applies`), with the parameters `gov.dwp.housing_benefit.minimum_weekly_amount` and `gov.dwp.housing_benefit.severe_disability_premium.qualifying_benefits`. diff --git a/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md b/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md new file mode 100644 index 0000000000..69fbf98252 --- /dev/null +++ b/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md @@ -0,0 +1 @@ +Universal Credit and Housing Benefit each apply their own benefit cap and LHA shared accommodation rules. The UC cap (UC Regs 2013 regs 79-83) treats a member of a couple claiming as a single person (reg 3(3)) as a single claimant, lifts the cap for earnings and the LCWRA and carer elements but not for working tax credit or age, and takes the childcare costs element off the reduction (reg 81). The HB cap (HB Regs 2006 Part 8A) treats that couple as a couple, lifts the cap for working tax credit and the benefits in reg 75F (the partner's included) but not for earnings or UC elements, does not reach pension-age claims (reg 5), and leaves at least 50 pence a week of HB (reg 75D(2)). The UC LHA category (`LHA_category`) and a new HB one (`housing_benefit_LHA_category`, HB reg 13D(2)) each apply their scheme's shared accommodation test, and armed forces independence payment now excepts a renter under 35 from both. The `claims_legacy_benefits` stopgap for reg 3(3) single claims is gone. diff --git a/changelog.d/uc-hb-split-shared-cap-lha-rules.removed.md b/changelog.d/uc-hb-split-shared-cap-lha-rules.removed.md new file mode 100644 index 0000000000..b935fe8632 --- /dev/null +++ b/changelog.d/uc-hb-split-shared-cap-lha-rules.removed.md @@ -0,0 +1 @@ +The shared benefit cap variables `benefit_cap`, `is_benefit_cap_exempt`, `is_benefit_cap_exempt_earnings`, `is_benefit_cap_exempt_health_disability`, `is_benefit_cap_exempt_other` and `is_benefit_cap_single_claimant_rate`, replaced by each scheme's own. `benefit_cap_reduction` remains as the sum of the Universal Credit and Housing Benefit reductions. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 3fa1f88ec0..d5dd1a86f2 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -38,10 +38,10 @@ "\n", "The award is computed in four stages:\n", "\n", - "1. **Eligible rent**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\n", + "1. **Eligible rent**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate for the Housing Benefit category of dwelling (`housing_benefit_LHA_category`: the shared accommodation rate for a young individual with no non-dependant to whom the severe disability premium does not apply, reg 13D(2)) or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", - "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `benefit_cap_reduction` if the household's total benefit income exceeds the cap and no cap exemption applies.\n", + "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `housing_benefit_benefit_cap_reduction`, the excess of the family's welfare benefits over the Housing Benefit cap (`housing_benefit_benefit_cap`, SI 2006/213 reg 75CA), leaving at least 50 pence a week (reg 75D). The cap does not apply where the claimant or couple is entitled to working tax credit (reg 75E), the claimant or partner receives a benefit listed in reg 75F, or the claim is under the pension-age regulations (reg 5). Universal Credit has its own cap.\n", "\n", "The take-up gate (`would_claim_housing_benefit`) is applied inside `housing_benefit_pre_benefit_cap`." ] diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index f7737bf061..e304787d48 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -44,7 +44,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): for example one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where the nomination is not supplied the model takes the claimant who works fewer hours. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or 30 for the responsible carer of a child under 13; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, and nothing for a partner with no work-related requirements for a reason other than earnings. The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the benunit's combined earnings above its work allowance (`uc_work_allowance`), and by all of its unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, savings, dividend and property income, and the tariff income deemed from capital (reg 72), which replaces the actual yield of the capital it is charged on. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." + "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): for example one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where the nomination is not supplied the model takes the claimant who works fewer hours. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or 30 for the responsible carer of a child under 13; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, and nothing for a partner with no work-related requirements for a reason other than earnings. The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the benunit's combined earnings above its work allowance (`uc_work_allowance`), and by all of its unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, savings, dividend and property income, and the tariff income deemed from capital (reg 72), which replaces the actual yield of the capital it is charged on. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `uc_benefit_cap_reduction` to produce `universal_credit`: the excess of the family's welfare benefits over the Universal Credit cap (`uc_benefit_cap`, UC Regulations 2013 reg 80A), less the childcare costs element (reg 81). The cap does not apply where earnings reach the reg 82 threshold or a claimant receives a benefit or element listed in reg 83 (`is_uc_benefit_cap_exempt`). A member of a couple claiming as a single person (reg 3(3)) has the single-claimant limit. Housing Benefit has its own cap.\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." }, { "cell_type": "code", diff --git a/docs/book/usage/scenarios.md b/docs/book/usage/scenarios.md index 026b624173..e302d6fe9d 100644 --- a/docs/book/usage/scenarios.md +++ b/docs/book/usage/scenarios.md @@ -270,8 +270,8 @@ baseline_sim = Simulation(situation=benefit_cap_family) reformed_sim = Simulation(situation=benefit_cap_family, scenario=reindex_benefit_cap) # Compare the benefit cap levels -baseline_cap = baseline_sim.calculate("benefit_cap", 2026).mean() -reformed_cap = reformed_sim.calculate("benefit_cap", 2026).mean() +baseline_cap = baseline_sim.calculate("uc_benefit_cap", 2026).mean() +reformed_cap = reformed_sim.calculate("uc_benefit_cap", 2026).mean() print( f"Benefit cap - frozen: £{baseline_cap:.0f}/year, indexed: £{reformed_cap:.0f}/year" diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml index 4714f22593..bf5af7f3f7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/entitlement/housing_benefit_lha_order.yaml @@ -43,7 +43,7 @@ # The model's 2026 rate for this household (default BRMA, Maidstone, # one-bedroom category) is also 9,467.64; it is an input here so the # test pins the order of the cap and the taper, not the LHA table. - BRMA_LHA_rate: 9_467.64 + housing_benefit_LHA_rate: 9_467.64 # Inputs, so the earnings disregard and the pension-age allowance # cannot move the taper. The applicable income input overrides the # income computed from state_pension_reported, which only keeps diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml index 394d7ba16e..80544aada7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml @@ -40,7 +40,7 @@ benunits: benunit: members: [older, younger] - BRMA_LHA_rate: 8_000 + housing_benefit_LHA_rate: 8_000 households: household: members: [older, younger] @@ -62,7 +62,7 @@ benunits: benunit: members: [person] - BRMA_LHA_rate: 12_000 + housing_benefit_LHA_rate: 12_000 housing_benefit_applicable_income: 20_000 households: household: diff --git a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py new file mode 100644 index 0000000000..d169f0c5ac --- /dev/null +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -0,0 +1,823 @@ +"""Differential and property tests for splitting the benefit cap and the LHA +shared accommodation rate between Universal Credit and Housing Benefit. + +Until this change one set of variables served both schemes: the cap rate +(``is_benefit_cap_single_claimant_rate``), the exceptions +(``is_benefit_cap_exempt_health_disability``, ``_other``, ``_earnings``), +the cap and its reduction (``benefit_cap``, ``benefit_cap_reduction``) and +the shared accommodation test behind ``LHA_category``. A Universal Credit +claim by a member of a couple as a single person (UC Regs 2013 reg. 3(3)) +followed the Universal Credit rule unless the family claimed legacy +benefits. ``REFERENCE`` re-adds those formulas, verbatim from the parent +commit, under ``reference_`` names, so one simulation computes the old and +the new values for the same families. + +Invariants, each over generated families and the explicit examples: + +1. Cap rate. Where the two schemes' rates agree, the old shared rate is + that rate. Where they differ, the old rate was the scheme the family + claimed under for a reg. 3(3) single claim (legacy: Housing Benefit; + otherwise Universal Credit), and otherwise the family rate whenever + either scheme gave it (the old test took responsibility under either). +2. Exceptions. With nobody at State Pension age: where the schemes agree, + the old exception is theirs; with no reg. 3(3) single claim, the old + exception is exactly the union of the two schemes' (it listed both + schemes' grounds). The age cases are intended changes (the UC cap has + no age exception; HB Regs 2006 reg. 5) and are left to the YAML tests. +3. LHA category. Without armed forces independence payment (added to the + exceptions of both schemes here), the old category is the new category + of the scheme the family claims under (legacy: Housing Benefit, + otherwise Universal Credit), and with no reg. 3(3) single claim the two + new categories are equal. +4. Reductions. 0 <= each scheme's reduction <= the excess of the welfare + benefits over that scheme's cap; the UC reduction is the excess less + the childcare costs element (reg. 81); Housing Benefit after the cap + keeps at least min(its amount before the cap, 50 pence a week) (reg. + 75D(2)); ``benefit_cap_reduction`` is the sum of the two; no family has + both awards before the cap. +5. Awards. Where a scheme's cap equals the old cap and neither reg. 81's + childcare offset nor reg. 75D(2)'s minimum applies, its award equals the + award under the old shared cap. +6. The maximum rent (LHA) is the rent capped at the Housing Benefit LHA + rate, which equals the Universal Credit category's weekly rate wherever + the two categories agree. +""" + +import copy + +import numpy as np +from hypothesis import HealthCheck, example, 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 * +from policyengine_uk.utils.benefit_unit import add_for_members +from policyengine_uk.variables.gov.dwp.LHA_category import LHACategory + +# model_api's YEAR is the period type the reference variables use. +PERIOD = 2026 +PROPERTY_SETTINGS = settings( + max_examples=12, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +# --------------------------------------------------------------------------- +# The shared rules before the split (parent commit 17b9b8a16), verbatim but +# for the names and the inlined stopgap helpers. + + +def _single_claim_in_shared_rules(benunit, period): + return benunit("uc_member_of_couple_claims_as_single_person", period) & ~benunit( + "claims_legacy_benefits", period + ) + + +def _other_member_in_shared_rules(person, period): + return ( + person("uc_is_ineligible_partner", period) + & person.benunit("uc_member_of_couple_claims_as_single_person", period) + & ~person.benunit("claims_legacy_benefits", period) + ) + + +class reference_is_benefit_cap_single_claimant_rate(Variable): + value_type = bool + entity = BenUnit + label = "Old shared benefit cap single claimant rate" + definition_period = YEAR + + def formula(benunit, period, parameters): + single_claimant = ~benunit("is_couple", period) | _single_claim_in_shared_rules( + benunit, period + ) + return single_claimant & ~benunit( + "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", + period, + ) + + +class reference_is_benefit_cap_exempt_health_disability(Variable): + value_type = bool + entity = BenUnit + label = "Old shared health and disability exception" + definition_period = YEAR + + def formula(benunit, period, parameters): + person = benunit.members + not_a_claimant = _other_member_in_shared_rules(person, period) + has_lcwra = benunit.any( + person("uc_limited_capability_for_WRA", period) & ~not_a_claimant + ) + gets_uc_carer_element = benunit("uc_carer_element", period) > 0 + qualifying_personal_benefits = add_for_members( + benunit, + period, + [ + "attendance_allowance", + "carers_allowance", + "carer_support_payment", + "dla", + "pip_dl", + "pip_m", + "iidb", + ], + ~not_a_claimant, + ) + qualifying_benunit_benefits = add( + benunit, period, ["esa_income", "working_tax_credit"] + ) + afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 + esa_support_component = ( + add_for_members(benunit, period, ["esa_contrib"], ~not_a_claimant) > 0 + ) + return ( + has_lcwra + | gets_uc_carer_element + | (qualifying_personal_benefits > 0) + | (qualifying_benunit_benefits > 0) + | afcs + | esa_support_component + ) + + +class reference_is_benefit_cap_exempt_other(Variable): + value_type = bool + entity = BenUnit + label = "Old shared other exception" + definition_period = YEAR + + def formula(benunit, period, parameters): + person = benunit.members + has_pensioner = benunit.any(person("is_SP_age", period)) + not_a_claimant = _other_member_in_shared_rules(person, period) + afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 + esa_support_component = ( + add_for_members(benunit, period, ["esa_contrib"], ~not_a_claimant) > 0 + ) + return has_pensioner | afcs | esa_support_component + + +class reference_is_benefit_cap_exempt_earnings(Variable): + value_type = bool + entity = BenUnit + label = "Old shared earnings exception" + definition_period = YEAR + + def formula(benunit, period, parameters): + 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) + ) + return uc_earned >= 10_152 + + +class reference_is_benefit_cap_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Old shared benefit cap exemption" + definition_period = YEAR + + def formula(benunit, period, parameters): + return ( + benunit("reference_is_benefit_cap_exempt_health_disability", period) + | benunit("reference_is_benefit_cap_exempt_earnings", period) + | benunit("reference_is_benefit_cap_exempt_other", period) + ) + + +class reference_benefit_cap(Variable): + value_type = float + entity = BenUnit + label = "Old shared benefit cap" + definition_period = YEAR + + def formula(benunit, period, parameters): + single_claimant = benunit( + "reference_is_benefit_cap_single_claimant_rate", period + ) + household_region = benunit.members.household("region", period) + region = benunit.value_from_first_person(household_region) + in_london = region == household_region.possible_values.LONDON + cap = parameters(period).gov.dwp.benefit_cap + rate = select( + [ + single_claimant & in_london, + single_claimant & ~in_london, + ~single_claimant & in_london, + ~single_claimant & ~in_london, + ], + [ + cap.single.in_london, + cap.single.outside_london, + cap.non_single.in_london, + cap.non_single.outside_london, + ], + ) + exempt = benunit("reference_is_benefit_cap_exempt", period) + return where(exempt, np.inf, rate) + + +class reference_benefit_cap_reduction(Variable): + value_type = float + entity = BenUnit + label = "Old shared benefit cap reduction" + definition_period = YEAR + + def formula(benunit, period, parameters): + capped = add( + benunit, + period, + [ + "child_benefit", + "child_tax_credit", + "jsa_income", + "income_support", + "esa_income", + "universal_credit_pre_benefit_cap", + "housing_benefit_pre_benefit_cap", + "jsa_contrib", + "incapacity_benefit", + "esa_contrib", + "sda", + ], + ) + return max_(capped - benunit("reference_benefit_cap", period), 0) + + +class reference_is_lha_shared_accommodation_rate_specified_renter(Variable): + value_type = bool + entity = BenUnit + label = "Old shared specified renter test" + definition_period = YEAR + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.LHA + person = benunit.members + claims_as_single_person = _single_claim_in_shared_rules(benunit, period) + other_member = _other_member_in_shared_rules(person, period) + renter = person("is_claimant_or_partner", period) & ~other_member + renter_age = benunit.max(where(renter, person("age", period), -np.inf)) + # The list before this change, without armed forces independence + # payment. + excepted_disabled_renter = benunit.any( + renter + & ( + add( + person, + period, + ["attendance_allowance", "dla_sc_middle_plus", "pip_dl"], + ) + > 0 + ) + ) + return ( + (~benunit("is_couple", period) | claims_as_single_person) + & (renter_age < p.shared_accommodation_age_threshold) + & ~benunit( + "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", + period, + ) + & ~benunit("lha_renter_has_non_dependant", period) + & ~excepted_disabled_renter + ) + + +class reference_LHA_category(Variable): + value_type = Enum + entity = BenUnit + label = "Old shared LHA category" + definition_period = YEAR + possible_values = LHACategory + default_value = LHACategory.C + + def formula(benunit, period, parameters): + num_rooms = benunit("LHA_allowed_bedrooms", period.this_year) + household = benunit.members.household + is_shared = benunit.any(household("is_shared_accommodation", period.this_year)) + can_only_claim_shared = benunit( + "reference_is_lha_shared_accommodation_rate_specified_renter", period + ) + return select( + [ + is_shared | can_only_claim_shared, + num_rooms == 1, + num_rooms == 2, + num_rooms == 3, + num_rooms > 3, + ], + [ + LHACategory.A, + LHACategory.B, + LHACategory.C, + LHACategory.D, + LHACategory.E, + ], + ) + + +class reference_universal_credit(Variable): + value_type = float + entity = BenUnit + label = "Universal Credit under the old shared cap" + definition_period = YEAR + + def formula(benunit, period, parameters): + return max_( + benunit("universal_credit_pre_benefit_cap", period) + - benunit("reference_benefit_cap_reduction", period) + - benunit("uc_deductions", period), + 0, + ) + + +class reference_housing_benefit(Variable): + value_type = float + entity = BenUnit + label = "Housing Benefit under the old shared cap" + definition_period = YEAR + + def formula(benunit, period, parameters): + return max_( + 0, + benunit("housing_benefit_pre_benefit_cap", period) + - benunit("reference_benefit_cap_reduction", period), + ) + + +REFERENCE_VARIABLES = [ + reference_is_benefit_cap_single_claimant_rate, + reference_is_benefit_cap_exempt_health_disability, + reference_is_benefit_cap_exempt_other, + reference_is_benefit_cap_exempt_earnings, + reference_is_benefit_cap_exempt, + reference_benefit_cap, + reference_benefit_cap_reduction, + reference_is_lha_shared_accommodation_rate_specified_renter, + reference_LHA_category, + reference_universal_credit, + reference_housing_benefit, +] + + +class REFERENCE(Reform): + def apply(self): + for variable in REFERENCE_VARIABLES: + self.add_variable(variable) + + +# --------------------------------------------------------------------------- +# Families. + +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +rarely = st.integers(0, 5).map(lambda n: n == 0) +sometimes_paid = st.one_of(*[st.just(0.0)] * 4, st.floats(500, 6_000)) +earnings = st.one_of(st.just(0.0), st.just(0.0), st.floats(0, 40_000)) + + +@st.composite +def adults(draw, ages): + return dict( + age=draw(ages), + employment_income=draw(earnings), + uc_limited_capability_for_WRA=draw(rarely), + care_hours=draw(st.sampled_from([0, 0, 0, 0, 35])), + attendance_allowance=draw(sometimes_paid), + pip_dl=draw(sometimes_paid), + carers_allowance=draw(sometimes_paid), + afcs=draw(sometimes_paid), + esa_contrib=draw(sometimes_paid), + armed_forces_independence_payment=draw( + st.one_of(*[st.just(0.0)] * 6, st.just(6_000.0)) + ), + ) + + +@st.composite +def families(draw): + has_partner = draw(st.booleans()) + return dict( + claimant=draw(adults(st.integers(18, 75))), + partner=draw(adults(st.integers(16, 80))) if has_partner else None, + partner_flag=draw(st.sampled_from([None, None, True, False])), + children=[ + dict( + age=draw(st.integers(0, 19)), + in_education=draw(st.booleans()), + ) + for _ in range(draw(st.integers(0, 3))) + ], + legacy=draw(st.booleans()), + working_tax_credit=draw(st.one_of(*[st.just(0.0)] * 4, st.just(1_500.0))), + childcare=draw(st.one_of(*[st.just(0.0)] * 3, st.floats(500, 8_000))), + rent=draw(st.floats(3_000, 30_000)), + region=draw(st.sampled_from(REGIONS)), + lodger=draw(rarely), + ) + + +populations = st.lists(families(), min_size=1, max_size=6) + + +def adult_inputs(age, **inputs): + values = dict( + age=age, + employment_income=0.0, + uc_limited_capability_for_WRA=False, + care_hours=0, + attendance_allowance=0.0, + pip_dl=0.0, + carers_allowance=0.0, + afcs=0.0, + esa_contrib=0.0, + armed_forces_independence_payment=0.0, + ) + values.update(inputs) + return values + + +def family(claimant, partner=None, **kw): + values = dict( + claimant=claimant, + partner=partner, + partner_flag=None, + children=[], + legacy=False, + working_tax_credit=0.0, + childcare=0.0, + rent=12_000.0, + region="NORTH_EAST", + lodger=False, + ) + values.update(kw) + return values + + +def example_families(): + """Families in which each scheme difference occurs.""" + return [ + # A reg. 3(3) single claim, Universal Credit (partner 17, no reg. 8 + # circumstance): single for the UC cap and shared rate. + family(adult_inputs(24), adult_inputs(17), rent=8_000.0), + # The same couple claiming legacy benefits. + family(adult_inputs(24), adult_inputs(17), legacy=True, rent=8_000.0), + # A large family on UC above the cap, with childcare costs (reg. 81). + family( + adult_inputs(30, employment_income=3_000.0), + adult_inputs(30), + children=[dict(age=a, in_education=False) for a in (1, 3, 5)], + childcare=4_000.0, + rent=24_000.0, + region="LONDON", + ), + # A lone parent with an HB-only young person (19, started at 17). + family( + adult_inputs(30), + children=[dict(age=19, in_education=True)], + rent=9_000.0, + ), + # A single renter under 35 receiving AFIP. + family(adult_inputs(28, armed_forces_independence_payment=6_000.0)), + # A mixed-age couple on UC with no other ground. + family(adult_inputs(70), adult_inputs(40)), + # A single claimant with LCWRA continuing a Housing Benefit award. + family(adult_inputs(30, uc_limited_capability_for_WRA=True), legacy=True), + ] + + +EXAMPLE_FAMILIES = example_families() + + +def situation(population, year=PERIOD): + people, benunits, households, lodger_units = {}, {}, {}, {} + for i, f in enumerate(population): + members = [] + adults_in = [("claimant", f["claimant"])] + if f["partner"] is not None: + adults_in.append(("partner", f["partner"])) + for role, inputs in adults_in: + name = f"{role}{i}" + person = {k: {year: v} for k, v in inputs.items()} + person["is_claimant_or_partner"] = {year: True} + person["would_claim_marriage_allowance"] = {year: False} + if role == "partner" and f["partner_flag"] is not None: + person["uc_is_ineligible_partner"] = {year: f["partner_flag"]} + people[name] = person + members.append(name) + for k, child in enumerate(f["children"]): + name = f"child{i}_{k}" + people[name] = { + "age": {year: child["age"]}, + "is_claimant_or_partner": {year: False}, + "current_education": { + year: "UPPER_SECONDARY" + if child["in_education"] + else "NOT_IN_EDUCATION" + }, + } + if child["age"] == 19 and child["in_education"]: + # Started at 17 and past UC's terminal date: a Child Benefit + # (HB) young person but not a UC qualifying young person. + people[name][ + "age_started_or_accepted_current_education_or_training" + ] = {year: 17} + people[name][ + "is_before_universal_credit_qualifying_young_person_terminal_date" + ] = {year: False} + members.append(name) + benunit = { + "members": list(members), + "would_claim_uc": {year: not f["legacy"]}, + "working_tax_credit": {year: f["working_tax_credit"]}, + } + if f["childcare"]: + benunit["uc_childcare_element"] = {year: f["childcare"]} + benunits[f"b{i}"] = benunit + household_members = list(members) + if f["lodger"]: + name = f"lodger{i}" + people[name] = {"age": {year: 45}} + lodger_units[f"lodger_unit{i}"] = {"members": [name]} + household_members.append(name) + if f["legacy"]: + # A continuing Housing Benefit award (SI 2014/1230 reg. 8). + people[f"claimant{i}"]["housing_benefit_reported"] = {year: 3_000.0} + households[f"h{i}"] = { + "members": household_members, + "rent": {year: f["rent"]}, + "tenure_type": {year: "RENT_PRIVATELY"}, + "region": {year: f["region"]}, + } + benunits.update(lodger_units) + return {"people": people, "benunits": benunits, "households": households} + + +BENUNIT_VARIABLES = [ + "is_couple", + "claims_legacy_benefits", + "uc_member_of_couple_claims_as_single_person", + "is_uc_benefit_cap_single_claimant_rate", + "is_housing_benefit_benefit_cap_single_claimant_rate", + "reference_is_benefit_cap_single_claimant_rate", + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", + "is_responsible_for_child_or_young_person_for_legacy_benefits", + "is_uc_benefit_cap_exempt", + "is_housing_benefit_benefit_cap_exempt", + "reference_is_benefit_cap_exempt", + "uc_benefit_cap", + "housing_benefit_benefit_cap", + "reference_benefit_cap", + "benefit_cap_welfare_benefits", + "uc_benefit_cap_reduction", + "housing_benefit_benefit_cap_reduction", + "benefit_cap_reduction", + "reference_benefit_cap_reduction", + "uc_childcare_element", + "universal_credit_pre_benefit_cap", + "housing_benefit_pre_benefit_cap", + "universal_credit", + "housing_benefit", + "reference_universal_credit", + "reference_housing_benefit", + "benunit_rent", + "LHA_cap", + "housing_benefit_LHA_rate", + "BRMA_LHA_rate", +] +CATEGORIES = ["LHA_category", "housing_benefit_LHA_category", "reference_LHA_category"] + + +def calculate(population): + sim = Simulation(situation=situation(population), reform=REFERENCE) + values = {v: np.asarray(sim.calculate(v, PERIOD)) for v in BENUNIT_VARIABLES} + for v in CATEGORIES: + values[v] = np.asarray(sim.calculate(v, PERIOD)).astype(str) + sp_age = np.asarray(sim.calculate("is_SP_age", PERIOD, map_to="benunit")) + values["anyone_sp_age"] = sp_age > 0 + afip = np.asarray( + sim.calculate("armed_forces_independence_payment", PERIOD, map_to="benunit") + ) + values["afip"] = afip > 0 + values["wtc_exempt"] = np.asarray( + sim.calculate( + "is_housing_benefit_benefit_cap_exempt_working_tax_credit", PERIOD + ) + ).astype(bool) + return values + + +# --------------------------------------------------------------------------- +# Properties. + + +@PROPERTY_SETTINGS +@given(population=populations) +@example(population=example_families()) +def test_cap_rate_is_the_old_rate_where_the_schemes_agree(population): + v = calculate(population) + uc, hb, ref = ( + v["is_uc_benefit_cap_single_claimant_rate"], + v["is_housing_benefit_benefit_cap_single_claimant_rate"], + v["reference_is_benefit_cap_single_claimant_rate"], + ) + agree = uc == hb + np.testing.assert_array_equal(ref[agree], uc[agree], err_msg=str(population)) + single_claim = v["uc_member_of_couple_claims_as_single_person"].astype(bool) + legacy = v["claims_legacy_benefits"].astype(bool) + same_responsibility = ( + v["is_responsible_for_child_or_qualifying_young_person_for_universal_credit"] + == v["is_responsible_for_child_or_young_person_for_legacy_benefits"] + ) + # A reg. 3(3) single claim with no responsibility difference: the old + # rate was the claimed scheme's. + stopgap = ~agree & single_claim & same_responsibility + np.testing.assert_array_equal( + ref[stopgap], np.where(legacy, hb, uc)[stopgap], err_msg=str(population) + ) + # A responsibility difference: the old test gave the family rate if + # either scheme did. + union = ~agree & ~single_claim + np.testing.assert_array_equal(ref[union], (uc & hb)[union], err_msg=str(population)) + # HB has no single claim by a member of a couple (HB Regs 2006 reg. 2(1)). + assert not np.any(hb[v["is_couple"].astype(bool)]), population + + +@PROPERTY_SETTINGS +@given(population=populations) +@example(population=example_families()) +def test_exceptions_are_the_old_exception_where_the_schemes_agree(population): + v = calculate(population) + uc, hb, ref = ( + v["is_uc_benefit_cap_exempt"].astype(bool), + v["is_housing_benefit_benefit_cap_exempt"].astype(bool), + v["reference_is_benefit_cap_exempt"].astype(bool), + ) + working_age = ~v["anyone_sp_age"] + single_claim = v["uc_member_of_couple_claims_as_single_person"].astype(bool) + legacy = v["claims_legacy_benefits"].astype(bool) + # The old exception listed both schemes' grounds. + union = working_age & ~single_claim + np.testing.assert_array_equal(ref[union], (uc | hb)[union], err_msg=str(population)) + agree = union & (uc == hb) + np.testing.assert_array_equal(ref[agree], uc[agree], err_msg=str(population)) + # A reg. 3(3) single claim outside legacy benefits: the old exception + # left out the other member as UC does, and added working tax credit. + uc_single_claim = working_age & single_claim & ~legacy + np.testing.assert_array_equal( + ref[uc_single_claim], + (uc | v["wtc_exempt"])[uc_single_claim], + err_msg=str(population), + ) + + +@PROPERTY_SETTINGS +@given(population=populations) +@example(population=example_families()) +def test_lha_category_is_the_claimed_schemes_old_category(population): + v = calculate(population) + uc, hb, ref = ( + v["LHA_category"], + v["housing_benefit_LHA_category"], + v["reference_LHA_category"], + ) + legacy = v["claims_legacy_benefits"].astype(bool) + no_afip = ~v["afip"] + claimed = np.where(legacy, hb, uc) + np.testing.assert_array_equal( + ref[no_afip], claimed[no_afip], err_msg=str(population) + ) + no_single_claim = ~v["uc_member_of_couple_claims_as_single_person"].astype(bool) + np.testing.assert_array_equal( + uc[no_single_claim], hb[no_single_claim], err_msg=str(population) + ) + + +@PROPERTY_SETTINGS +@given(population=populations) +@example(population=example_families()) +def test_each_scheme_reduces_by_its_own_excess(population): + v = calculate(population) + welfare = v["benefit_cap_welfare_benefits"] + uc_excess = np.maximum(welfare - v["uc_benefit_cap"], 0) + hb_excess = np.maximum(welfare - v["housing_benefit_benefit_cap"], 0) + uc_red, hb_red = ( + v["uc_benefit_cap_reduction"], + v["housing_benefit_benefit_cap_reduction"], + ) + on_uc = v["universal_credit_pre_benefit_cap"] > 0 + hb_pre = v["housing_benefit_pre_benefit_cap"] + # UC Regs 2013 reg. 81: the excess less the childcare costs element, on + # an award only. + np.testing.assert_allclose( + uc_red, + np.where(on_uc, np.maximum(uc_excess - v["uc_childcare_element"], 0), 0), + atol=0.01, + err_msg=str(population), + ) + assert np.all((0 <= hb_red) & (hb_red <= hb_excess + 0.01)), population + # HB Regs 2006 reg. 75D(2) with reg. 75: at least 50 pence a week stays. + minimum = 0.5 * 52 + assert np.all(v["housing_benefit"] >= np.minimum(hb_pre, minimum) - 0.01), ( + population + ) + assert np.all(v["housing_benefit"] <= hb_pre + 0.01), population + np.testing.assert_allclose( + v["benefit_cap_reduction"], uc_red + hb_red, atol=0.01, err_msg=str(population) + ) + assert not np.any(on_uc & (hb_pre > 0)), population + + +@PROPERTY_SETTINGS +@given(population=populations) +@example(population=example_families()) +def test_awards_are_the_old_awards_where_the_caps_agree(population): + v = calculate(population) + old_cap = v["reference_benefit_cap"] + uc_same = ( + np.isclose(v["uc_benefit_cap"], old_cap, equal_nan=False) + | (np.isinf(v["uc_benefit_cap"]) & np.isinf(old_cap)) + ) & (v["uc_childcare_element"] == 0) + np.testing.assert_allclose( + v["universal_credit"][uc_same], + v["reference_universal_credit"][uc_same], + atol=0.01, + err_msg=str(population), + ) + hb_pre = v["housing_benefit_pre_benefit_cap"] + old_reduction = v["reference_benefit_cap_reduction"] + hb_same = ( + np.isclose(v["housing_benefit_benefit_cap"], old_cap) + | (np.isinf(v["housing_benefit_benefit_cap"]) & np.isinf(old_cap)) + ) & ((old_reduction == 0) | (hb_pre - old_reduction >= 0.5 * 52)) + np.testing.assert_allclose( + v["housing_benefit"][hb_same], + v["reference_housing_benefit"][hb_same], + atol=0.01, + err_msg=str(population), + ) + + +@PROPERTY_SETTINGS +@given(population=populations) +@example(population=example_families()) +def test_maximum_rent_reads_the_housing_benefit_category(population): + v = calculate(population) + np.testing.assert_allclose( + v["LHA_cap"], + np.minimum(v["benunit_rent"], v["housing_benefit_LHA_rate"]), + atol=0.01, + err_msg=str(population), + ) + same = v["LHA_category"] == v["housing_benefit_LHA_category"] + np.testing.assert_allclose( + v["housing_benefit_LHA_rate"][same], + v["BRMA_LHA_rate"][same], + atol=0.01, + err_msg=str(population), + ) + + +def test_examples_reach_the_cases(): + """Each example changes the result it is there for.""" + v = calculate(EXAMPLE_FAMILIES) + uc_family, legacy_family, capped, lone_parent, afip, mixed_age, hb_lcwra = range(7) + # Reg. 3(3): single for UC, a couple for HB, whatever the legacy claim. + for i in (uc_family, legacy_family): + assert v["uc_member_of_couple_claims_as_single_person"][i] + assert v["is_uc_benefit_cap_single_claimant_rate"][i] + assert not v["is_housing_benefit_benefit_cap_single_claimant_rate"][i] + assert v["LHA_category"][i] == "A" + assert v["housing_benefit_LHA_category"][i] == "B" + assert v["claims_legacy_benefits"][legacy_family] + assert v["reference_LHA_category"][uc_family] == "A" + assert v["reference_LHA_category"][legacy_family] == "B" + # Reg. 81: the childcare costs element comes off the UC reduction. + excess = v["benefit_cap_welfare_benefits"][capped] - v["uc_benefit_cap"][capped] + assert excess > 0 + assert v["uc_benefit_cap_reduction"][capped] < excess + assert v["universal_credit"][capped] > v["reference_universal_credit"][capped] + # The HB-only young person: UC single claimant rate, HB lone parent. + assert v["is_uc_benefit_cap_single_claimant_rate"][lone_parent] + assert not v["is_housing_benefit_benefit_cap_single_claimant_rate"][lone_parent] + assert not v["reference_is_benefit_cap_single_claimant_rate"][lone_parent] + # AFIP: excepted from both shared rates, but not under the old list. + assert v["LHA_category"][afip] == v["housing_benefit_LHA_category"][afip] == "B" + assert v["reference_LHA_category"][afip] == "A" + # The mixed-age couple on UC: the UC cap has no age exception, and with + # the younger member on UC the HB Regs 2006 apply (reg. 5(1)(b)); the old + # shared cap exempted any family with a pensioner. + assert v["anyone_sp_age"][mixed_age] + assert v["universal_credit_pre_benefit_cap"][mixed_age] > 0 + assert not v["is_uc_benefit_cap_exempt"][mixed_age] + assert not v["is_housing_benefit_benefit_cap_exempt"][mixed_age] + assert v["reference_is_benefit_cap_exempt"][mixed_age] + # LCWRA is a UC exception (reg. 83(1)(a)), not an HB one (reg. 75F). + assert v["housing_benefit_pre_benefit_cap"][hb_lcwra] > 0 + assert v["is_uc_benefit_cap_exempt"][hb_lcwra] + assert not v["is_housing_benefit_benefit_cap_exempt"][hb_lcwra] + assert v["reference_is_benefit_cap_exempt"][hb_lcwra] diff --git a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py index a61eda1dfd..c1fbc5b853 100644 --- a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py @@ -170,7 +170,7 @@ def calculate(population): people[name] = {"age": {YEAR: 70}} benunits[f"b{i}"] = { "members": [name], - "BRMA_LHA_rate": {YEAR: unit["lha_rate"]}, + "housing_benefit_LHA_rate": {YEAR: unit["lha_rate"]}, "housing_benefit_applicable_amount": {YEAR: unit["applicable_amount"]}, "housing_benefit_applicable_income": {YEAR: unit["income"]}, "housing_benefit_non_dep_deductions": {YEAR: unit["non_dep_deductions"]}, @@ -188,7 +188,7 @@ def calculate(population): for variable in [ "housing_benefit_entitlement", "benunit_rent", - "BRMA_LHA_rate", + "housing_benefit_LHA_rate", "LHA_cap", "LHA_eligible", "housing_benefit_applicable_amount", @@ -231,7 +231,7 @@ def test_bounds_closed_form_and_differential(population): # LHA tenants are exactly the private renters here. assert np.array_equal(lha, tenures == "RENT_PRIVATELY") assert np.allclose( - values["LHA_cap"], np.minimum(rent, values["BRMA_LHA_rate"]), atol=TOLERANCE + values["LHA_cap"], np.minimum(rent, values["housing_benefit_LHA_rate"]), atol=TOLERANCE ) # 1. Bounds. @@ -357,7 +357,7 @@ def end_to_end(population): names.append(name) benunits[f"b{i}"] = { "members": names, - "BRMA_LHA_rate": {YEAR: unit["lha_rate"]}, + "housing_benefit_LHA_rate": {YEAR: unit["lha_rate"]}, } household_members = list(names) if unit["non_dependant_earnings"] is not None: @@ -384,7 +384,7 @@ def end_to_end(population): "housing_benefit", "housing_benefit_eligible", "benunit_rent", - "BRMA_LHA_rate", + "housing_benefit_LHA_rate", "LHA_cap", "LHA_eligible", "housing_benefit_applicable_amount", @@ -424,5 +424,5 @@ def test_end_to_end_pension_age_housing_benefit(population): eligible = values["housing_benefit_eligible"].astype(bool) assert np.allclose(hb[eligible], expected[eligible], atol=TOLERANCE), population assert np.all(hb[~eligible] == 0) - lha_limit = np.minimum(rent, values["BRMA_LHA_rate"]) + lha_limit = np.minimum(rent, values["housing_benefit_LHA_rate"]) assert np.all(hb[lha] <= lha_limit[lha] + TOLERANCE), population From c7bd4c0d7360cc62042364b6996fe039fd0aed06 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 13:56:56 -0400 Subject: [PATCH 04/11] Exercise the HB reg 75D(2) minimum in the split property examples Co-Authored-By: Claude Opus 5.5 --- ...benefit_cap_lha_scheme_split_properties.py | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py index d169f0c5ac..369a3c86e5 100644 --- a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -453,6 +453,7 @@ def family(claimant, partner=None, **kw): rent=12_000.0, region="NORTH_EAST", lodger=False, + benunit_inputs={}, ) values.update(kw) return values @@ -487,6 +488,16 @@ def example_families(): family(adult_inputs(70), adult_inputs(40)), # A single claimant with LCWRA continuing a Housing Benefit award. family(adult_inputs(30, uc_limited_capability_for_WRA=True), legacy=True), + # A lone parent of five on Housing Benefit and income support whose + # excess is more than their Housing Benefit (reg. 75D(2)). + family( + adult_inputs(30), + children=[dict(age=a, in_education=False) for a in (1, 3, 5, 7, 9)], + legacy=True, + rent=20_000.0, + region="LONDON", + benunit_inputs=dict(income_support=24_000.0), + ), ] @@ -537,6 +548,8 @@ def situation(population, year=PERIOD): } if f["childcare"]: benunit["uc_childcare_element"] = {year: f["childcare"]} + for variable, value in f.get("benunit_inputs", {}).items(): + benunit[variable] = {year: value} benunits[f"b{i}"] = benunit household_members = list(members) if f["lodger"]: @@ -785,7 +798,16 @@ def test_maximum_rent_reads_the_housing_benefit_category(population): def test_examples_reach_the_cases(): """Each example changes the result it is there for.""" v = calculate(EXAMPLE_FAMILIES) - uc_family, legacy_family, capped, lone_parent, afip, mixed_age, hb_lcwra = range(7) + ( + uc_family, + legacy_family, + capped, + lone_parent, + afip, + mixed_age, + hb_lcwra, + hb_floor, + ) = range(8) # Reg. 3(3): single for UC, a couple for HB, whatever the legacy claim. for i in (uc_family, legacy_family): assert v["uc_member_of_couple_claims_as_single_person"][i] @@ -821,3 +843,14 @@ def test_examples_reach_the_cases(): assert v["is_uc_benefit_cap_exempt"][hb_lcwra] assert not v["is_housing_benefit_benefit_cap_exempt"][hb_lcwra] assert v["reference_is_benefit_cap_exempt"][hb_lcwra] + # Reg. 75D(2): the excess is more than the Housing Benefit, which keeps + # 50 pence a week; the old shared reduction took it all. + hb_pre = v["housing_benefit_pre_benefit_cap"][hb_floor] + assert hb_pre > 0 + assert ( + v["benefit_cap_welfare_benefits"][hb_floor] + - v["housing_benefit_benefit_cap"][hb_floor] + > hb_pre + ) + assert abs(v["housing_benefit"][hb_floor] - 0.5 * 52) < 0.01 + assert v["reference_housing_benefit"][hb_floor] == 0 From 12ac78526c7462a06eeabea7e2989af3d7f589de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 14:38:25 -0400 Subject: [PATCH 05/11] Format the HB LHA order property test Co-Authored-By: Claude Opus 5.5 --- .../tests/test_housing_benefit_lha_order_properties.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py index c1fbc5b853..ba3818a4f9 100644 --- a/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_lha_order_properties.py @@ -231,7 +231,9 @@ def test_bounds_closed_form_and_differential(population): # LHA tenants are exactly the private renters here. assert np.array_equal(lha, tenures == "RENT_PRIVATELY") assert np.allclose( - values["LHA_cap"], np.minimum(rent, values["housing_benefit_LHA_rate"]), atol=TOLERANCE + values["LHA_cap"], + np.minimum(rent, values["housing_benefit_LHA_rate"]), + atol=TOLERANCE, ) # 1. Bounds. From c8fd1c9d19abd3d0238e5f0b218af678201035e2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 22:51:15 -0400 Subject: [PATCH 06/11] Address review r1: registry, AFIP cap exception, bedroom-correct LHA case, verbatim reference award - programs.yaml: the Benefit Cap entry named the removed benefit_cap; it now reads benefit_cap_reduction and says each scheme has its own cap. - Armed forces independence payment lifts both caps: UC reg 83(1)(c) (reg 2's "attendance allowance") and HB reg 75F(1)(ea). - The HB-only young person LHA case supplies the two bedrooms both schemes' size criteria give (UC Sch 4 para 10(1); HB reg 13D(3)(b)): C. - reference_universal_credit is the parent formula, protected floor included; the differential excludes AFIP families (an intended change). - Document that uc_benefit_cap_reduction is the reg 81 amount, which can exceed the award; BRMA_LHA_rate is the UC category's weekly rate. Co-Authored-By: Claude Opus 5.5 --- .../uc-hb-split-shared-cap-lha-rules.fixed.md | 2 +- policyengine_uk/programs.yaml | 3 +- .../housing_benefit_lha_category.yaml | 12 +++-- .../gov/dwp/benefit_cap_by_scheme.yaml | 13 +++++ ...benefit_cap_lha_scheme_split_properties.py | 53 +++++++++++++------ .../variables/gov/dwp/BRMA_LHA_rate.py | 14 +++-- .../gov/dwp/benefit_cap_reduction.py | 5 +- ...it_benefit_cap_exempt_specified_benefit.py | 9 ++-- ...uc_benefit_cap_exempt_specified_benefit.py | 7 ++- .../benefit_cap/uc_benefit_cap_reduction.py | 5 +- 10 files changed, 91 insertions(+), 32 deletions(-) diff --git a/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md b/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md index 69fbf98252..c57809557a 100644 --- a/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md +++ b/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md @@ -1 +1 @@ -Universal Credit and Housing Benefit each apply their own benefit cap and LHA shared accommodation rules. The UC cap (UC Regs 2013 regs 79-83) treats a member of a couple claiming as a single person (reg 3(3)) as a single claimant, lifts the cap for earnings and the LCWRA and carer elements but not for working tax credit or age, and takes the childcare costs element off the reduction (reg 81). The HB cap (HB Regs 2006 Part 8A) treats that couple as a couple, lifts the cap for working tax credit and the benefits in reg 75F (the partner's included) but not for earnings or UC elements, does not reach pension-age claims (reg 5), and leaves at least 50 pence a week of HB (reg 75D(2)). The UC LHA category (`LHA_category`) and a new HB one (`housing_benefit_LHA_category`, HB reg 13D(2)) each apply their scheme's shared accommodation test, and armed forces independence payment now excepts a renter under 35 from both. The `claims_legacy_benefits` stopgap for reg 3(3) single claims is gone. +Universal Credit and Housing Benefit each apply their own benefit cap and LHA shared accommodation rules. The UC cap (UC Regs 2013 regs 79-83) treats a member of a couple claiming as a single person (reg 3(3)) as a single claimant, lifts the cap for earnings and the LCWRA and carer elements but not for working tax credit or age, and takes the childcare costs element off the reduction (reg 81). The HB cap (HB Regs 2006 Part 8A) treats that couple as a couple, lifts the cap for working tax credit and the benefits in reg 75F (the partner's included) but not for earnings or UC elements, does not reach pension-age claims (reg 5), and leaves at least 50 pence a week of HB (reg 75D(2)). The UC LHA category (`LHA_category`) and a new HB one (`housing_benefit_LHA_category`, HB reg 13D(2)) each apply their scheme's shared accommodation test, and armed forces independence payment now excepts a renter under 35 from both and lifts both caps (UC reg 83(1)(c) with reg 2; HB reg 75F(1)(ea)). The `claims_legacy_benefits` stopgap for reg 3(3) single claims is gone. diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 46d253f103..0ca05b291a 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -315,9 +315,10 @@ programs: agency: DWP status: complete coverage: UK - variable: benefit_cap + variable: benefit_cap_reduction parameter_prefix: gov.dwp verified_start_year: 2022 + notes: Universal Credit (uc_benefit_cap) and Housing Benefit (housing_benefit_benefit_cap) each apply their own cap; benefit_cap_reduction is the sum of their reductions - id: tax_credits name: Tax credits diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml index 8cb2de6a55..daaf20d1e1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml @@ -161,7 +161,12 @@ # for no child or qualifying young person, but the 19-year-old is a # non-dependant (Sch 4 para 9), so the parent is not a specified renter # (para 28(4)). For HB the parent is a lone parent, not a single claimant, - # so not a young individual (reg 2(1)). One bedroom either way: B. + # so not a young individual (reg 2(1)). Each scheme gives the parent a + # bedroom and the 19-year-old, "a non-dependant who is not a child" (UC + # Sch 4 para 10(1)) or "a person who is not a child" (HB reg 13D(3)(b)), + # another: two bedrooms, C. The model's bedroom count does not yet give + # benefit-unit members over 16 their own room (PE-UK #2010), so the test + # supplies it. period: 2025 input: people: @@ -176,6 +181,7 @@ benunits: benunit: members: [parent, student] + LHA_allowed_bedrooms: 2 households: household: members: [parent, student] @@ -184,5 +190,5 @@ is_responsible_for_child_or_young_person_for_legacy_benefits: true is_lha_shared_accommodation_rate_specified_renter: false is_housing_benefit_young_individual: false - LHA_category: B - housing_benefit_LHA_category: B + LHA_category: C + housing_benefit_LHA_category: C diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml index 4e6acfa70e..f2c6d9c7ff 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml @@ -61,6 +61,19 @@ is_housing_benefit_benefit_cap_exempt_specified_benefit: true is_housing_benefit_benefit_cap_exempt: true +- name: UC reg 83(1)(c) with reg 2, and HB reg 75F(1)(ea) - armed forces independence payment lifts both caps + period: 2025 + input: + age: 30 + region: NORTH_EAST + armed_forces_independence_payment: 6_000 + working_tax_credit: 0 + output: + is_uc_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt_specified_benefit: true + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt: true + - name: UC reg 82(1)(a) - earnings lift the UC cap; HB has no earnings exception period: 2025 input: diff --git a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py index 369a3c86e5..e912882b05 100644 --- a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -8,9 +8,12 @@ the shared accommodation test behind ``LHA_category``. A Universal Credit claim by a member of a couple as a single person (UC Regs 2013 reg. 3(3)) followed the Universal Credit rule unless the family claimed legacy -benefits. ``REFERENCE`` re-adds those formulas, verbatim from the parent -commit, under ``reference_`` names, so one simulation computes the old and -the new values for the same families. +benefits. ``REFERENCE`` re-adds those formulas from the parent commit +under ``reference_`` names, so one simulation computes the old and the new +values for the same families. They are verbatim except for the names they +read, the inlined stopgap helpers and the earnings exception's dead code. +The reference awards read the current ``uc_deductions``, which these +single-household simulations leave at nil. Invariants, each over generated families and the explicit examples: @@ -19,11 +22,13 @@ claimed under for a reg. 3(3) single claim (legacy: Housing Benefit; otherwise Universal Credit), and otherwise the family rate whenever either scheme gave it (the old test took responsibility under either). -2. Exceptions. With nobody at State Pension age: where the schemes agree, - the old exception is theirs; with no reg. 3(3) single claim, the old - exception is exactly the union of the two schemes' (it listed both - schemes' grounds). The age cases are intended changes (the UC cap has - no age exception; HB Regs 2006 reg. 5) and are left to the YAML tests. +2. Exceptions. With nobody at State Pension age and no armed forces + independence payment: where the schemes agree, the old exception is + theirs; with no reg. 3(3) single claim, the old exception is exactly the + union of the two schemes' (it listed both schemes' grounds). The age and + AFIP cases are intended changes (the UC cap has no age exception; HB + Regs 2006 reg. 5; AFIP lifts both caps under UC reg. 83(1)(c) with reg. 2 + and HB reg. 75F(1)(ea)) and are pinned in the YAML tests and examples. 3. LHA category. Without armed forces independence payment (added to the exceptions of both schemes here), the old category is the new category of the scheme the family claims under (legacy: Housing Benefit, @@ -328,12 +333,22 @@ class reference_universal_credit(Variable): definition_period = YEAR def formula(benunit, period, parameters): - return max_( - benunit("universal_credit_pre_benefit_cap", period) - - benunit("reference_benefit_cap_reduction", period) - - benunit("uc_deductions", period), - 0, + # The parent's formula, reading the old shared reduction. Deductions + # are the current uc_deductions, which read the UC reduction; the + # single-household simulations here have none (uc_has_deduction). + uc_max_entitlement = benunit("universal_credit_pre_benefit_cap", period) + benefit_cap_reduction = benunit("reference_benefit_cap_reduction", period) + deductions = benunit("uc_deductions", period) + floor_rate = parameters( + period + ).gov.dwp.universal_credit.deductions.protected_floor + total_reductions = benefit_cap_reduction + deductions + max_reductions = where( + floor_rate > 0, + (1 - floor_rate) * benunit("uc_standard_allowance", period), + np.inf, ) + return max_(uc_max_entitlement - min_(total_reductions, max_reductions), 0) class reference_housing_benefit(Variable): @@ -670,7 +685,11 @@ def test_exceptions_are_the_old_exception_where_the_schemes_agree(population): v["is_housing_benefit_benefit_cap_exempt"].astype(bool), v["reference_is_benefit_cap_exempt"].astype(bool), ) - working_age = ~v["anyone_sp_age"] + # Intended changes, pinned in YAML instead: the UC cap has no age + # exception and HB reg 5 sets the pension-age one; armed forces + # independence payment now lifts both caps (UC reg 83(1)(c) with reg 2; + # HB reg 75F(1)(ea)). + working_age = ~v["anyone_sp_age"] & ~v["afip"] single_claim = v["uc_member_of_couple_claims_as_single_person"].astype(bool) legacy = v["claims_legacy_benefits"].astype(bool) # The old exception listed both schemes' grounds. @@ -827,9 +846,13 @@ def test_examples_reach_the_cases(): assert v["is_uc_benefit_cap_single_claimant_rate"][lone_parent] assert not v["is_housing_benefit_benefit_cap_single_claimant_rate"][lone_parent] assert not v["reference_is_benefit_cap_single_claimant_rate"][lone_parent] - # AFIP: excepted from both shared rates, but not under the old list. + # AFIP: excepted from both shared rates, and lifts both caps, which the + # old lists did not do. assert v["LHA_category"][afip] == v["housing_benefit_LHA_category"][afip] == "B" assert v["reference_LHA_category"][afip] == "A" + assert v["is_uc_benefit_cap_exempt"][afip] + assert v["is_housing_benefit_benefit_cap_exempt"][afip] + assert not v["reference_is_benefit_cap_exempt"][afip] # The mixed-age couple on UC: the UC cap has no age exception, and with # the younger member on UC the HB Regs 2006 apply (reg. 5(1)(b)); the old # shared cap exempted any family with a pensioner. diff --git a/policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py b/policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py index 3a48583431..9316d6da97 100644 --- a/policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py +++ b/policyengine_uk/variables/gov/dwp/BRMA_LHA_rate.py @@ -14,18 +14,24 @@ class BRMA_LHA_rate(Variable): value_type = float entity = BenUnit label = "LHA rate" - documentation = "Local Housing Allowance rate, capped at the national maximum" + documentation = ( + "Local Housing Allowance rate for the Universal Credit category " + "(LHA_category), capped at the weekly national maximum. Housing " + "Benefit reads its own category's rate: housing_benefit_LHA_rate." + ) definition_period = YEAR unit = GBP reference = "https://www.legislation.gov.uk/uksi/1997/1984/schedule/3B" def formula(benunit, period, parameters): - """The published Housing Benefit rate. + """The weekly-capped rate for the Universal Credit category. Rates are the lower of the Broad Rental Market Area percentile and the weekly national maximum for the category (Rent Officers (Housing - Benefit Functions) Order 1997, Schedule 3B). Universal Credit has its - own monthly maximum: see ``uc_LHA_cap``. + Benefit Functions) Order 1997, Schedule 3B). Universal Credit applies + its own monthly maximum from 2020 (``uc_LHA_cap``), and uses this + weekly figure before then; Housing Benefit's rate for its own category + is ``housing_benefit_LHA_rate``. """ rate = benunit("uncapped_BRMA_LHA_rate", period) maximum = category_maximum(benunit, period, "maximum") diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py index 6e9de19409..fce41d35fb 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py @@ -8,7 +8,10 @@ class benefit_cap_reduction(Variable): "(uc_benefit_cap_reduction, UC Regs 2013 reg. 81) or Housing Benefit " "(housing_benefit_benefit_cap_reduction, HB Regs 2006 reg. 75D). Each " "scheme applies its own cap; the model pays a family one or the " - "other, never both. A family with neither award has no reduction." + "other, never both. A family with neither award has no reduction. The " + "Universal Credit part is the reg. 81 amount, which can exceed the " + "award; the Housing Benefit part never exceeds the award less the " + "reg. 75 minimum." ) entity = BenUnit definition_period = YEAR diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py index b884be826b..20eebbfaa9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py @@ -3,10 +3,11 @@ # Benefits whose receipt (or entitlement) by the claimant or partner, or for # some a child or young person, lifts the cap: industrial injuries benefit # (HB Regs 2006 reg. 75F(1)(b)), attendance allowance (c), disability living -# allowance (e), personal independence payment (ea), carer's allowance (h) -# and carer support payment (ha). +# allowance (e), personal independence payment and armed forces independence +# payment (ea), carer's allowance (h) and carer support payment (ha). SPECIFIED_PERSONAL_BENEFITS = [ "attendance_allowance", + "armed_forces_independence_payment", "carers_allowance", "carer_support_payment", "dla", @@ -26,8 +27,8 @@ class is_housing_benefit_benefit_cap_exempt_specified_benefit(Variable): "2006 reg. 75F(1): employment and support allowance with the support " "component, industrial injuries benefit, attendance allowance, a war " "pension or armed forces compensation, disability living allowance, " - "personal independence payment, carer's allowance or carer support " - "payment. Housing Benefit has no claim by a member of a couple as a " + "personal independence payment, armed forces independence payment, " + "carer's allowance or carer support payment. Housing Benefit has no claim by a member of a couple as a " "single person, so the partner's benefits always count. The Universal " "Credit LCWRA and carer elements are not Housing Benefit exceptions. " "Regulation 75F(1)(g) (a claimant receiving Universal Credit) never " diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py index 4bfa81bf4b..05b0b6640c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py @@ -6,10 +6,12 @@ # Benefits whose receipt by "a claimant" (or, for some, a child or qualifying # young person the claimant is responsible for) lifts the cap: industrial # injuries benefit (UC Regs 2013 reg. 83(1)(b)), attendance allowance (c), +# which reg. 2 defines to include armed forces independence payment, # disability living allowance (f), personal independence payment (g), carer's # allowance (i) and carer support payment (ia). SPECIFIED_PERSONAL_BENEFITS = [ "attendance_allowance", + "armed_forces_independence_payment", "carers_allowance", "carer_support_payment", "dla", @@ -27,8 +29,9 @@ class is_uc_benefit_cap_exempt_specified_benefit(Variable): "Whether the award includes the LCWRA or carer element, or a " "claimant receives a benefit listed in UC Regs 2013 reg. 83(1): " "employment and support allowance with the support component, " - "industrial injuries benefit, attendance allowance, a war pension or " - "armed forces compensation, disability living allowance, personal " + "industrial injuries benefit, attendance allowance (including armed " + "forces independence payment, reg. 2), a war pension or armed forces " + "compensation, disability living allowance, personal " "independence payment, carer's allowance or carer support payment. " "A partner who cannot be a joint claimant, so that the other member " "claims as a single person (reg. 3(3)), is not a claimant: their own " diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index 47fa993130..f0b18ea2db 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -11,7 +11,10 @@ class uc_benefit_cap_reduction(Variable): "the Universal Credit cap, minus the childcare costs element, and " "nothing where the childcare costs element is greater than the " "excess (UC Regs 2013 reg. 81). The cap reduces an award of Universal " - "Credit (reg. 78(1)), so there is no reduction without one." + "Credit (reg. 78(1)), so there is no reduction without one. This is the " + "amount reg. 81 takes off, which can exceed the award before the cap; " + "universal_credit floors the award at nil, so the amount actually " + "removed is the smaller of the two." ) definition_period = YEAR unit = GBP From 965bbad5efe097ab0089e14897392f018ec91c74 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 15:17:54 -0400 Subject: [PATCH 07/11] Point two YAML cases at main's HB non-dependant rule and the UC reduction - housing_benefit_lha_category.yaml: main's severe disability premium counts an adult outside the family only as the liable family's non-dependant (#2006). The case now makes the claimant's family the household head's, so the other adult is a non-dependant (HB reg 3) and defeats Sch 3 para 14(2)(a)(ii), as the case intends. - housing_benefit_on_passporting_benefit.yaml (main): universal_credit reads uc_benefit_cap_reduction, so the case sets that input. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_lha_category.yaml | 18 +++++++++++------- ...housing_benefit_on_passporting_benefit.yaml | 2 +- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml index daaf20d1e1..1277152cf6 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml @@ -132,26 +132,30 @@ output: housing_benefit_severe_disability_premium_applies: true -- name: Another benefit unit's adult claimant in the household defeats the severe disability premium +- name: An adult in another family who is the household head's non-dependant defeats the severe disability premium period: 2025 input: people: claimant: age: 30 attendance_allowance: 4_000 - lodger: + # The claimant's family is liable for the rent. + is_household_head: true + non_dependant: age: 40 benunits: claimant_unit: members: [claimant] - lodger_unit: - members: [lodger] + non_dependant_unit: + members: [non_dependant] households: household: - members: [claimant, lodger] + members: [claimant, non_dependant] output: - # The household composition proxy treats another family's claimant as a - # non-dependant, as lha_renter_has_non_dependant does. + # HB reg 3: the other adult normally resides with the claimant and is not + # liable to pay for their occupation, so is a non-dependant; aged 18 or + # over with no qualifying benefit, they defeat Sch 3 para 14(2)(a)(ii). + is_non_dependant_of_household_head: [false, true] housing_benefit_severe_disability_premium_applies: [false, false] - name: An HB young person who is not a UC qualifying young person blocks both shared rates diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml index 655939c656..537f48df6a 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml @@ -56,7 +56,7 @@ input: would_claim_uc: true universal_credit_pre_benefit_cap: 2_000 - benefit_cap_reduction: 2_000 + uc_benefit_cap_reduction: 2_000 income_support: 0 jsa_income: 0 esa_income: 0 From 1f9a71a6b8e64b8d410ebeae2805bbdf132d14cf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 15:30:52 -0400 Subject: [PATCH 08/11] Name the UC reduction in the Housing Benefit passport's circularity note Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit/housing_benefit_on_passporting_benefit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index b0b3a3edc8..12ebef97a3 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -55,7 +55,8 @@ def formula(benunit, period, parameters): ) # Universal Credit before the benefit cap and deductions: Universal # Credit depends on Housing Benefit through the benefit cap - # (benefit_cap_reduction), so reading the amount paid + # (uc_benefit_cap_reduction, whose total benefit_cap_welfare_benefits + # counts Housing Benefit), so reading the amount paid # (universal_credit) here would be circular. Before the cap is also # the law's reading: entitlement counts whether or not it is paid. on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb From 5366f04aed2ee177ecf09c651175411c7dfdac90 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 20:23:31 -0400 Subject: [PATCH 09/11] Compare HB LHA categories with the old shared one only where the bedroom counts agree The old shared category read the Universal Credit size criteria (LHA_allowed_bedrooms). Since #2006, Housing Benefit has its own count (HB Regs 2006 reg. 13D(3), housing_benefit_LHA_allowed_bedrooms), which gives a bedroom to a young person the Universal Credit count leaves out. The differential now compares Housing Benefit's category with the reference only where the two counts agree, and the examples check that the lone-parent case reaches that difference (HB category C). Co-Authored-By: Claude Opus 5.5 --- ...benefit_cap_lha_scheme_split_properties.py | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py index e912882b05..1207239f4f 100644 --- a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -33,7 +33,14 @@ exceptions of both schemes here), the old category is the new category of the scheme the family claims under (legacy: Housing Benefit, otherwise Universal Credit), and with no reg. 3(3) single claim the two - new categories are equal. + new categories are equal. Housing Benefit's comparisons need the two + schemes' size criteria to give the same number of bedrooms. The old + category read the Universal Credit count (``LHA_allowed_bedrooms``); + #2006 gave Housing Benefit its own (HB Regs 2006 reg. 13D(3), + ``housing_benefit_LHA_allowed_bedrooms``), which counts a young person + in the family whom the Universal Credit count leaves out, such as one + who is not a qualifying young person for Universal Credit. This change + leaves both counts as main has them. 4. Reductions. 0 <= each scheme's reduction <= the excess of the welfare benefits over that scheme's cap; the UC reduction is the excess less the childcare costs element (reg. 81); Housing Benefit after the cap @@ -613,6 +620,8 @@ def situation(population, year=PERIOD): "reference_universal_credit", "reference_housing_benefit", "benunit_rent", + "LHA_allowed_bedrooms", + "housing_benefit_LHA_allowed_bedrooms", "LHA_cap", "housing_benefit_LHA_rate", "BRMA_LHA_rate", @@ -719,14 +728,18 @@ def test_lha_category_is_the_claimed_schemes_old_category(population): ) legacy = v["claims_legacy_benefits"].astype(bool) no_afip = ~v["afip"] + # Each scheme's size criteria (see invariant 3). + same_bedrooms = ( + v["LHA_allowed_bedrooms"] == v["housing_benefit_LHA_allowed_bedrooms"] + ) claimed = np.where(legacy, hb, uc) + compared = no_afip & (~legacy | same_bedrooms) np.testing.assert_array_equal( - ref[no_afip], claimed[no_afip], err_msg=str(population) + ref[compared], claimed[compared], err_msg=str(population) ) no_single_claim = ~v["uc_member_of_couple_claims_as_single_person"].astype(bool) - np.testing.assert_array_equal( - uc[no_single_claim], hb[no_single_claim], err_msg=str(population) - ) + compared = no_single_claim & same_bedrooms + np.testing.assert_array_equal(uc[compared], hb[compared], err_msg=str(population)) @PROPERTY_SETTINGS @@ -846,6 +859,13 @@ def test_examples_reach_the_cases(): assert v["is_uc_benefit_cap_single_claimant_rate"][lone_parent] assert not v["is_housing_benefit_benefit_cap_single_claimant_rate"][lone_parent] assert not v["reference_is_benefit_cap_single_claimant_rate"][lone_parent] + # HB's size criteria give the young person a bedroom (reg. 13D(3)(b)), + # so the LHA comparisons leave this family out (invariant 3). + assert v["housing_benefit_LHA_category"][lone_parent] == "C" + assert ( + v["LHA_allowed_bedrooms"][lone_parent] + != v["housing_benefit_LHA_allowed_bedrooms"][lone_parent] + ) # AFIP: excepted from both shared rates, and lifts both caps, which the # old lists did not do. assert v["LHA_category"][afip] == v["housing_benefit_LHA_category"][afip] == "B" From 9e9c4618f547f57a70e2b0ad4f380f0b6797e380 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 11:56:31 -0400 Subject: [PATCH 10/11] Count only the claimants in the UC reg 60C age exception Review after merge (P1): the reg 60C exception took the claimants as is_uc_assessed_claimant less other_member_of_single_claim. That helper recognises a reg 3(3) single claim only where the claimant is under the qualifying age, so a single claimant over it whose partner cannot be a joint claimant (say, subject to immigration control) had the partner counted as a claimant, and a supplied award was capped. Read is_uc_single_or_joint_claimant (WRA 2012 s. 40) instead. Main computes the same wrong result for that case, so this is no regression against main. Adds that case and a mixed-age joint-claimant control to benefit_cap_by_scheme.yaml, and brings the changelog fragments in line with main after the merges: the HB LHA variables and parameter came from #2006 and the HB pension-age variable from #1944, and the UC cap now has the reg 60C exception. Co-Authored-By: Claude Opus 5.5 --- .../uc-hb-split-shared-cap-lha-rules.added.md | 2 +- .../uc-hb-split-shared-cap-lha-rules.fixed.md | 2 +- .../gov/dwp/benefit_cap_by_scheme.yaml | 63 +++++++++++++++++++ ...is_uc_benefit_cap_exempt_qualifying_age.py | 18 +++--- 4 files changed, 74 insertions(+), 11 deletions(-) diff --git a/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md b/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md index 4c627c405d..d6da38960e 100644 --- a/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md +++ b/changelog.d/uc-hb-split-shared-cap-lha-rules.added.md @@ -1 +1 @@ -Per-scheme benefit cap variables (`uc_benefit_cap`, `is_uc_benefit_cap_single_claimant_rate`, `is_uc_benefit_cap_exempt`, `is_uc_benefit_cap_exempt_earnings`, `is_uc_benefit_cap_exempt_specified_benefit`, `uc_benefit_cap_reduction`; `housing_benefit_benefit_cap`, `is_housing_benefit_benefit_cap_single_claimant_rate`, `is_housing_benefit_benefit_cap_exempt`, `is_housing_benefit_benefit_cap_exempt_working_tax_credit`, `is_housing_benefit_benefit_cap_exempt_specified_benefit`, `housing_benefit_benefit_cap_reduction`, `housing_benefit_pension_age_regulations_apply`; `benefit_cap_welfare_benefits`) and Housing Benefit LHA variables (`housing_benefit_LHA_category`, `housing_benefit_LHA_rate`, `is_housing_benefit_young_individual`, `housing_benefit_severe_disability_premium_applies`), with the parameters `gov.dwp.housing_benefit.minimum_weekly_amount` and `gov.dwp.housing_benefit.severe_disability_premium.qualifying_benefits`. +Per-scheme benefit cap variables (`uc_benefit_cap`, `is_uc_benefit_cap_single_claimant_rate`, `is_uc_benefit_cap_exempt`, `is_uc_benefit_cap_exempt_earnings`, `is_uc_benefit_cap_exempt_specified_benefit`, `is_uc_benefit_cap_exempt_qualifying_age`, `uc_benefit_cap_reduction`; `housing_benefit_benefit_cap`, `is_housing_benefit_benefit_cap_single_claimant_rate`, `is_housing_benefit_benefit_cap_exempt`, `is_housing_benefit_benefit_cap_exempt_working_tax_credit`, `is_housing_benefit_benefit_cap_exempt_specified_benefit`, `housing_benefit_benefit_cap_reduction`; `benefit_cap_welfare_benefits`), with the parameter `gov.dwp.housing_benefit.minimum_weekly_amount`. diff --git a/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md b/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md index c57809557a..935ed6766c 100644 --- a/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md +++ b/changelog.d/uc-hb-split-shared-cap-lha-rules.fixed.md @@ -1 +1 @@ -Universal Credit and Housing Benefit each apply their own benefit cap and LHA shared accommodation rules. The UC cap (UC Regs 2013 regs 79-83) treats a member of a couple claiming as a single person (reg 3(3)) as a single claimant, lifts the cap for earnings and the LCWRA and carer elements but not for working tax credit or age, and takes the childcare costs element off the reduction (reg 81). The HB cap (HB Regs 2006 Part 8A) treats that couple as a couple, lifts the cap for working tax credit and the benefits in reg 75F (the partner's included) but not for earnings or UC elements, does not reach pension-age claims (reg 5), and leaves at least 50 pence a week of HB (reg 75D(2)). The UC LHA category (`LHA_category`) and a new HB one (`housing_benefit_LHA_category`, HB reg 13D(2)) each apply their scheme's shared accommodation test, and armed forces independence payment now excepts a renter under 35 from both and lifts both caps (UC reg 83(1)(c) with reg 2; HB reg 75F(1)(ea)). The `claims_legacy_benefits` stopgap for reg 3(3) single claims is gone. +Universal Credit and Housing Benefit each apply their own benefit cap. The UC cap (UC Regs 2013 regs 79-83) treats a member of a couple claiming as a single person (reg 3(3)) as a single claimant, lifts the cap for earnings and the LCWRA and carer elements but not for working tax credit, lifts it on age only where the single claimant or both joint claimants have reached the qualifying age for State Pension Credit (SI 2014/1230 reg 60C), and takes the childcare costs element off the reduction (reg 81). The HB cap (HB Regs 2006 Part 8A) treats that couple as a couple, lifts the cap for working tax credit and the benefits in reg 75F (the partner's included) but not for earnings or UC elements, does not reach pension-age claims (reg 5), and leaves at least 50 pence a week of HB (reg 75D(2)). The UC shared accommodation test (`is_lha_shared_accommodation_rate_specified_renter`, UC Sch 4 paras 27-29) applies reg 3(3) whatever legacy benefits the family reports, and armed forces independence payment now excepts a renter under 35 from it (UC reg 2) and lifts both caps (UC reg 83(1)(c) with reg 2; HB reg 75F(1)(ea)). The `claims_legacy_benefits` stopgap for reg 3(3) single claims is gone. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml index 99129ac76a..44b92a11fb 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml @@ -151,6 +151,69 @@ uc_benefit_cap_reduction: 0 universal_credit: 30_000 +- name: SI 2014/1230 reg 60C with UC reg 3(3) - a single claimant at the qualifying age whose partner cannot be a joint claimant is not capped + # The partner (say, subject to immigration control: reg 3(3)(e)) is not a + # claimant, so the claim is a single claimant's, and reg 60C asks only + # whether that claimant has reached the qualifying age. The award is an + # input, as above. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + partner: + age: 50 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + universal_credit_pre_benefit_cap: 30_000 + uc_deductions: 0 + households: + household: + members: [claimant, partner] + region: LONDON + output: + is_uc_single_or_joint_claimant: [true, false] + is_uc_benefit_cap_exempt_qualifying_age: true + is_uc_benefit_cap_exempt: true + uc_benefit_cap_reduction: 0 + universal_credit: 30_000 + +- name: SI 2014/1230 reg 60C - the same award to joint claimants of whom only one has reached the qualifying age is capped + # Reg 60C needs both joint claimants to have reached the qualifying age. + # London couple cap 25,323 (reg 80A(2)(b)(i)); excess 30,000 - 25,323 = + # 4,677; no childcare costs element (reg 81). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + partner: + age: 50 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + universal_credit_pre_benefit_cap: 30_000 + uc_deductions: 0 + households: + household: + members: [claimant, partner] + region: LONDON + output: + is_uc_single_or_joint_claimant: [true, true] + is_uc_benefit_cap_exempt_qualifying_age: false + is_uc_benefit_cap_exempt: false + uc_benefit_cap: 25_323 + uc_benefit_cap_reduction: 4_677 + universal_credit: 25_323 + - name: UC regs 79-83 - a mixed-age couple on UC is capped; under reg 5(1)(b) so is their HB period: 2025 input: diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_qualifying_age.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_qualifying_age.py index 056664bf17..78de710755 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_qualifying_age.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_qualifying_age.py @@ -1,7 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim, -) class is_uc_benefit_cap_exempt_qualifying_age(Variable): @@ -20,7 +17,9 @@ class is_uc_benefit_cap_exempt_qualifying_age(Variable): "claimants is one to which reg. 60C applies. The model's own " "eligibility test needs a claimant under the qualifying age, so this " "matters only where an award is supplied as an input. A mixed-age " - "couple's joint award is capped like any other." + "couple's joint award is capped like any other. A member of a couple " + "who claims as a single person (UC Regs 2013 reg. 3(3)) is a single " + "claimant, so only their own age counts." ) definition_period = YEAR reference = ( @@ -32,10 +31,11 @@ class is_uc_benefit_cap_exempt_qualifying_age(Variable): def formula(benunit, period, parameters): person = benunit.members - # The claimants: the single claimant or each joint claimant. The other - # member of a reg. 3(3) single claim is not a claimant. - claimant = person("is_uc_assessed_claimant", period) & ~( - other_member_of_single_claim(person, period) - ) + # The claimants: the single claimant or each joint claimant (WRA 2012 + # s. 40). A partner who cannot be a joint claimant (reg. 3(3)) is not + # one. That holds whatever the claimant's age: the reg. 3(3) routing + # in uc_member_of_couple_claims_as_single_person needs a claimant under + # the qualifying age, which a reg. 60C claimant never is. + claimant = person("is_uc_single_or_joint_claimant", period) attained = person("has_attained_state_pension_credit_qualifying_age", period) return benunit.any(claimant) & benunit.all(attained | ~claimant) From 61b4127a340ef3c03a8a329f4ec0f0097c7673dd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 13:50:21 -0400 Subject: [PATCH 11/11] Hold each scheme's benefit cap reduction in the legacy readers property #2294 holds the benefit cap at zero in test_legacy_award_readers_properties.py by setting benefit_cap_reduction, which Universal Credit read on main. Here Universal Credit reads uc_benefit_cap_reduction and Housing Benefit reads housing_benefit_benefit_cap_reduction, and benefit_cap_reduction is only their sum, so that input no longer held either award. Hold both scheme reductions instead, keeping the hold's purpose (#2298). Co-Authored-By: Claude Opus 5.5 --- .../tests/test_legacy_award_readers_properties.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py index 5cba93171c..2927d25656 100644 --- a/policyengine_uk/tests/test_legacy_award_readers_properties.py +++ b/policyengine_uk/tests/test_legacy_award_readers_properties.py @@ -290,8 +290,12 @@ def parent(age, **awards): # income-related ESA, not only the claimant's and partner's # (PolicyEngine/policyengine-uk#2298). The cap is not one of the readers this # file tests, so it is held at zero here until that is fixed; drop the hold -# when it is. -CAP_HELD = {"benefit_cap_reduction": 0} +# when it is. Universal Credit and Housing Benefit each read their own +# scheme's reduction, so both are held. +CAP_HELD = { + "uc_benefit_cap_reduction": 0, + "housing_benefit_benefit_cap_reduction": 0, +} @SETTINGS