diff --git a/changelog.d/uc-claim-at-legacy-closure.added.md b/changelog.d/uc-claim-at-legacy-closure.added.md new file mode 100644 index 0000000000..2da4fc8b22 --- /dev/null +++ b/changelog.d/uc-claim-at-legacy-closure.added.md @@ -0,0 +1 @@ +- Move families onto Universal Credit when DWP closes a legacy benefit they receive: tax credits from 2025-26, Income Support and income-based JSA from 2026-27. Families on Pension Credit, and families on Child Tax Credit only that meet the Pension Credit age conditions, go to Pension Credit instead (SI 2019/167 art. 3A). Pension-age families on Working Tax Credit can claim Universal Credit with the upper age condition waived (SI 2014/1230 reg 60A), and protected mixed-age couples on it can claim jointly (UC Regulations 2013 reg 3(2)(a)); either leaves the pension-age route only by claiming and being eligible. A new dataset input, `would_claim_uc_at_legacy_closure` (default true), says whether the family claims. `claims_universal_credit` replaces `would_claim_uc` wherever the model asks whether a family claims Universal Credit, including whose claim counts a household's non-dependants. The family's other legacy awards end at the closure whether or not it claims (SI 2014/1230 regs 8 and 46). diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index 7f2e0d6738..1578e46643 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 `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\nUniversal Credit is paid only to families that claim it (`claims_universal_credit`). There are two routes. The input `would_claim_uc` is drawn stochastically when the dataset is built, so that caseload aggregates match published DWP claimant numbers rather than the full eligible population. The second route covers a family whose legacy benefit has closed: tax credits from 2025-26, Income Support and income-based JSA from 2026-27 (`legacy_benefits_closed`). DWP's migration notice ended all such a family's legacy awards, and it claims Universal Credit if the input `would_claim_uc_at_legacy_closure` is true. The dataset draws that input at DWP's Move to Universal Credit claim rates by legacy benefit combination. Families DWP sent to Pension Credit instead are excluded: those on Pension Credit, and those on Child Tax Credit only that meet the Pension Credit age conditions (SI 2019/167 art. 3A). Pension-age families on Working Tax Credit can claim, because the upper age condition is waived for them (SI 2014/1230 reg 60A). Protected mixed-age couples on it can claim jointly (UC Regulations 2013 reg 3(2)(a)). Such a family leaves the pension-age route if it claims and is eligible. A protected couple that does not claim keeps its route, because it can reclaim Housing Benefit (SI 2019/37 art. 4(3)(b)(ii)). Both inputs default to true. To model a family that does not claim, set both to false. Not modelled: transitional protection, including the transitional capital disregard (SI 2014/1230 regs 48-57); nil-award Working Tax Credit families, which DWP also moved to Universal Credit; and the choice between routes, since a pension-age family that claims may get less than it would by declining.\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/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_universal_credit.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_universal_credit.yaml new file mode 100644 index 0000000000..4e72a3c1c8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_universal_credit.yaml @@ -0,0 +1,556 @@ +# Universal Credit claims when a legacy benefit closes (managed migration). +# Tax credits closed from 6 April 2025 (PE year 2025) and Income Support and +# income-based JSA from 1 April 2026 (PE year 2026). Once one of a working-age +# family's reported legacy benefits has closed, all its legacy awards end +# (SI 2014/1230 regs 8 and 46) and it claims Universal Credit when +# would_claim_uc_at_legacy_closure is true. + +- name: Before tax credits close, a tax credit and Housing Benefit family keeps both awards + period: 2024 + input: + people: + parent: + age: 30 + child_tax_credit_reported: 3_000 + housing_benefit_reported: 4_000 + child: + age: 4 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: false + claims_universal_credit: false + housing_benefit_eligible: true + universal_credit: 0 + +- name: Once tax credits close, the family claims Universal Credit and its Housing Benefit ends + period: 2025 + input: + people: + parent: + age: 30 + child_tax_credit_reported: 3_000 + housing_benefit_reported: 4_000 + child: + age: 4 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: true + housing_benefit_eligible: false + housing_benefit: 0 + +- name: A family that does not claim at the closure loses its Housing Benefit and gets no Universal Credit + period: 2025 + input: + people: + parent: + age: 30 + child_tax_credit_reported: 3_000 + housing_benefit_reported: 4_000 + child: + age: 4 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: false + housing_benefit_eligible: false + housing_benefit: 0 + universal_credit: 0 + +- name: Housing Benefit alone has not closed, so a Housing Benefit family keeps its award + period: 2027 + input: + people: + adult: + age: 40 + housing_benefit_reported: 4_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [adult] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: false + claims_universal_credit: false + housing_benefit_eligible: true + universal_credit: 0 + +- name: Income-related ESA continues before tax credits close + period: 2024 + input: + people: + parent: + age: 35 + child_tax_credit_reported: 3_000 + esa_income_reported: 5_000 + child: + age: 8 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + households: + household: + members: [parent, child] + output: + legacy_benefits_closed: false + esa_income_eligible: true + +- name: Income-related ESA ends with the tax credit closure + period: 2025 + input: + people: + parent: + age: 35 + child_tax_credit_reported: 3_000 + esa_income_reported: 5_000 + child: + age: 8 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + households: + household: + members: [parent, child] + output: + legacy_benefits_closed: true + claims_universal_credit: true + esa_income_eligible: false + esa_income: 0 + +- name: Income Support and Housing Benefit continue in 2025-26 + period: 2025 + input: + people: + parent: + age: 25 + income_support_reported: 5_000 + housing_benefit_reported: 4_000 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: false + claims_universal_credit: false + housing_benefit_eligible: true + +- name: Once Income Support closes, the family claims Universal Credit and its Housing Benefit ends + period: 2026 + input: + people: + parent: + age: 25 + income_support_reported: 5_000 + housing_benefit_reported: 4_000 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: true + housing_benefit_eligible: false + income_support: 0 + +- name: Income Support ends with the tax credit closure for a family on both + period: 2025 + input: + people: + parent: + age: 25 + income_support_reported: 5_000 + child_tax_credit_reported: 3_000 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + households: + household: + members: [parent, child] + output: + legacy_benefits_closed: true + income_support: 0 + +- name: Income-based JSA continues before its closure + period: 2025 + input: + people: + adult: + age: 40 + jsa_income_reported: 4_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + households: + household: + members: [adult] + output: + legacy_benefits_closed: false + jsa_income_eligible: true + +- name: Income-based JSA closure moves the family + period: 2026 + input: + people: + adult: + age: 40 + jsa_income_reported: 4_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [adult] + output: + legacy_benefits_closed: true + claims_universal_credit: false + jsa_income_eligible: false + universal_credit: 0 + +- name: A pension-age family is not moved to Universal Credit when tax credits close + period: 2025 + input: + people: + grandparent: + age: 70 + child_tax_credit_reported: 3_000 + housing_benefit_reported: 4_000 + child: + age: 10 + benunits: + benunit: + members: [grandparent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [grandparent, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: false + claims_universal_credit: false + housing_benefit_eligible: true + +- name: A family that already claims Universal Credit claims it in every year + period: 2024 + input: + people: + adult: + age: 40 + benunits: + benunit: + members: [adult] + would_claim_uc: true + would_claim_uc_at_legacy_closure: false + households: + household: + members: [adult] + output: + legacy_benefits_closed: false + claims_universal_credit: true + +# Pension-age and protected mixed-age families. A tax credit closure notice +# sent families on Pension Credit, and families on Child Tax Credit but not +# Working Tax Credit that meet the Pension Credit age conditions, to Pension +# Credit (SI 2019/167 art. 3A(1)). Families on Working Tax Credit got a +# Universal Credit migration notice, and a pension-age one not entitled to +# Pension Credit has the Universal Credit upper age condition waived +# (SI 2014/1230 reg 60A; DWP HB circular A9/2024 paras 5-14). + +- name: A protected mixed-age couple on Working Tax Credit claims Universal Credit at the closure and leaves the pension-age route + period: 2025 + input: + people: + older: + age: 72 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + younger: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + has_mixed_age_couple_pension_credit_saving: true + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [older, younger] + tenure_type: RENT_FROM_COUNCIL + output: + meets_pension_credit_age_conditions: true + legacy_benefits_closed: true + claims_universal_credit: true + is_uc_eligible: true + left_pension_route_at_legacy_closure: true + housing_benefit_eligible: false + is_pension_credit_eligible: false + +- name: A protected mixed-age couple on Working Tax Credit that does not claim keeps the pension-age route + # SI 2019/37 art. 4(3)(b)(ii) lets it reclaim Housing Benefit and keep the + # saving (DWP HB circular A9/2024 paras 12-13). + period: 2025 + input: + people: + older: + age: 72 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + younger: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [older, younger] + has_mixed_age_couple_pension_credit_saving: true + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [older, younger] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: false + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + is_pension_credit_eligible: true + universal_credit: 0 + +- name: A protected mixed-age couple on Child Tax Credit only takes the Pension Credit route and keeps its Housing Benefit + period: 2025 + input: + people: + older: + age: 72 + is_claimant_or_partner: true + child_tax_credit_reported: 3_000 + housing_benefit_reported: 4_000 + younger: + age: 40 + is_claimant_or_partner: true + child: + age: 10 + is_claimant_or_partner: false + benunits: + benunit: + members: [older, younger, child] + has_mixed_age_couple_pension_credit_saving: true + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [older, younger, child] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: false + claims_universal_credit: false + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + +- name: A pension-age Working Tax Credit recipient not on Pension Credit can claim Universal Credit at the closure + period: 2025 + input: + people: + adult: + age: 70 + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [adult] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: true + is_uc_eligible: true + housing_benefit_eligible: false + is_pension_credit_eligible: false + +- name: A pension-age Working Tax Credit recipient that does not claim stays on the pension-age route + period: 2025 + input: + people: + adult: + age: 70 + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [adult] + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: false + is_uc_eligible: false + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + +- name: A pension-age Working Tax Credit recipient already on Pension Credit takes the Pension Credit route + period: 2025 + input: + people: + adult: + age: 70 + working_tax_credit_reported: 2_000 + pension_credit_reported: 1_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [adult] + output: + legacy_benefits_closed: false + claims_universal_credit: false + is_uc_eligible: false + +- name: Under a reform keeping tax credits open, a family moved off legacy benefits by the Income Support closure gets no Working Tax Credit + period: 2026 + input: + gov.dwp.tax_credits.active: true + people: + parent: + age: 35 + weekly_hours: 30 + employment_income: 5_000 + working_tax_credit_reported: 3_000 + income_support_reported: 4_000 + child: + age: 3 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [parent, child] + output: + legacy_benefits_closed: true + claims_universal_credit: true + is_WTC_eligible: false + tax_credits: 0 + +- name: A pension-age Working Tax Credit recipient refused Universal Credit on capital stays on the pension-age route + # The transitional capital disregard (SI 2014/1230 reg 51) is not modelled. + period: 2025 + input: + people: + adult: + age: 70 + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + benunits: + benunit: + members: [adult] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [adult] + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + claims_universal_credit: true + is_uc_eligible: false + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + +- name: A closure claim counts the household's non-dependants like any Universal Credit claim + # UC Regs 2013 Sch 4 para 9: a non-dependant son aged 24 in the household. + # The 2025-26 housing cost contribution is 93.02 a month (12 x 93.02). + period: 2025 + input: + people: + head: + age: 40 + is_household_head: true + working_tax_credit_reported: 2_000 + son: + age: 24 + employment_income: 20_000 + benunits: + head_family: + members: [head] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + son_family: + members: [son] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + home: + members: [head, son] + rent: 9_000 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + claims_universal_credit: [true, false] + uc_non_dependants_counted: [true, false] + uc_non_dep_deductions: [1_116.24, 0] + LHA_allowed_bedrooms: [2, 1] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml index 760c906392..a908734e91 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/childcare_grant/childcare_grant.yaml @@ -324,6 +324,8 @@ benunits: benunit: members: ["student", "child"] + # Tax credits closed in 2025; keep the award to test the overlap. + legacy_benefits_closed: false households: household: members: ["student", "child"] diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index ccebd466a4..0b68d8b3b1 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -395,7 +395,7 @@ def test_no_op_without_sharers_boarders_or_lodgers(population): claims_uc = calc(sim, "uc_non_dependants_counted") assert np.array_equal( claims_uc, - head & calc(sim, "is_uc_eligible") & calc(sim, "would_claim_uc"), + head & calc(sim, "is_uc_eligible") & calc(sim, "claims_universal_credit"), ) person_head_family = sim.map_result(head, "benunit", "person") > 0 individual = calc(sim, "uc_individual_non_dep_deduction") diff --git a/policyengine_uk/tests/test_uc_legacy_closure_properties.py b/policyengine_uk/tests/test_uc_legacy_closure_properties.py new file mode 100644 index 0000000000..aa1262a78e --- /dev/null +++ b/policyengine_uk/tests/test_uc_legacy_closure_properties.py @@ -0,0 +1,328 @@ +"""Property-based tests for Universal Credit claims when legacy benefits close. + +Tax credits closed from 6 April 2025 and Income Support and income-based JSA +from 1 April 2026. DWP moved each family off its legacy benefits with a notice +covering all of them. A Universal Credit migration notice ends the awards on a +claim (SI 2014/1230 reg 8) or at its deadline (reg 46). A tax credit closure +notice sent families on Pension Credit, and families on Child Tax Credit but +not Working Tax Credit that meet the Pension Credit age conditions, to Pension +Credit instead (SI 2019/167 art. 3A). Pension-age families on Working Tax +Credit can claim Universal Credit (reg 60A), and a protected mixed-age couple +claims it jointly. Datasets say which families claim through +would_claim_uc_at_legacy_closure. + +Invariants, for any generated population of families in 2024 to 2027: + +1. Closure: legacy_benefits_closed holds exactly for a family that reports + tax credits from 2025 and is not sent to Pension Credit, or reports Income + Support or income-based JSA from 2026, is not on Pension Credit and is not + wholly pension age. +2. Claim: claims_universal_credit is would_claim_uc, or the closure with the + closure claim flag; Universal Credit is paid only to claiming families. +3. Migration ends every legacy award: a family moved off legacy benefits gets + no tax credits, Income Support, income-related ESA or income-based JSA. + It gets no Housing Benefit unless it keeps the pension-age route by not + claiming (or by being refused Universal Credit). A family that leaves + that route gets no Pension Credit either, and any closure claimant + within the capital limit is eligible for Universal Credit. +4. Exclusive: no family gets Universal Credit with Pension Credit or with + Housing Benefit. +5. Metamorphic, the flag is inert unless it decides a claim: negating + would_claim_uc_at_legacy_closure changes no benefit for a family not + moved off legacy benefits, or for a working-age one that would claim + Universal Credit anyway. In particular nothing changes in 2024, before + any closure. +6. Differential: a working-age family claiming at the closure gets the + benefits it would get if would_claim_uc were true. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEARS = (2024, 2025, 2026, 2027) +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 67 and over is pension age in every year here; 60 and under working age. +PENSION_AGE = st.integers(67, 90) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single": [("claimant", WORKING_AGE)], + "couple": [("claimant", WORKING_AGE), ("partner", WORKING_AGE)], + "lone_parent": [("claimant", WORKING_AGE), ("child", st.integers(0, 15))], + "couple_parents": [ + ("claimant", WORKING_AGE), + ("partner", WORKING_AGE), + ("child", st.integers(0, 15)), + ], + "pensioner": [("claimant", PENSION_AGE)], + "pensioner_with_child": [("claimant", PENSION_AGE), ("child", st.integers(5, 15))], + "mixed_age": [("claimant", PENSION_AGE), ("partner", WORKING_AGE)], + "mixed_age_parents": [ + ("claimant", PENSION_AGE), + ("partner", WORKING_AGE), + ("child", st.integers(0, 15)), + ], +} +LEGACY = ( + "child_tax_credit_reported", + "working_tax_credit_reported", + "housing_benefit_reported", + "esa_income_reported", + "income_support_reported", + "jsa_income_reported", + "pension_credit_reported", +) +TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] +BENEFITS = [ + "universal_credit", + "housing_benefit", + "pension_credit", + "tax_credits", + "income_support", + "esa_income", + "jsa_income", +] +OUTPUTS = BENEFITS + [ + "legacy_benefits_closed", + "claims_universal_credit", + "left_pension_route_at_legacy_closure", + "meets_pension_credit_age_conditions", + "is_mixed_age_couple", + "is_uc_eligible", + "uc_assessable_capital", +] +award = st.sampled_from([0.0, 0.0, 2_000.0, 6_000.0]) + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(sorted(SHAPES))) + reported = {name: draw(award) for name in LEGACY} + if not shape.startswith(("pensioner", "mixed_age")): + reported["pension_credit_reported"] = 0.0 + return dict( + shape=shape, + members=[(role, draw(age)) for role, age in SHAPES[shape]], + reported=reported, + saving=draw(st.booleans()), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.floats(0, 12_000)), + earnings=draw(st.sampled_from([0.0, 8_000.0, 25_000.0])), + savings=draw(st.sampled_from([0.0, 5_000.0, 20_000.0])), + would_claim_uc=draw(st.booleans()), + claims_at_closure=draw(st.booleans()), + ) + + +def every_year(value): + return {year: value for year in YEARS} + + +def situation(units, negate_flag=False, would_claim_uc=None): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, (role, age) in enumerate(unit["members"]): + name = f"p{i}_{j}" + # Set every input for every member, or it stops being an input + # for the rest of the simulation. + person = { + "age": every_year(age), + "is_claimant_or_partner": every_year(role != "child"), + "employment_income": every_year( + unit["earnings"] if role == "claimant" else 0.0 + ), + } + for name_reported in LEGACY: + amount = unit["reported"][name_reported] if role == "claimant" else 0 + person[name_reported] = every_year(amount) + people[name] = person + names.append(name) + claims_anyway = ( + unit["would_claim_uc"] if would_claim_uc is None else would_claim_uc + ) + benunits[f"b{i}"] = { + "members": names, + "would_claim_uc": every_year(claims_anyway), + "would_claim_uc_at_legacy_closure": every_year( + unit["claims_at_closure"] ^ negate_flag + ), + "has_mixed_age_couple_pension_credit_saving": every_year(unit["saving"]), + "claims_all_entitled_benefits": every_year(False), + } + households[f"h{i}"] = { + "members": names, + "rent": every_year(unit["rent"]), + "tenure_type": every_year(unit["tenure"]), + "savings": every_year(unit["savings"]), + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, **kwargs): + sim = Simulation(situation=situation(units, **kwargs)) + return { + year: {v: np.asarray(sim.calculate(v, year)) for v in OUTPUTS} for year in YEARS + } + + +def expected_closure(unit, year, pension_age_route, mixed_age): + r = {name: amount > 0 for name, amount in unit["reported"].items()} + wholly_pension_age = pension_age_route and not mixed_age + tax_credits = r["child_tax_credit_reported"] or r["working_tax_credit_reported"] + to_pension_credit = r["pension_credit_reported"] or ( + pension_age_route and not r["working_tax_credit_reported"] + ) + is_or_jsa = r["income_support_reported"] or r["jsa_income_reported"] + return (tax_credits and year >= 2025 and not to_pension_credit) or ( + is_or_jsa + and year >= 2026 + and not r["pension_credit_reported"] + and not wholly_pension_age + ) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=25)) +def test_closure_claims_and_the_end_of_legacy_awards(units): + values = calculate(units) + for year in YEARS: + v = values[year] + for i, unit in enumerate(units): + pension_age_route = bool(v["meets_pension_credit_age_conditions"][i]) + mixed_age = bool(v["is_mixed_age_couple"][i]) + closed = expected_closure(unit, year, pension_age_route, mixed_age) + assert bool(v["legacy_benefits_closed"][i]) == closed, (year, unit) + claims_at_closure = closed and unit["claims_at_closure"] + claims = unit["would_claim_uc"] or claims_at_closure + assert bool(v["claims_universal_credit"][i]) == claims, (year, unit) + if not claims: + assert v["universal_credit"][i] == 0, (year, unit) + assert not (v["universal_credit"][i] > 0 and v["pension_credit"][i] > 0) + assert not (v["universal_credit"][i] > 0 and v["housing_benefit"][i] > 0) + if not closed: + continue + for benefit in [ + "tax_credits", + "income_support", + "esa_income", + "jsa_income", + ]: + assert v[benefit][i] == 0, (year, benefit, unit) + uc_eligible = bool(v["is_uc_eligible"][i]) + # Any closure claimant within the capital limit is eligible for + # Universal Credit: on the pension-age route through a joint claim + # (reg 3(2)(a)) or the reg 60A waiver. + if claims_at_closure and v["uc_assessable_capital"][i] <= 16_000: + assert uc_eligible, (year, unit) + left = pension_age_route and claims_at_closure and uc_eligible + assert bool(v["left_pension_route_at_legacy_closure"][i]) == left + if left or not pension_age_route: + assert v["housing_benefit"][i] == 0, (year, unit) + if left: + assert v["pension_credit"][i] == 0, (year, unit) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=25)) +def test_the_closure_flag_only_decides_closure_claims(units): + drawn = calculate(units) + negated = calculate(units, negate_flag=True) + for year in YEARS: + for i, unit in enumerate(units): + # On the pension-age route would_claim_uc gives no Universal + # Credit, so there the flag alone decides the closure claim. + decides = bool(drawn[year]["legacy_benefits_closed"][i]) and ( + not unit["would_claim_uc"] + or bool(drawn[year]["meets_pension_credit_age_conditions"][i]) + ) + if year == 2024: + assert not decides, unit + if decides: + continue + for benefit in BENEFITS: + assert drawn[year][benefit][i] == negated[year][benefit][i], ( + year, + benefit, + unit, + ) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=25)) +def test_a_closure_claim_pays_the_universal_credit_of_any_claim(units): + for unit in units: + unit["claims_at_closure"] = True + at_closure = calculate(units, would_claim_uc=False) + anyway = calculate(units, would_claim_uc=True) + for year in YEARS: + v = at_closure[year] + for i, unit in enumerate(units): + # On the pension-age route only a closure claim reaches Universal + # Credit, so the two claims differ by design there. + if ( + v["legacy_benefits_closed"][i] + and not (v["meets_pension_credit_age_conditions"][i]) + ): + for benefit in BENEFITS: + assert abs(v[benefit][i] - anyway[year][benefit][i]) < 0.01, ( + year, + benefit, + unit, + ) + + +@st.composite +def paid_legacy_families(draw): + """Working-age families on tax credits and one other legacy award they + are paid in 2024: no earnings or savings, and for Income Support a lone + parent with a child under 5.""" + other = draw( + st.sampled_from( + ["income_support_reported", "esa_income_reported", "jsa_income_reported"] + ) + ) + members = [("claimant", draw(st.integers(18, 60)))] + members.append(("child", draw(st.integers(0, 4)))) + reported = {name: 0.0 for name in LEGACY} + reported["child_tax_credit_reported"] = draw(st.sampled_from([2_000.0, 6_000.0])) + reported[other] = draw(st.sampled_from([2_000.0, 6_000.0])) + reported["housing_benefit_reported"] = draw(st.sampled_from([0.0, 3_000.0])) + return dict( + shape="lone_parent", + members=members, + reported=reported, + saving=False, + tenure="RENT_FROM_COUNCIL", + rent=draw(st.floats(1_000, 8_000)), + earnings=0.0, + savings=0.0, + would_claim_uc=False, + claims_at_closure=draw(st.booleans()), + other=other, + ) + + +@PROPERTY_SETTINGS +@given(st.lists(paid_legacy_families(), min_size=1, max_size=25)) +def test_the_tax_credit_closure_ends_awards_paid_the_year_before(units): + values = calculate(units) + benefit = { + "income_support_reported": "income_support", + "esa_income_reported": "esa_income", + "jsa_income_reported": "jsa_income", + } + for i, unit in enumerate(units): + other = benefit[unit["other"]] + # The generator only makes families paid the award before 2025. + assert values[2024][other][i] > 0, unit + assert values[2025]["legacy_benefits_closed"][i], unit + assert values[2025][other][i] == 0, unit + assert values[2025]["housing_benefit"][i] == 0, unit diff --git a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py index 4810792498..299c81c1e5 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py @@ -15,4 +15,11 @@ def formula(benunit, period, parameters): ESA = parameters(period).gov.dwp.ESA.income capital = benunit("esa_income_assessable_capital", period) reported_award = add(benunit, period, ["esa_income_reported"]) > 0 - return reported_award & (capital <= ESA.capital.limit) + # Once one of the family's legacy benefits has closed, DWP's + # migration notice ends this award too: a Universal Credit claim + # brings the abolition of income-related ESA (Welfare Reform Act + # 2012 s. 33(1)) into force for the claimant (see SI 2025/1148 + # art. 3A(3)), and a family that does not claim loses the award at the + # notice's deadline (SI 2014/1230 reg 46(1)(a)). + closed = benunit("legacy_benefits_closed", period) + return reported_award & ~closed & (capital <= ESA.capital.limit) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py index 3811f0f227..a5ad1a3ffa 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py @@ -11,8 +11,10 @@ class housing_benefit_eligible(Variable): "Pension Credit " "can make a new claim; Universal Credit is not available to it. Any " "other family keeps Housing Benefit only while it continues an " - "existing award (reported Housing Benefit) and does not claim " - "Universal Credit. Claims for specified or temporary accommodation " + "existing award (reported Housing Benefit), does not claim " + "Universal Credit and has not been moved off a closed legacy benefit " + "(legacy_benefits_closed). Claims for specified or temporary " + "accommodation " "are not modelled." ) definition_period = YEAR @@ -20,6 +22,7 @@ class housing_benefit_eligible(Variable): "https://www.legislation.gov.uk/uksi/2014/1230/regulation/6A", "https://www.legislation.gov.uk/ukpga/2002/16/section/4", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/46", "https://www.legislation.gov.uk/uksi/2019/37/article/4", "https://www.legislation.gov.uk/ukpga/2012/5/section/4", "https://www.legislation.gov.uk/nisr/2016/226/regulation/4A", @@ -38,7 +41,14 @@ def formula(benunit, period, parameters): # the claimant and partner (is_claimant_or_partner), so a pensioner # with an 18 or 19 year old dependant can claim Housing Benefit and # not Universal Credit. - pension_age = benunit("meets_pension_credit_age_conditions", period) + # A family DWP moved to Universal Credit leaves this route if it + # claims Universal Credit and is eligible for it + # (left_pension_route_at_legacy_closure). One that does not claim + # keeps it; a protected mixed-age couple can reclaim Housing Benefit + # and keep its saving (SI 2019/37 art. 4(3)(b)(ii)). + pension_age = benunit("meets_pension_credit_age_conditions", period) & ~benunit( + "left_pension_route_at_legacy_closure", period + ) # Working-age families, and mixed-age couples without the saving # (since 15 May 2019), claim Universal Credit instead. They keep an # existing award until they claim it (reg 8(2A)), so this route also @@ -46,8 +56,12 @@ def formula(benunit, period, parameters): # specified or temporary accommodation ended on 1 July 2026 in Great # Britain (SI 2025/1148 art. 7); that is not modelled. already_claiming = add(benunit, period, ["housing_benefit_reported"]) > 0 - claiming_uc = benunit("would_claim_uc", period) - continuing_award = already_claiming & ~claiming_uc + claiming_uc = benunit("claims_universal_credit", period) + # Once another of the family's legacy benefits has closed, DWP's + # migration notice ends this award even if the family does not claim + # Universal Credit (reg 46(1)(a), (2)). + migrated = benunit("legacy_benefits_closed", period) + continuing_award = already_claiming & ~claiming_uc & ~migrated social = benunit.any(person("in_social_housing", period)) lha_eligible = benunit("LHA_eligible", period) pension_age_regulations = benunit( 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..42bc4575e4 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 @@ -15,7 +15,7 @@ class housing_benefit_on_passporting_benefit(Variable): "in payment or not (SI 2006/213 and SR 2006/405 reg 2(3B)). The model " "reads that as Universal Credit above nil before the benefit cap and " "deductions (is_uc_entitled), for a family that claims it " - "(would_claim_uc), so a Universal Credit award that the cap or " + "(claims_universal_credit), so a Universal Credit award that the cap or " "deductions reduce to nil still counts. The model tests the year " "rather than each day, and both members of a joint claim are on " "Universal Credit. There is no age condition: the working-age " diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py index d9bc07fec4..e2882b6e52 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_pension_age_regulations_apply.py @@ -19,11 +19,11 @@ class housing_benefit_pension_age_regulations_apply(Variable): "age. That is right for the cap, which SI 2014/1230 reg 60C disapplies " "for a claim where every claimant has reached that age, but not for " "reg 5 itself, under which such an award would bring in the " - "working-age regulations. The model has no lawful such awards (tax " - "credit migrants under SI 2014/1230 reg 60A are not modelled), and its " - "Universal Credit eligibility test also needs a claimant or partner " - "under the qualifying age, so the pension-age regulations are the right " - "result." + "working-age regulations. The model's only such awards go to tax " + "credit migrants under SI 2014/1230 reg 60A (claims_uc_at_legacy_" + "closure); they leave the pension-age route, so they get no Housing " + "Benefit under either set of regulations, and the cap does not apply " + "to them (reg 60C)." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/dwp/income_support.py b/policyengine_uk/variables/gov/dwp/income_support.py index 73b673bec1..1f9cd8ec94 100644 --- a/policyengine_uk/variables/gov/dwp/income_support.py +++ b/policyengine_uk/variables/gov/dwp/income_support.py @@ -12,4 +12,8 @@ class income_support(Variable): def formula(benunit, period, parameters): if not parameters(period).gov.dwp.income_support.active: return benunit.empty_array() - return benunit("income_support_entitlement", period) + # Once one of the family's legacy benefits has closed, DWP's + # migration notice ends this award too: on a Universal Credit claim + # (SI 2014/1230 reg 8(2A)) or at the notice's deadline (reg 46(1)(a)). + closed = benunit("legacy_benefits_closed", period) + return where(closed, 0, benunit("income_support_entitlement", period)) diff --git a/policyengine_uk/variables/gov/dwp/is_CTC_eligible.py b/policyengine_uk/variables/gov/dwp/is_CTC_eligible.py index e04b69a5fb..2bcaa824eb 100644 --- a/policyengine_uk/variables/gov/dwp/is_CTC_eligible.py +++ b/policyengine_uk/variables/gov/dwp/is_CTC_eligible.py @@ -9,8 +9,11 @@ class is_CTC_eligible(Variable): reference = "Tax Credits Act 2002 s. 8" def formula(benunit, period, parameters): - already_claiming = (add(benunit, period, ["child_tax_credit_reported"]) > 0) & ( - ~add(benunit, period, ["would_claim_uc"]) > 0 + already_claiming = ( + add(benunit, period, ["child_tax_credit_reported"]) > 0 + ) & ~( + benunit("claims_universal_credit", period) + | benunit("legacy_benefits_closed", period) ) return ( benunit.any( diff --git a/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py b/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py index 9152c5078f..84b2d6c770 100644 --- a/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py +++ b/policyengine_uk/variables/gov/dwp/is_WTC_eligible.py @@ -41,7 +41,11 @@ def formula(benunit, period, parameters): meets_medium_person_hours = max_person_hours >= WTC.min_hours.lower meets_medium = meets_medium_total_hours & meets_medium_person_hours meets_higher = total_hours >= WTC.min_hours.default - already_claiming = add(benunit, period, ["working_tax_credit_reported"]) > 0 + # A family DWP moved off its legacy benefits keeps no tax credits, + # even under a reform that restores them (SI 2014/1230 regs 8, 46). + already_claiming = ( + add(benunit, period, ["working_tax_credit_reported"]) > 0 + ) & ~benunit("legacy_benefits_closed", period) return ( (lower_req & meets_lower) | (medium_req & meets_medium) diff --git a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py index 30f366cd52..2a2e383e17 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py @@ -15,4 +15,11 @@ def formula(benunit, period, parameters): JSA = parameters(period).gov.dwp.JSA.income capital = benunit("jsa_income_assessable_capital", period) reported_award = add(benunit, period, ["jsa_income_reported"]) > 0 - return reported_award & (capital <= JSA.capital.limit) + # Once one of the family's legacy benefits has closed, DWP's + # migration notice ends this award too: a Universal Credit claim + # brings the abolition of income-based JSA (Welfare Reform Act + # 2012 s. 33(1)) into force for the claimant (see SI 2025/1148 + # art. 3A(3)), and a family that does not claim loses the award at the + # notice's deadline (SI 2014/1230 reg 46(1)(a)). + closed = benunit("legacy_benefits_closed", period) + return reported_award & ~closed & (capital <= JSA.capital.limit) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py index d970390993..e89ca49424 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/is_pension_credit_eligible.py @@ -16,6 +16,9 @@ class is_pension_credit_eligible(Variable): def formula(benunit, period, parameters): is_gc_eligible = benunit("is_guarantee_credit_eligible", period) is_sc_eligible = benunit("is_savings_credit_eligible", period) - return benunit("meets_pension_credit_age_conditions", period) & ( - is_gc_eligible | is_sc_eligible + # A family that left the pension-age route when DWP moved it to + # Universal Credit cannot claim Pension Credit as well. + route = benunit("meets_pension_credit_age_conditions", period) & ~benunit( + "left_pension_route_at_legacy_closure", period ) + return route & (is_gc_eligible | is_sc_eligible) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/left_pension_route_at_legacy_closure.py b/policyengine_uk/variables/gov/dwp/pension_credit/left_pension_route_at_legacy_closure.py new file mode 100644 index 0000000000..58f61aa17a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/left_pension_route_at_legacy_closure.py @@ -0,0 +1,40 @@ +from policyengine_uk.model_api import * + + +class left_pension_route_at_legacy_closure(Variable): + value_type = bool + entity = BenUnit + label = "Left the pension-age route when its legacy benefits closed" + documentation = ( + "Whether a family meeting the Pension Credit age conditions left that " + "route when DWP moved it to Universal Credit (legacy_benefits_closed): " + "it claimed Universal Credit at the closure and is eligible for it. A " + "family that does not claim keeps the route. A wholly pension-age one " + "can claim Pension Credit and Housing Benefit afresh, and a protected " + "mixed-age couple can reclaim Housing Benefit within three months, " + "keeping its saving. A claimant refused Universal Credit, say on " + "capital, is taken to stay on the route: the transitional capital " + "disregard that would admit a tax credit migrant is not modelled." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/46", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/51", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60A", + "https://www.legislation.gov.uk/uksi/2019/37/article/4", + "https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2024/a92024-the-social-security-state-pension-age-claimants-closure-of-tax-credits-amendment-regulations-2024", + ) + + def formula(benunit, period, parameters): + moved = benunit("legacy_benefits_closed", period) & benunit( + "meets_pension_credit_age_conditions", period + ) + # SI 2019/37 art. 4(3)(b)(ii), inserted by SI 2024/604, lets a + # protected couple that did not claim Universal Credit reclaim Housing + # Benefit within three months of its award ending and keep the saving + # (DWP HB circular A9/2024 paras 12-13), so only a claim moves it. + return ( + moved + & benunit("claims_uc_at_legacy_closure", period) + & benunit("is_uc_eligible", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py b/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py index ec299bceea..ec577f5437 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/meets_pension_credit_age_conditions.py @@ -11,7 +11,9 @@ class meets_pension_credit_age_conditions(Variable): "2019), so has any partner, unless the couple keeps Pension Credit " "under the SI 2019/37 article 4 saving. A family meeting these " "conditions is on the pension-age route (Pension Credit and pension-age " - "Housing Benefit) and is not eligible for Universal Credit." + "Housing Benefit) and is not eligible for Universal Credit unless " + "it claims it when DWP moves it off its legacy benefits " + "(claims_uc_at_legacy_closure)." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/claims_uc_at_legacy_closure.py b/policyengine_uk/variables/gov/dwp/universal_credit/claims_uc_at_legacy_closure.py new file mode 100644 index 0000000000..2ee3f940cb --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/claims_uc_at_legacy_closure.py @@ -0,0 +1,19 @@ +from policyengine_uk.model_api import * + + +class claims_uc_at_legacy_closure(Variable): + value_type = bool + entity = BenUnit + label = "Claims Universal Credit when its legacy benefits close" + documentation = ( + "Whether this family claims Universal Credit because DWP moved it off " + "its legacy benefits (legacy_benefits_closed) and it would claim then " + "(would_claim_uc_at_legacy_closure)." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2014/1230/regulation/44" + + def formula(benunit, period, parameters): + return benunit("legacy_benefits_closed", period) & benunit( + "would_claim_uc_at_legacy_closure", period + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py new file mode 100644 index 0000000000..21a1bdc84f --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class claims_universal_credit(Variable): + value_type = bool + entity = BenUnit + label = "Claims Universal Credit" + documentation = ( + "Whether this family claims Universal Credit: it would claim it " + "anyway (would_claim_uc), or it claims when DWP moves it off its " + "legacy benefits (claims_uc_at_legacy_closure). Universal Credit is " + "paid only to claiming families. A claim ends the family's Housing " + "Benefit continuing award and Child Tax Credit. Once DWP moves a " + "family off legacy benefits (legacy_benefits_closed) all its legacy " + "awards end; for other claimants the overlap of Universal Credit " + "with Working Tax Credit, Income Support, income-related ESA and " + "income-based JSA is not modelled, nor is Housing Benefit for " + "specified or temporary accommodation." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/46", + ) + + def formula(benunit, period, parameters): + return benunit("would_claim_uc", period) | benunit( + "claims_uc_at_legacy_closure", period + ) 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..9ae0e218f1 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 @@ -39,6 +39,6 @@ def formula(benunit, period, parameters): - benunit("benefit_cap_reduction", period), 0, ) - on_uc = benunit("would_claim_uc", period) & (award > 0) + on_uc = benunit("claims_universal_credit", period) & (award > 0) draw = benunit("uc_deduction_random_draw", period) return on_uc & (draw < adjusted_incidence) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py index f217751f58..c1b4d49993 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dependants_counted.py @@ -28,7 +28,7 @@ def formula(benunit, period, parameters): claims = ( liable_family & benunit("is_uc_eligible", period) - & benunit("would_claim_uc", period) + & benunit("claims_universal_credit", period) ) head_family = benunit("benunit_contains_household_head", period) # Order the claims: the household head's family first, then the diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py index 74f807d8b8..0e79b12c28 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py @@ -14,7 +14,9 @@ class is_uc_eligible(Variable): "a couple claims as a single person (regulation 3(3)), that member " "must meet the conditions, and the capital tested includes the other " "member's (regulation 18(2)). A family on the Pension Credit route " - "(meets_pension_credit_age_conditions) is not eligible." + "(meets_pension_credit_age_conditions) is not eligible unless it " + "claims when DWP moves it off its legacy benefits " + "(claims_uc_at_legacy_closure)." ) definition_period = YEAR reference = ( @@ -22,6 +24,7 @@ class is_uc_eligible(Variable): "https://www.legislation.gov.uk/ukpga/2012/5/section/4", "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/5", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60A", ) def formula(benunit, period, parameters): @@ -46,4 +49,17 @@ def formula(benunit, period, parameters): # couples not reported on Universal Credit to Pension Credit before # 15 May 2019, are modelling choices. pension_credit_route = benunit("meets_pension_credit_age_conditions", period) - return has_qualifying_claimant & ~pension_credit_route & (capital <= limit) + # A family DWP moved off its legacy benefits claims Universal Credit + # even from the pension-age route: a protected mixed-age couple + # claims jointly, giving up the saving (reg 3(2)(a)), and a + # pension-age Working Tax Credit recipient not entitled to Pension + # Credit has the upper age condition waived (SI 2014/1230 reg 60A). + closure_claim = benunit("claims_uc_at_legacy_closure", period) + on_working_tax_credit = ( + add(benunit, period, ["working_tax_credit_reported"]) > 0 + ) + reg_60a = on_working_tax_credit & benunit.any(claimant & meets_minimum_age) + route = (has_qualifying_claimant & ~pension_credit_route) | ( + closure_claim & (has_qualifying_claimant | reg_60a) + ) + return route & (capital <= limit) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/legacy_benefits_closed.py b/policyengine_uk/variables/gov/dwp/universal_credit/legacy_benefits_closed.py new file mode 100644 index 0000000000..fec4ccaa0d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/legacy_benefits_closed.py @@ -0,0 +1,71 @@ +from policyengine_uk.model_api import * + + +class legacy_benefits_closed(Variable): + value_type = bool + entity = BenUnit + label = "Moved off legacy benefits onto the Universal Credit route" + documentation = ( + "Whether DWP has moved this family off its legacy means-tested " + "benefits towards Universal Credit because one it reports has closed: " + "tax credits from 6 April 2025, and Income Support and income-based " + "Jobseeker's Allowance from 1 April 2026 (their active parameters). " + "The migration notice covers all the family's legacy awards, so they " + "all end: on a Universal Credit claim, or at the notice's deadline if " + "the family does not claim. Families DWP sent to Pension Credit " + "instead are excluded: those on Pension Credit, and those on Child Tax " + "Credit but not Working Tax Credit that meet the Pension Credit age " + "conditions. Pension-age and protected mixed-age families on Working " + "Tax Credit are included; Income Support and income-based JSA " + "closures do not move a wholly pension-age family. Housing Benefit and income-related " + "Employment and Support Allowance join this list when the model ends " + "their working-age awards." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/44", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/46", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60A", + "https://www.legislation.gov.uk/uksi/2019/167/article/3A", + "https://www.legislation.gov.uk/ukpga/2012/5/section/33", + "https://www.legislation.gov.uk/uksi/2025/1148/article/3A", + ) + + def formula(benunit, period, parameters): + dwp = parameters(period).gov.dwp + + def reports(*variables): + return add(benunit, period, list(variables)) > 0 + + on_pension_credit = reports("pension_credit_reported") + pension_age_route = benunit("meets_pension_credit_age_conditions", period) + # A tax credit closure notice sent to Pension Credit, not Universal + # Credit, a family on Pension Credit, or one on Child Tax Credit but + # not Working Tax Credit that meets the Pension Credit age conditions + # (single, both pension age, or a protected mixed-age couple; SI + # 2019/167 art. 3A(1)). Every other tax credit family, including a + # pension-age one on Working Tax Credit (SI 2014/1230 reg 60A), got a + # Universal Credit migration notice (reg 44). + pension_credit_route = on_pension_credit | ( + pension_age_route & ~reports("working_tax_credit_reported") + ) + tax_credits_closed = ( + reports("child_tax_credit_reported", "working_tax_credit_reported") + & (not dwp.tax_credits.active) + & ~pension_credit_route + ) + # Income Support and income-based JSA are working-age benefits; the + # rules ending legacy awards on a Universal Credit claim do not apply + # to a single claimant, or joint claimants both, over the qualifying + # age for State Pension Credit (reg 8(2B)). + wholly_pension_age = pension_age_route & ~benunit("is_mixed_age_couple", period) + income_support_closed = reports("income_support_reported") & ( + not dwp.income_support.active + ) + jsa_income_closed = reports("jsa_income_reported") & (not dwp.JSA.income.active) + return tax_credits_closed | ( + (income_support_closed | jsa_income_closed) + & ~on_pension_credit + & ~wholly_pension_age + ) 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..a77e418ca6 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit.py @@ -7,7 +7,7 @@ class universal_credit(Variable): definition_period = YEAR value_type = float unit = GBP - defined_for = "would_claim_uc" + defined_for = "claims_universal_credit" def formula(benunit, period, parameters): uc_max_entitlement = benunit("universal_credit_pre_benefit_cap", period) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit_pre_benefit_cap.py b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit_pre_benefit_cap.py index 7f6392708e..b91aafd76a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit_pre_benefit_cap.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/universal_credit_pre_benefit_cap.py @@ -9,7 +9,7 @@ class universal_credit_pre_benefit_cap(Variable): definition_period = YEAR unit = GBP category = BENEFIT - defined_for = "would_claim_uc" + defined_for = "claims_universal_credit" adds = ["uc_maximum_amount"] subtracts = ["uc_income_reduction"] diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/would_claim_uc_at_legacy_closure.py b/policyengine_uk/variables/gov/dwp/universal_credit/would_claim_uc_at_legacy_closure.py new file mode 100644 index 0000000000..05e75d08f5 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/would_claim_uc_at_legacy_closure.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class would_claim_uc_at_legacy_closure(Variable): + value_type = bool + entity = BenUnit + label = "Would claim Universal Credit when its legacy benefits close" + documentation = ( + "Whether this family would claim Universal Credit once a legacy " + "means-tested benefit it receives closes (legacy_benefits_closed). " + "Datasets draw it at DWP's Move to Universal Credit claim rates by " + "legacy benefit combination. It does not affect years before the " + "closure, when would_claim_uc alone decides the Universal Credit " + "claim. Defaults to True, like would_claim_uc." + ) + definition_period = YEAR + default_value = True diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py index 32d33f3bdc..3de57569f1 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py @@ -70,8 +70,11 @@ def formula(benunit, period, parameters): # both members reach the qualifying age (SI 2012/2885 reg 3(2); # WSI 2013/3029 reg 3(2); SSI 2021/249 reg 3(2)). England, and Wales # for schemes from 2026-27, also disregard a tax credit migrant's award - # (UC (TP) Regs 2014 reg 60A) and Scotland does not; the model does not - # model those migrants. + # (UC (TP) Regs 2014 reg 60A) and Scotland does not. The model's reg 60A + # migrants (claims_uc_at_legacy_closure) have no applicant or partner + # under the qualifying age, so their award is disregarded in every + # nation; counting it in Scotland, and in Wales before 2026-27, is not + # modelled. working_age_applicant = benunit.any(applicant_or_partner & ~over_qualifying_age) universal_credit_award = ( benunit("is_uc_entitled", period)