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..d6da38960e --- /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`, `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 new file mode 100644 index 0000000000..935ed6766c --- /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. 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/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 7328c12022..277482eaf4 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -40,11 +40,11 @@ "\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, or a claimant entitled to one bedroom who lacks exclusive use of self-contained accommodation, unless the severe disability premium applies, 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", " If the claimant or partner is on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`housing_benefit_on_passporting_benefit`), the whole of the family's earnings, other income and capital is disregarded ([SI 2006/213 Sch. 4 para. 12](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12), [Sch. 5 para. 4](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4) and [Sch. 6 para. 5](https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5); Northern Ireland: [SR 2006/405 Sch. 5 para. 12](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12), [Sch. 6 para. 4](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4) and [Sch. 7 para. 5](https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5)). `housing_benefit_applicable_income` and `housing_benefit_assessable_capital` are then zero, so the award is that maximum. This applies at any age: a mixed-age couple whose younger member is on one of these benefits is assessed under the working-age regulations ([SI 2006/213 reg. 5(1)(b)](https://www.legislation.gov.uk/uksi/2006/213/regulation/5)). Income Support, income-based Jobseeker's Allowance and income-related Employment and Support Allowance count when a positive amount is paid on the claimant's or partner's award (`in_receipt_of_income_support_jsa_ib_or_esa_ir`, which reads `claimant_or_partner_jsa_income` and `claimant_or_partner_esa_income`). Another member's own award, such as a non-dependant's, is payable to them and does not passport the claimant. A person is on Universal Credit on any day they are entitled to it, whether it is in payment or not ([reg. 2(3B)](https://www.legislation.gov.uk/uksi/2006/213/regulation/2)); PolicyEngine reads that as Universal Credit above nil before the benefit cap and deductions (`is_uc_entitled`), for a family that claims it, so the passport adds no circular dependency through the benefit cap. The universal credit limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). It matters only where Housing Benefit is paid alongside Universal Credit, for specified or temporary accommodation, which the eligibility rules above do not yet model.\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 7f2e0d6738..a8548749c6 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**: only the claimants' income counts: the single claimant, or the two joint claimants (`is_uc_assessed_claimant`; UC Regulations 2013 reg 22(1)). The earnings and unearned income of a child, a qualifying young person or anyone else in the benefit unit are theirs, not the claimant's, so the Universal Credit means test does not count them. Who the claimants are comes from `is_uc_claimant` (the claimant and partner); a member flagged there is taken as a claimant whatever their age. 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 (`hours_worked`), then the elder. `hours_worked` defaults to 0, so for a couple entered without hours the default falls to the elder; supply `uc_is_responsible_carer` where the nomination is known. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or for the responsible carer of a child under 13 the hours DWP uses: 30 from February 2025, from April 2017 until then 16 for a child under compulsory school age and 25 for an older child, and 25 for any child under 13 before April 2017; 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, nothing for a partner with no work-related requirements for a reason other than earnings, and 35 hours at the national living wage (the adult rate before April 2016) for a partner who cannot be a joint claimant (reg 90(3)(b)). The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the claimants' combined earnings above the work allowance (`uc_work_allowance`), and by all of the claimants' 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, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. 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 (the claimants earning enough, having a qualifying disability benefit, etc.). The earnings exception reads the claimants' earnings only (reg 82(1)(a)), the capped total counts only the claimants' own contributory benefits (WRA 2012 s.96(1)), and the childcare work condition tests only the claimants (reg 32(1)).\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**: only the claimants' income counts: the single claimant, or the two joint claimants (`is_uc_assessed_claimant`; UC Regulations 2013 reg 22(1)). The earnings and unearned income of a child, a qualifying young person or anyone else in the benefit unit are theirs, not the claimant's, so the Universal Credit means test does not count them. Who the claimants are comes from `is_uc_claimant` (the claimant and partner); a member flagged there is taken as a claimant whatever their age. 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 (`hours_worked`), then the elder. `hours_worked` defaults to 0, so for a couple entered without hours the default falls to the elder; supply `uc_is_responsible_carer` where the nomination is known. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or for the responsible carer of a child under 13 the hours DWP uses: 30 from February 2025, from April 2017 until then 16 for a child under compulsory school age and 25 for an older child, and 25 for any child under 13 before April 2017; 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, nothing for a partner with no work-related requirements for a reason other than earnings, and 35 hours at the national living wage (the adult rate before April 2016) for a partner who cannot be a joint claimant (reg 90(3)(b)). The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the claimants' combined earnings above the work allowance (`uc_work_allowance`), and by all of the claimants' 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, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. 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. The earnings exception reads the claimants' earnings only (reg 82(1)(a)), the capped total counts only the claimants' own contributory benefits (WRA 2012 s.96(1)), and the childcare work condition tests only the claimants (reg 32(1)). 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/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/programs.yaml b/policyengine_uk/programs.yaml index e8ba90376b..63d7e581c7 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -318,9 +318,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/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml index 9f2078bd74..79162f0948 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,9 @@ 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 + # HB reg 13D(3)(a)-(b): a bedroom for the renter and one for the + # 17-year-old, who is not a child (reg 2(1): under 16): 2 bedrooms, C. + housing_benefit_LHA_category: C - name: Lone parent with an 18-year-old qualifying young person period: 2025 @@ -89,8 +100,11 @@ is_lha_shared_accommodation_rate_specified_renter: false # Responsibility for a UC QYP blocks A. Existing bedroom formula = 1: B. LHA_category: B + # HB reg 13D(3)(a)-(b): a bedroom for the renter and one for the + # 18-year-old, who is not a child (reg 2(1): under 16): 2 bedrooms, C. + housing_benefit_LHA_category: C -- 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 +132,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 +162,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 in another family is a non-dependant of the household head only period: 2025 @@ -246,6 +266,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 gives the shared rate for Housing Benefit only period: 2025 @@ -271,6 +292,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 @@ -282,6 +304,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 @@ -290,6 +313,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 @@ -299,3 +323,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/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..1277152cf6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_lha_category.yaml @@ -0,0 +1,198 @@ +# 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: 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 + # The claimant's family is liable for the rent. + is_household_head: true + non_dependant: + age: 40 + benunits: + claimant_unit: + members: [claimant] + non_dependant_unit: + members: [non_dependant] + households: + household: + members: [claimant, non_dependant] + output: + # 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 + # 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)). 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: + 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] + LHA_allowed_bedrooms: 2 + 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: C + housing_benefit_LHA_category: C diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml index c48cc616df..629d28791b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml @@ -338,12 +338,13 @@ targeted_childcare_entitlement_eligible: true # Benefit cap: no cap where "the claimant or the claimant's partner" receives -# ESA with the support component (HB Regs 2006 reg 75F(1)(a)). The model -# exempts any income-related ESA. This PR leaves the cap's reading of the -# benefit unit's ESA unchanged: scoping the exemption alone would cap the -# claimant with another member's own award in the capped sum -# (benefit_cap_reduction), which is out of scope here. The claimant's own -# award still exempts the family. +# ESA with the support component (HB Regs 2006 reg 75F(1)(a)); Universal +# Credit's exception names "a claimant" (UC Regs 2013 reg 83(1)(a)). The model +# exempts any income-related ESA under each scheme. This PR leaves the cap's +# reading of the benefit unit's ESA unchanged: scoping the exemption alone +# would cap the claimant with another member's own award in the capped sum +# (benefit_cap_welfare_benefits), which is out of scope here. The claimant's +# own award still exempts the family from both caps. - name: The claimant's own income-related ESA exempts the family from the benefit cap period: 2025 @@ -365,7 +366,8 @@ household: members: [claimant, other] output: - is_benefit_cap_exempt_health_disability: true + is_uc_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt_specified_benefit: true # Maintenance loans: the "students entitled to benefits" schedule (SI 2011/1986 # reg 71(1)(h)(iii)) follows the special support grant test in reg 61(2)(b), 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 4176316e7c..66b8c4e5e0 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 @@ -586,14 +586,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 @@ -609,14 +613,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 @@ -638,8 +647,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 @@ -660,7 +673,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 @@ -682,7 +696,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 @@ -709,7 +724,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 @@ -735,6 +754,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 @@ -758,6 +781,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 @@ -802,7 +827,7 @@ is_uc_single_or_joint_claimant: false is_uc_eligible: false -- name: A family that reports Housing Benefit but claims UC gets the UC single-claim rules in the cap and LHA +- name: A family that reports Housing Benefit but claims UC is single for UC's cap and shared rate and a couple for HB's period: 2026 absolute_error_margin: 0.01 input: @@ -828,18 +853,26 @@ output: claims_legacy_benefits: true uc_member_of_couple_claims_as_single_person: true - # Calculated on UC, so reg. 80A(2)(c), reg. 83(1) and Sch 4 para 28(2) - # apply: the single cap, no exception for the other member's PIP, and - # shared accommodation. - is_benefit_cap_single_claimant_rate: true - is_benefit_cap_exempt: false - benefit_cap: 14_753 + # Each scheme applies its own rules. UC: a single claimant (reg. + # 80A(2)(c)); the other member's PIP is not a claimant's, so no exception + # (reg. 83(1)(g)); shared accommodation (Sch 4 para 28(2)). + is_uc_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_exempt: false + 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: the couple + # rate (reg. 75CA(2)(d)), the partner's PIP lifts the cap (reg. + # 75F(1)(ea), "the claimant or the claimant's partner"), and a couple is + # not a young individual (reg. 2(1); reg. 13D(2)(a)(i)). + is_housing_benefit_benefit_cap_single_claimant_rate: false + is_housing_benefit_benefit_cap_exempt: true + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B # 424.90 x 12 uc_standard_allowance: 5_098.80 -- name: A family that stays on legacy benefits keeps the couple rules Housing Benefit shares +- name: A family that stays on legacy benefits gets the same per-scheme rules period: 2026 absolute_error_margin: 0.01 input: @@ -864,11 +897,19 @@ region: NORTH_EAST output: claims_legacy_benefits: true - # Housing Benefit has no single claim by a member of a couple: the couple - # rate (HB reg. 75CA), the partner's PIP lifts the cap (HB reg. - # 75F(1)(ea), "the claimant or the claimant's partner"), and a couple is - # not a young individual (HB reg. 2(1)). - is_benefit_cap_single_claimant_rate: false - is_benefit_cap_exempt: true - is_lha_shared_accommodation_rate_specified_renter: false - LHA_category: B + # Staying on legacy benefits changes neither scheme's rules. Housing + # Benefit, which this family receives, has no single claim by a member of + # a couple: the couple rate (HB reg. 75CA(2)(d)), the partner's PIP lifts + # the cap (HB reg. 75F(1)(ea)), and a couple is not a young individual + # (HB reg. 2(1)): one bedroom (B). + is_housing_benefit_benefit_cap_single_claimant_rate: false + is_housing_benefit_benefit_cap_exempt: true + is_housing_benefit_young_individual: false + housing_benefit_LHA_category: B + # The Universal Credit rules are those of the reg. 3(3) single claim + # (reg. 80A(2)(c), reg. 83(1), Sch 4 para 28(2)). + is_uc_benefit_cap_single_claimant_rate: true + is_uc_benefit_cap_exempt: false + uc_benefit_cap: 14_753 + is_lha_shared_accommodation_rate_specified_renter: true + LHA_category: A diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml index 19bfeb3f28..76e57e1c30 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml @@ -31,8 +31,8 @@ members: [parent, young_person] region: NORTH_EAST output: - is_benefit_cap_exempt_earnings: false - benefit_cap: 22_020 + is_uc_benefit_cap_exempt_earnings: false + uc_benefit_cap: 22_020 - name: The claimant's own earnings still meet the benefit cap earnings exception period: 2026 @@ -54,7 +54,7 @@ members: [parent, young_person] region: NORTH_EAST output: - is_benefit_cap_exempt_earnings: true + is_uc_benefit_cap_exempt_earnings: true - name: Joint claimants' combined earnings meet the benefit cap earnings exception # Neither partner's £6,000 reaches the threshold alone; together they do. @@ -80,7 +80,7 @@ members: [parent_1, parent_2, child] region: NORTH_EAST output: - is_benefit_cap_exempt_earnings: true + is_uc_benefit_cap_exempt_earnings: true - name: A dependent 18-year-old's job makes no difference to the childcare work condition # The working lone parent meets it whether or not the young person works. @@ -184,7 +184,9 @@ members: [parent, young_person] region: NORTH_EAST output: - benefit_cap: 22_020 + uc_benefit_cap: 22_020 + benefit_cap_welfare_benefits: 30_000 + uc_benefit_cap_reduction: 30_000 - 22_020 benefit_cap_reduction: 30_000 - 22_020 - name: The claimant's own contributory benefit is part of the benefit cap total @@ -209,5 +211,7 @@ members: [parent, young_person] region: NORTH_EAST output: - benefit_cap: 22_020 + uc_benefit_cap: 22_020 + benefit_cap_welfare_benefits: 30_000 + 2_000 + uc_benefit_cap_reduction: 30_000 + 2_000 - 22_020 benefit_cap_reduction: 30_000 + 2_000 - 22_020 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/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml index b0232bd974..0471cc7488 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_pension_age.yaml @@ -40,9 +40,9 @@ output: is_uc_entitled: true housing_benefit_pension_age_regulations_apply: false - is_benefit_cap_exempt_other: false - is_benefit_cap_exempt: false - benefit_cap: 25_323 + is_uc_benefit_cap_exempt_qualifying_age: false + is_uc_benefit_cap_exempt: false + uc_benefit_cap: 25_323 # 30,000 + 3,000 - 25,323 benefit_cap_reduction: 7_677 universal_credit: 22_323 @@ -72,9 +72,9 @@ members: [older, younger, child_1, child_2] region: LONDON output: - is_benefit_cap_exempt_other: false - is_benefit_cap_exempt_earnings: true - is_benefit_cap_exempt: true + is_uc_benefit_cap_exempt_qualifying_age: false + is_uc_benefit_cap_exempt_earnings: true + is_uc_benefit_cap_exempt: true benefit_cap_reduction: 0 universal_credit: 30_000 @@ -103,7 +103,7 @@ output: is_uc_entitled: false housing_benefit_pension_age_regulations_apply: true - is_benefit_cap_exempt_other: true + is_housing_benefit_benefit_cap_exempt: true benefit_cap_reduction: 0 housing_benefit: 30_000 @@ -134,9 +134,8 @@ output: jsa_income: 4_000 housing_benefit_pension_age_regulations_apply: false - is_benefit_cap_exempt_other: false - is_benefit_cap_exempt: false - benefit_cap: 25_323 + is_housing_benefit_benefit_cap_exempt: false + housing_benefit_benefit_cap: 25_323 # 30,000 + 4,000 - 25,323 benefit_cap_reduction: 8_677 housing_benefit: 21_323 @@ -162,7 +161,7 @@ output: is_uc_entitled: false housing_benefit_pension_age_regulations_apply: true - is_benefit_cap_exempt_other: true + is_housing_benefit_benefit_cap_exempt: true benefit_cap_reduction: 0 housing_benefit: 30_000 @@ -191,16 +190,19 @@ region: LONDON output: housing_benefit_pension_age_regulations_apply: false - is_benefit_cap_exempt: false + is_uc_benefit_cap_exempt: false benefit_cap_reduction: 7_677 universal_credit: 22_323 - name: A pensioner whose only younger adult is a qualifying young person is not capped # Universal Credit needs a claimant or partner under the qualifying age (UC # Regs 2013 reg 3(2)(a)), and the model's is_uc_eligible needs one too, so - # the award here is an input the family could not hold in law. The - # 18-year-old is a dependant, not a partner, so the award does not take the - # family out of the pension-age Housing Benefit rules, which have no cap. + # the award here is an input. The only claim a claimant over that age can + # make is a qualifying claim by a tax credit migrant (SI 2014/1230 reg + # 60A), and SI 2014/1230 reg 60C disapplies the cap for it. The + # 18-year-old is a dependant, not a partner, so the parent is the only + # claimant, and the award does not take the family out of the pension-age + # Housing Benefit rules, which have no cap. period: 2026 absolute_error_margin: 0.01 input: @@ -227,7 +229,9 @@ is_claimant_or_partner: [true, false, false] is_uc_entitled: true housing_benefit_pension_age_regulations_apply: true - is_benefit_cap_exempt_other: true + is_uc_benefit_cap_exempt_qualifying_age: true + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt: true benefit_cap_reduction: 0 universal_credit: 30_000 @@ -260,7 +264,8 @@ output: is_claimant_or_partner: [true, false, false] housing_benefit_pension_age_regulations_apply: true - is_benefit_cap_exempt_other: true - is_benefit_cap_exempt: true + is_uc_benefit_cap_exempt_qualifying_age: true + is_uc_benefit_cap_exempt: true + is_housing_benefit_benefit_cap_exempt: true benefit_cap_reduction: 0 universal_credit: 30_000 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..44b92a11fb --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml @@ -0,0 +1,400 @@ +# 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 + # No entitlement to carer's allowance or carer support payment. + carers_allowance_pre_overlap: 0 + carer_support_payment_pre_overlap: 0 + carers_allowance: 0 + carer_support_payment: 0 + working_tax_credit: 0 + output: + is_entitled_to_carer_benefit: false + 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 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: + 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 + # UC regs 79, 82 and 83 have no age exception. A claimant over the + # qualifying age can claim UC only on a qualifying claim (SI 2014/1230 + # reg 60A), for which reg 60C disapplies the cap. + is_uc_benefit_cap_exempt_earnings: false + is_uc_benefit_cap_exempt_specified_benefit: false + is_uc_benefit_cap_exempt_qualifying_age: true + is_uc_benefit_cap_exempt: true + +- name: SI 2014/1230 reg 60C - joint claimants who have both reached the qualifying age are not capped + # Reg 60C: "Where a qualifying claim is made by a single claimant who has, + # or joint claimants both of whom have, reached the qualifying age for state + # pension credit, regulation 79 ... is not to apply". Reg 60A makes such a + # claim possible for tax credit migrants; the model's own eligibility test + # does not, so the award is an input. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 72 + younger: + age: 68 + benunits: + benunit: + members: [older, younger] + universal_credit_pre_benefit_cap: 30_000 + uc_deductions: 0 + households: + household: + members: [older, younger] + region: LONDON + output: + 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 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: + 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 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_overlapping_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_overlapping_benefits.yaml index 72cf06ab25..6467d450c6 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_overlapping_benefits.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_overlapping_benefits.yaml @@ -194,7 +194,8 @@ is_entitled_to_carer_benefit: true # HB Regs 2006 reg 75F(1)(h) and UC Regs 2013 reg 83(1)(i) exempt a # person "entitled to" Carer's Allowance. - is_benefit_cap_exempt_health_disability: true + is_uc_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt_specified_benefit: true - name: Case 13, Severe Disablement Allowance at its maximum rate exceeds the allowance. period: 2026 @@ -255,7 +256,8 @@ carers_allowance_pre_overlap: 0 receives_carers_allowance: true is_entitled_to_carer_benefit: true - is_benefit_cap_exempt_health_disability: true + is_uc_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt_specified_benefit: true - name: Case 17, Pension Credit counts the allowance after the overlap. period: 2026 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 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml index e993dfbfb0..6dd710343d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/state_pension_credit_qualifying_age.yaml @@ -39,7 +39,9 @@ is_pension_credit_eligible: true is_uc_eligible: false council_tax_reduction_pensioner: true - is_benefit_cap_exempt_other: true + # HB Regs 2006 reg 5: the pension-age regulations, which have no cap. + housing_benefit_pension_age_regulations_apply: true + is_housing_benefit_benefit_cap_exempt: true # SI 2000/729 reg 2(1)(i): 200 for a person under 80 not in residential care. winter_fuel_allowance: 200 @@ -161,7 +163,9 @@ is_pension_credit_eligible: false is_uc_eligible: true council_tax_reduction_pensioner: false - is_benefit_cap_exempt_other: false + # HB Regs 2006 reg 5: the pension-age regulations, which have no cap. + housing_benefit_pension_age_regulations_apply: false + is_housing_benefit_benefit_cap_exempt: false winter_fuel_allowance: 0 - name: Case 7, the same man is over both ages in 2018-19. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml index e791a4029a..f19bc2277c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.yaml @@ -240,7 +240,9 @@ is_uc_eligible: false is_uc_entitled: false housing_benefit_pension_age_regulations_apply: true - is_benefit_cap_exempt: true + # Housing Benefit under the pension-age regulations has no cap; there is + # no Universal Credit award to cap. + is_housing_benefit_benefit_cap_exempt: true benefit_cap_reduction: 0 council_tax_reduction_pensioner: true council_tax_reduction_scheme_supported: true 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_benefit_cap_and_ctr_pension_age_properties.py b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py index 2e997d22cb..682e1fa939 100644 --- a/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_and_ctr_pension_age_properties.py @@ -32,15 +32,18 @@ Pension age and the family is neither on Universal Credit nor on a legacy income-related benefit. P2 Every Universal Credit award is capped unless a non-age exception applies: - such a family has a finite benefit cap. + such a family has a finite Universal Credit benefit cap. P3 Differential against the formulas before this change. A family with no member over State Pension age, or one over it with neither Universal - Credit nor a legacy income-related benefit, gets exactly the same benefit - cap, Universal Credit, Housing Benefit, Council Tax Reduction and household - net income. -P4 The change only removes exemptions: exempt now implies exempt before, the - cap is never higher, and Universal Credit and Housing Benefit never rise. - A CTR pensioner now was a CTR pensioner before. + Credit nor a legacy income-related benefit, gets exactly the same Housing + Benefit cap, Universal Credit, Housing Benefit, Council Tax Reduction and + household net income. A family with no member over State Pension age also + gets the same Universal Credit cap; one with such a member and no + Universal Credit award has no award for that cap to reduce. +P4 The change only removes exemptions: for each scheme, exempt now implies + exempt before and the cap is never higher, and Universal Credit and + Housing Benefit never rise. A CTR pensioner now was a CTR pensioner + before. P5 The Welsh and Scottish CTR schemes do not depend on pensioner status, but they count Universal Credit as income. So with the CTR pensioner test held as it is now (it also sets the pension-age applicable amount and @@ -93,19 +96,42 @@ TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] -# The two formulas as they were before this change, kept as the reference for -# the differential invariants. -class is_benefit_cap_exempt_other(Variable): +# The formulas as they were before this change, kept as the reference for +# the differential invariants. Before it, any member over State Pension age +# lifted the cap. Each scheme's cap exceptions now have their own variable, +# so the reference adds that age test to each scheme's non-age exceptions. +def has_pensioner(benunit, period): + return benunit.any(benunit.members("is_SP_age", period)) + + +class is_uc_benefit_cap_exempt(Variable): value_type = bool entity = BenUnit - label = "Benefit cap exemption before this change" + label = "Universal Credit benefit cap exemption before this change" definition_period = YEAR def formula(benunit, period, parameters): - has_pensioner = benunit.any(benunit.members("is_SP_age", period)) - afcs = benunit("afcs", period) > 0 - esa_support_component = benunit("esa_contrib", period) > 0 - return has_pensioner | afcs | esa_support_component + return ( + has_pensioner(benunit, period) + | benunit("is_uc_benefit_cap_exempt_earnings", period) + | benunit("is_uc_benefit_cap_exempt_specified_benefit", period) + ) + + +class is_housing_benefit_benefit_cap_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Housing Benefit benefit cap exemption before this change" + definition_period = YEAR + + def formula(benunit, period, parameters): + return ( + has_pensioner(benunit, period) + | benunit( + "is_housing_benefit_benefit_cap_exempt_working_tax_credit", period + ) + | benunit("is_housing_benefit_benefit_cap_exempt_specified_benefit", period) + ) class council_tax_reduction_pensioner(Variable): @@ -125,7 +151,8 @@ def formula(benunit, period, parameters): class before_this_change(Reform): def apply(self): - self.update_variable(is_benefit_cap_exempt_other) + self.update_variable(is_uc_benefit_cap_exempt) + self.update_variable(is_housing_benefit_benefit_cap_exempt) self.update_variable(council_tax_reduction_pensioner) @@ -135,7 +162,8 @@ def apply(self): # through Universal Credit. class before_the_cap_change(Reform): def apply(self): - self.update_variable(is_benefit_cap_exempt_other) + self.update_variable(is_uc_benefit_cap_exempt) + self.update_variable(is_housing_benefit_benefit_cap_exempt) @st.composite @@ -218,11 +246,12 @@ def situation(units): "is_uc_entitled", "housing_benefit_pension_age_regulations_apply", "council_tax_reduction_pensioner", - "is_benefit_cap_exempt", - "is_benefit_cap_exempt_health_disability", - "is_benefit_cap_exempt_earnings", - "is_benefit_cap_exempt_other", - "benefit_cap", + "is_uc_benefit_cap_exempt", + "is_uc_benefit_cap_exempt_specified_benefit", + "is_uc_benefit_cap_exempt_earnings", + "uc_benefit_cap", + "is_housing_benefit_benefit_cap_exempt", + "housing_benefit_benefit_cap", "universal_credit", "housing_benefit", "council_tax_benefit", @@ -231,8 +260,10 @@ def situation(units): ] HOUSEHOLD = ["household_net_income"] BEFORE = [ - "is_benefit_cap_exempt", - "benefit_cap", + "is_uc_benefit_cap_exempt", + "uc_benefit_cap", + "is_housing_benefit_benefit_cap_exempt", + "housing_benefit_benefit_cap", "universal_credit", "housing_benefit", "council_tax_benefit", @@ -280,22 +311,33 @@ def test_benefit_cap_and_ctr_pension_age_invariants(units): assert now["council_tax_reduction_pensioner"][i] == expected, unit # P2 other_exception = ( - now["is_benefit_cap_exempt_health_disability"][i] - or now["is_benefit_cap_exempt_earnings"][i] + now["is_uc_benefit_cap_exempt_specified_benefit"][i] + or now["is_uc_benefit_cap_exempt_earnings"][i] or now["afcs"][i] > 0 or now["esa_contrib"][i] > 0 ) if on_uc and not other_exception: - assert not now["is_benefit_cap_exempt"][i], unit - assert np.isfinite(now["benefit_cap"][i]), unit + assert not now["is_uc_benefit_cap_exempt"][i], unit + assert np.isfinite(now["uc_benefit_cap"][i]), unit # P3 if not has_pension_age_claimant(unit) or not (on_uc or legacy): + uc_cap = ["is_uc_benefit_cap_exempt", "uc_benefit_cap"] for name in BEFORE: + if name in uc_cap and has_pension_age_claimant(unit): + # Before this change the age test lifted the Universal + # Credit cap too, but such a family has no award for it + # to reduce. + assert close(now["universal_credit"][i], 0), unit + continue assert close(now[name][i], before[name][i]), (name, unit) # P4 - if now["is_benefit_cap_exempt"][i]: - assert before["is_benefit_cap_exempt"][i], unit - assert now["benefit_cap"][i] <= before["benefit_cap"][i], unit + for exempt, cap in [ + ("is_uc_benefit_cap_exempt", "uc_benefit_cap"), + ("is_housing_benefit_benefit_cap_exempt", "housing_benefit_benefit_cap"), + ]: + if now[exempt][i]: + assert before[exempt][i], (exempt, unit) + assert now[cap][i] <= before[cap][i], (cap, unit) assert now["universal_credit"][i] <= before["universal_credit"][i] + 0.01 assert now["housing_benefit"][i] <= before["housing_benefit"][i] + 0.01 if now["council_tax_reduction_pensioner"][i]: @@ -358,12 +400,15 @@ def run(older_is_pension_age): ) return { name: np.asarray(simulation.calculate(name, PERIOD)) - for name in ["is_benefit_cap_exempt", "benefit_cap_reduction"] + for name in ["is_uc_benefit_cap_exempt", "benefit_cap_reduction"] } working, mixed = run(False), run(True) for i, c in enumerate(couples): - assert mixed["is_benefit_cap_exempt"][i] == working["is_benefit_cap_exempt"][i] + assert ( + mixed["is_uc_benefit_cap_exempt"][i] + == working["is_uc_benefit_cap_exempt"][i] + ) assert close( mixed["benefit_cap_reduction"][i], working["benefit_cap_reduction"][i] ), c 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..1207239f4f --- /dev/null +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -0,0 +1,899 @@ +"""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 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: + +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 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, + otherwise Universal Credit), and with no reg. 3(3) single claim the two + 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 + 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): + # 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): + 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, + benunit_inputs={}, + ) + 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), + # 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), + ), + ] + + +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"]} + 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"]: + 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_allowed_bedrooms", + "housing_benefit_LHA_allowed_bedrooms", + "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), + ) + # 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. + 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"] + # 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[compared], claimed[compared], err_msg=str(population) + ) + no_single_claim = ~v["uc_member_of_couple_claims_as_single_person"].astype(bool) + compared = no_single_claim & same_bedrooms + np.testing.assert_array_equal(uc[compared], hb[compared], 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, + 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] + 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] + # 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" + 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. + 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] + # 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 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 diff --git a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py index a1eea4e249..7748b39a26 100644 --- a/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py +++ b/policyengine_uk/tests/test_state_pension_credit_qualifying_age.py @@ -184,7 +184,11 @@ def assert_consumers_follow_the_qualifying_age(births, male, year): assert np.array_equal( sim.calculate("council_tax_reduction_pensioner", year), attained ) - assert np.array_equal(sim.calculate("is_benefit_cap_exempt_other", year), attained) + # HB Regs 2006 reg 5: Housing Benefit under the pension-age regulations + # has no cap. + assert np.array_equal( + sim.calculate("is_housing_benefit_benefit_cap_exempt", year), attained + ) aged = sim.tax_benefit_system.parameters( f"{year}-06-01" ).gov.dwp.housing_benefit.allowances.single.aged diff --git a/policyengine_uk/tests/test_uc_claimant_income_properties.py b/policyengine_uk/tests/test_uc_claimant_income_properties.py index b03ea77dd3..19ac9c1b77 100644 --- a/policyengine_uk/tests/test_uc_claimant_income_properties.py +++ b/policyengine_uk/tests/test_uc_claimant_income_properties.py @@ -300,7 +300,7 @@ def test_dependants_earnings_do_not_lift_the_benefit_cap(units, year): err_msg=f"{v}: {units}", ) assert not np.any( - np.asarray(with_income.calculate("is_benefit_cap_exempt_earnings", year)) + np.asarray(with_income.calculate("is_uc_benefit_cap_exempt_earnings", year)) ), units 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_income_from_capital_properties.py b/policyengine_uk/tests/test_uc_income_from_capital_properties.py index 4a07d757fd..1af5f27cf0 100644 --- a/policyengine_uk/tests/test_uc_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_uc_income_from_capital_properties.py @@ -87,7 +87,7 @@ "uc_maximum_amount", "uc_assessable_capital", "is_uc_eligible", - "is_benefit_cap_exempt", + "is_uc_benefit_cap_exempt", ] @@ -256,10 +256,10 @@ def test_interest_dividends_and_rent_leave_the_award_unchanged(units, scale, yea # The cap's earnings exception can change with the income (#1986), so # the capped award is compared only where the exemption is the same. same_cap = unchanged & ( - scaled["is_benefit_cap_exempt"] == base["is_benefit_cap_exempt"] + scaled["is_uc_benefit_cap_exempt"] == base["is_uc_benefit_cap_exempt"] ) for variable in UC_VARIABLES: - if variable == "is_benefit_cap_exempt": + if variable == "is_uc_benefit_cap_exempt": continue compared = same_cap if variable == "universal_credit" else unchanged np.testing.assert_allclose( diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 266cd3a93a..f6a782458e 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -29,27 +29,28 @@ claimants', and the sum of each member's capital calculated alone, the other member's Lifetime ISA included. 5. Benefit cap: in families whose children are all under 16, the - single-claimant rate applies exactly when there is no child (reg. 80A(2); - a 16 or 17-year-old is a qualifying young person only in qualifying - education, which the generator leaves unset). The other member's - disability benefits, LCWRA, caring, AFCS and contributory ESA lift no cap - (reg. 83(1)), so both cap exceptions equal the joint claimants' with those - removed. The model takes any contributory ESA as including the support - component (reg. 83(1)(a) needs the support component); that proxy is the - model's, not this change's. The second exception is also met where - Housing Benefit falls under the pension-age regulations, which the model - applies to a family with a member over the qualifying age for State - Pension Credit and no Universal Credit award. Whether there is an award - can differ between the single claim and the stripped calculation, either - way round. The single claim has a single claimant's amounts (reg. 36(3)) - and still counts the other member's income, such as contributory ESA - (reg. 22(3)), so its award can be the lower. It also keeps the work - allowance the other member's limited capability gives (reg. 22(3)), - which stripping removes, so its award can be the higher. So each - calculation's second exception is checked against its own inputs (the - route as that calculation gives it, or a claimant's AFCS or contributory - ESA), and the two are compared only for families the pension-age route - reaches in neither (see off_pension_age_route). + Universal Credit single-claimant rate applies exactly when there is no + child (reg. 80A(2); a 16 or 17-year-old is a qualifying young person only + in qualifying education, which the generator leaves unset). The other + member's disability benefits, LCWRA, caring, AFCS and contributory ESA + lift no Universal Credit cap (reg. 83(1)), so the Universal Credit + exceptions equal the joint claimants' with those removed, and a + claimant's own AFCS or contributory ESA lifts it. The model takes any + contributory ESA as including the support component (reg. 83(1)(a) needs + the support component); that proxy is the model's, not this change's. + 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. Its cap exception is also met where Housing Benefit + falls under the pension-age regulations, which the model applies to a + family with a member over the qualifying age for State Pension Credit and + no Universal Credit award. Whether there is an award can differ between + the single claim and the joint claimants, either way round: the single + claim has a single claimant's amounts (reg. 36(3)) but still counts the + other member's income (reg. 22(3)). So within each calculation the route + lifts the Housing Benefit cap wherever it applies, and the two are + compared only for families the route reaches in neither (see + off_pension_age_route). 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 @@ -57,12 +58,13 @@ 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. - Such a flag leaves no one able to claim Universal Credit, so for the - second cap exception each calculation is again checked against its own - inputs, and families on the pension-age route in either calculation are - left out of the comparison (see off_pension_age_route). + couple) leaves the Universal Credit cap rate, cap exceptions and shared + accommodation test, and their Housing Benefit counterparts, as they were + without the flag. Such a flag leaves no one able to claim Universal + Credit, so families on the pension-age route in either calculation are + left out of the comparison of the Housing Benefit cap exception (see + off_pension_age_route), and each calculation's Universal Credit exception + is checked against its own inputs (a claimant's AFCS or contributory ESA). Each property also runs on EXAMPLE_FAMILIES, built so that the cases the properties are about occur, each in a family of its own: an other member @@ -335,10 +337,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", "housing_benefit_pension_age_regulations_apply", ] PENSION_AGE_ROUTE = "housing_benefit_pension_age_regulations_apply" @@ -347,9 +353,9 @@ def situation(families, year, mode): def off_pension_age_route(*calculations): """The families that no calculation puts on the pension-age route. - is_benefit_cap_exempt_other also lifts the cap where Housing Benefit falls - under the pension-age regulations (HB (SPC) Regs 2006 reg. 5; the cap's - age exception, SI 2014/1230 reg. 60C). The model takes that route for a + is_housing_benefit_benefit_cap_exempt also lifts the cap where Housing + Benefit falls under the pension-age regulations (HB (SPC) Regs 2006 reg. + 5); Universal Credit has no such exception. The model takes that route for a family with a claimant or partner over the qualifying age for State Pension Credit and no Universal Credit award. Whether there is an award can differ between the calculations, either way round: a single claim has @@ -360,22 +366,20 @@ def off_pension_age_route(*calculations): calculation the route lifts the cap wherever it applies. """ for values in calculations: - assert np.all(values["is_benefit_cap_exempt_other"][values[PENSION_AGE_ROUTE]]) + assert np.all( + values["is_housing_benefit_benefit_cap_exempt"][values[PENSION_AGE_ROUTE]] + ) return ~np.logical_or.reduce([values[PENSION_AGE_ROUTE] for values in calculations]) -def exempt_other_from_inputs(families, values, members): - """is_benefit_cap_exempt_other as the inputs give it, with no mask. +def assert_own_afcs_or_esa_lifts_the_uc_cap(families, values, members): + """A claimant's AFCS or contributory ESA lifts the Universal Credit cap. - The pension-age route as the same calculation gives it (taken from the - model; test_benefit_cap_and_ctr_pension_age_properties.py and - benefit_cap_pension_age.yaml test the route itself), or AFCS or - contributory ESA of one of ``members`` (reg. 83(1)(a) and (e); the model - takes any contributory ESA as including the support component). - ``members`` are the people whose own benefits count: the claimant alone - on a single claim, in a stripped calculation (the other member's - benefits removed) or with no partner, and both members of a couple - neither of whom is excluded. + Reg. 83(1)(a) and (e); the model takes any contributory ESA as including + the support component. ``members`` are the people whose own benefits + count: the claimant alone on a single claim, in a stripped calculation + (the other member's benefits removed) or with no partner, and both + members of a couple neither of whom is excluded. """ own = np.array( [ @@ -386,7 +390,7 @@ def exempt_other_from_inputs(families, values, members): for family in families ] ) - return values[PENSION_AGE_ROUTE] | own + assert np.all(values["is_uc_benefit_cap_exempt_specified_benefit"][own]), families def calculate(families, year, mode, extra=()): @@ -395,6 +399,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") @@ -533,6 +539,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 @@ -542,26 +549,39 @@ 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], ) - np.testing.assert_array_equal( - single["is_benefit_cap_exempt_health_disability"], - stripped["is_benefit_cap_exempt_health_disability"], - err_msg=f"is_benefit_cap_exempt_health_disability: {families}", - ) + for variable in [ + "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}" + ) for values in [single, stripped]: + assert_own_afcs_or_esa_lifts_the_uc_cap(families, values, ["claimant"]) + # 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_young_individual", + "housing_benefit_LHA_category", + ]: np.testing.assert_array_equal( - values["is_benefit_cap_exempt_other"], - exempt_other_from_inputs(families, values, ["claimant"]), - err_msg=f"is_benefit_cap_exempt_other: {families}", + single[variable], joint[variable], err_msg=f"{variable}: {families}" ) - off_route = off_pension_age_route(single, stripped) + off_route = off_pension_age_route(single, joint) np.testing.assert_array_equal( - single["is_benefit_cap_exempt_other"][off_route], - stripped["is_benefit_cap_exempt_other"][off_route], - err_msg=f"is_benefit_cap_exempt_other: {families}", + single["is_housing_benefit_benefit_cap_exempt"][off_route], + joint["is_housing_benefit_benefit_cap_exempt"][off_route], + err_msg=f"is_housing_benefit_benefit_cap_exempt: {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 @@ -610,12 +630,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") @@ -631,17 +657,13 @@ def test_a_flag_marking_no_single_claim_leaves_shared_rules_alone(families, year (alone_flagged, ["claimant"]), (solo, ["claimant"]), ]: - np.testing.assert_array_equal( - values["is_benefit_cap_exempt_other"], - exempt_other_from_inputs(families, values, members), - err_msg=f"is_benefit_cap_exempt_other: {families}", - ) + assert_own_afcs_or_esa_lifts_the_uc_cap(families, values, members) for flagged, unflagged in [(both, joint), (alone_flagged, solo)]: off_route = off_pension_age_route(flagged, unflagged) for variable in shared: compared = ( off_route - if variable == "is_benefit_cap_exempt_other" + if variable == "is_housing_benefit_benefit_cap_exempt" else np.ones_like(off_route) ) np.testing.assert_array_equal( @@ -669,21 +691,27 @@ def test_examples_reach_the_cases(): assert single["uc_carer_element"][family] == 0 < joint["uc_carer_element"][family] assert single["uc_carer_element"][i["carer_claimant"]] > 0 # Reg. 83(1): the other member's LCWRA and PIP, AFCS or ESA lift the - # joint claimants' cap only; the claimant's AFCS lifts it. - family = i["young_renter"] - assert not single["is_benefit_cap_exempt_health_disability"][family] - assert joint["is_benefit_cap_exempt_health_disability"][family] - for family in [i["afcs_partner"], i["esa_partner"]]: - assert not single["is_benefit_cap_exempt_other"][family] - assert joint["is_benefit_cap_exempt_other"][family] - assert not single["is_benefit_cap_exempt_health_disability"][family] - assert joint["is_benefit_cap_exempt_health_disability"][family] - assert single["is_benefit_cap_exempt_other"][i["afcs_claimant"]] + # joint claimants' cap only; the claimant's AFCS lifts it. HB reg. + # 75F(1): the partner's benefits lift the HB cap either way. + for family in [i["young_renter"], i["afcs_partner"], i["esa_partner"]]: + assert not single["is_uc_benefit_cap_exempt_specified_benefit"][family] + assert joint["is_uc_benefit_cap_exempt_specified_benefit"][family] + assert single["is_housing_benefit_benefit_cap_exempt_specified_benefit"][family] + assert single["is_uc_benefit_cap_exempt_specified_benefit"][i["afcs_claimant"]] # Reg. 80A(2) and Sch 4 para 28(2): single rate and shared accommodation. + # HB regs. 2(1), 13D(2) and 75CA(2): a couple either way. family = i["young_renter"] - assert single["is_benefit_cap_single_claimant_rate"][family] - assert not joint["is_benefit_cap_single_claimant_rate"][family] + assert single["is_uc_benefit_cap_single_claimant_rate"][family] + assert not joint["is_uc_benefit_cap_single_claimant_rate"][family] + assert not single["is_housing_benefit_benefit_cap_single_claimant_rate"][family] assert single["is_lha_shared_accommodation_rate_specified_renter"][family] assert not joint["is_lha_shared_accommodation_rate_specified_renter"][family] + assert single["LHA_category"][family] == "A" + assert not single["is_housing_benefit_young_individual"][family] + assert ( + single["housing_benefit_LHA_category"][family] + == joint["housing_benefit_LHA_category"][family] + == "B" + ) # Reg. 22(3): the other member's LCW gives the work allowance either way. assert single["is_uc_work_allowance_eligible"][family] 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 80e936c34b..51647c41ff 100644 --- a/policyengine_uk/utils/benefit_unit.py +++ b/policyengine_uk/utils/benefit_unit.py @@ -22,3 +22,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 4df69d4cbc..175ee17968 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -7,17 +7,24 @@ """ +def couple_members(person, period): + """The claimant and any partner: at most two people in a benefit unit. + + Where the data flag more (an adult child in the parents' benefit unit), + the two eldest are the couple. + """ + age = person("age", period) + flagged = person("is_uc_claimant", period) + return flagged & (person.get_rank(person.benunit, -age, condition=flagged) < 2) + + def claimants(person, period): """The single claimant or the joint claimants. - The members of the couple are `is_uc_assessed_claimant`: at most two - people in a benefit unit. A partner who cannot be a joint claimant (reg. - 3(3)) is a member of the couple but not a claimant: no work-related group - or floor applies to them. + A partner who cannot be a joint claimant (reg. 3(3)) is a member of the + couple but not a claimant: no work-related group or floor applies to them. """ - return person("is_uc_assessed_claimant", period) & ~person( - "uc_is_ineligible_partner", period - ) + return couple_members(person, period) & ~person("uc_is_ineligible_partner", period) def other_member_of_single_claim(person, period): @@ -32,38 +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 stays_on_legacy_benefits(benunit, period): - """Whether the family stays on legacy benefits rather than claiming UC. - - A family that reports a legacy benefit (`claims_legacy_benefits`) and - would not claim Universal Credit keeps its legacy awards; this is the - route `housing_benefit_eligible` gives a continuing working-age Housing - Benefit award. A family that would claim Universal Credit is calculated - on Universal Credit whatever it reports. - """ - return benunit("claims_legacy_benefits", period) & ~benunit( - "would_claim_uc", 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 that stays on legacy benefits keeps the couple rules; - a family claiming Universal Credit gets the Universal Credit rule. - """ - return benunit( - "uc_member_of_couple_claims_as_single_person", period - ) & ~stays_on_legacy_benefits(benunit, 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) & ~stays_on_legacy_benefits( - person.benunit, period - ) 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 7d021f0b6f..fce41d35fb 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py @@ -3,28 +3,18 @@ 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. 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 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", - ] - # The cap applies to "the welfare benefits to which a single person or - # couple is entitled" (WRA 2012 s. 96(1); UC Regs 2013 reg. 80(1)), so - # a dependant's own contributory benefit is not part of the total. - claimants = benunit.members("is_uc_assessed_claimant", period) - total = add_for_members(benunit, period, CAPPED_BENEFITS, claimants) - return max_(total - 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..67563e5b8d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -0,0 +1,47 @@ +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). Only the benefits of the " + "single person or couple count (is_uc_assessed_claimant): a " + "dependant's own benefits do not." + ) + 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): + # The cap applies to "the welfare benefits to which a single person or + # couple is entitled" (WRA 2012 s. 96(1); UC Regs 2013 reg. 80(1)), so + # a dependant's own contributory benefit is not part of the total. + claimants = benunit.members("is_uc_assessed_claimant", period) + return add_for_members(benunit, period, CAPPED_BENEFITS, claimants) 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..5823ddf5b0 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py @@ -0,0 +1,58 @@ +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 and armed forces independence +# payment (ea), and entitlement to carer's allowance (h) or carer support +# payment (ha). +SPECIFIED_PERSONAL_BENEFITS = [ + "attendance_allowance", + "armed_forces_independence_payment", + # Entitlement, not payment: reg. 75F(1)(h)-(ha) read "is entitled to", so + # an overlapping benefit that reduces the payment to nil still counts. + "is_entitled_to_carer_benefit", + "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, armed forces independence payment, " + "or entitlement to 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_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 6f525e3d70..a2fe00a90b 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 @@ -56,7 +56,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 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 1bdb797d6a..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ /dev/null @@ -1,64 +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): - person = benunit.members - - # 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 Regs 2013 reg. 82(1)(a) tests "the claimant's earned income or, - # if the claimant is a member of a couple, the couple's combined - # earned income": a dependant's earnings do not count. - claimant = person("is_uc_assessed_claimant", period) - uc_earned = benunit.sum( - ( - person("employment_income", period) - + person("self_employment_income", period) - - person("income_tax", period) - - person("national_insurance", period) - ) - * claimant - ) - 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 69e1116338..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): - person = benunit.members - - # 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", - # Entitlement, not payment: HB Regs 2006 reg 75F(1)(h)-(ha) and UC - # Regs 2013 reg 83(1)(i)-(ia) exempt a person "entitled to" Carer's - # Allowance or Carer Support Payment, even if an overlapping benefit - # reduces it to nil. - "is_entitled_to_carer_benefit", - "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 b6a3608a2b..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +++ /dev/null @@ -1,50 +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", - "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", - "https://www.legislation.gov.uk/uksi/2013/376/regulation/83", - "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60C", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/75A", - "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", - ) - - def formula(benunit, period, parameters): - person = benunit.members - # The benefit cap applies to a Universal Credit award whatever the - # claimants' ages (UC Regs 2013 regs 79, 82 and 83), including a - # mixed-age couple's joint award (reg 3(2)(a)). The one age-based - # exception, SI 2014/1230 reg 60C (NI: SR 2016/226 reg 61C), covers - # only claims where every claimant has reached the qualifying age for - # State Pension Credit. The cap also applies to Housing Benefit under - # the working-age regulations (HB Regs 2006 Part 8A). Housing Benefit - # under the pension-age regulations, which never apply to a family on - # Universal Credit, has no cap. - pension_age_housing_benefit = benunit( - "housing_benefit_pension_age_regulations_apply", period - ) - - # 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 pension_age_housing_benefit | 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 793001b342..0000000000 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py +++ /dev/null @@ -1,42 +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 that stays on legacy " - "benefits (reports one and would not claim Universal Credit) 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_lha_shared_accommodation_rate_specified_renter.py b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py index 92578088c2..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 stays on legacy benefits (reports one and " - "would not claim Universal Credit)." + "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/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..787ce359a8 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt.py @@ -0,0 +1,29 @@ +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)), or SI 2014/1230 reg. 60C disapplies it because every " + "claimant has reached the qualifying age for State Pension Credit. " + "That is the cap's only age exception: 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", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60C", + ) + + def formula(benunit, period, parameters): + return ( + benunit("is_uc_benefit_cap_exempt_earnings", period) + | benunit("is_uc_benefit_cap_exempt_specified_benefit", period) + | benunit("is_uc_benefit_cap_exempt_qualifying_age", 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..5f62d2d43a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py @@ -0,0 +1,40 @@ +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): + person = benunit.members + # Earnings exemption for UC (£846/month = £10,152/year) + # Note: Only check earned income, not UC amount itself to avoid circular dependency + # Reg. 82(1)(a) tests "the claimant's earned income or, if the + # claimant is a member of a couple, the couple's combined earned + # income": a dependant's earnings do not count. + claimant = person("is_uc_assessed_claimant", period) + uc_earned = benunit.sum( + ( + person("employment_income", period) + + person("self_employment_income", period) + - person("income_tax", period) + - person("national_insurance", period) + ) + * claimant + ) + 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_qualifying_age.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_qualifying_age.py new file mode 100644 index 0000000000..78de710755 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_qualifying_age.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class is_uc_benefit_cap_exempt_qualifying_age(Variable): + value_type = bool + entity = BenUnit + label = "Exempt from the Universal Credit benefit cap at State Pension Credit age" + documentation = ( + "Whether the single claimant, or both joint claimants, have reached " + "the qualifying age for State Pension Credit. SI 2014/1230 reg. 60C " + "disapplies the cap (UC Regs 2013 reg. 79) for a qualifying claim " + "made by such claimants. A claimant over that age does not meet the " + "basic condition in WRA 2012 s. 4(1)(b); a couple may claim jointly " + "where only one member is over it (UC Regs 2013 reg. 3(2)(a)), and " + "reg. 60A waives the condition only for a qualifying claim by " + "claimants migrated from working tax credit. So an award to such " + "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. 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 = ( + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60C", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60A", + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ) + + def formula(benunit, period, parameters): + person = benunit.members + # 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) 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..8026e200a4 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py @@ -0,0 +1,77 @@ +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), +# which reg. 2 defines to include armed forces independence payment, +# disability living allowance (f), personal independence payment (g), and +# entitlement to carer's allowance (i) or carer support payment (ia). +SPECIFIED_PERSONAL_BENEFITS = [ + "attendance_allowance", + "armed_forces_independence_payment", + # Entitlement, not payment: reg. 83(1)(i)-(ia) read "is entitled to", so + # an overlapping benefit that reduces the payment to nil still counts. + "is_entitled_to_carer_benefit", + "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 (including armed " + "forces independence payment, reg. 2), a war pension or armed forces " + "compensation, disability living allowance, personal " + "independence payment, or entitlement to 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..f0b18ea2db --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -0,0 +1,35 @@ +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. 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 + 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