diff --git a/changelog.d/2119.fixed.md b/changelog.d/2119.fixed.md new file mode 100644 index 0000000000..f03367f965 --- /dev/null +++ b/changelog.d/2119.fixed.md @@ -0,0 +1 @@ +- Fixed Universal Credit closure claims with ineligible partners, preserved protected mixed-age couples' pension-age route when Universal Credit entitlement is nil, and applied the nation-specific Council Tax Reduction rules for pension-age tax credit migrants. 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..5c5ed7c847 --- /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 having positive Universal Credit entitlement before the benefit cap and deductions. 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..fddac7a189 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 only if it claims and has positive Universal Credit entitlement before the benefit cap and deductions. A protected mixed-age couple that does not claim, or claims but has nil entitlement, can reclaim Housing Benefit and keep its saving (SI 2019/37 art. 4(3)(b)(ii), as amended by SI 2024/604 art. 2). For Council Tax Reduction, Scotland treats pension-age tax credit migrants entitled to Universal Credit as working-age applicants. England disregards their reg 60A awards for schemes from April 2025, and Wales does so only for schemes from April 2026 (SI 2025/39; WSI 2026/13). 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); pension-age and protected mixed-age Child Tax Credit recipients whose Working Tax Credit entitlement tapered to nil, whom DWP also moved to Universal Credit (HB circular A9/2024 paras 6 and 10). Reported award amounts and current annual hours/income do not establish pre-migration Working Tax Credit entitlement. This gap requires a methodology decision or historical-entitlement input; no proxy is selected; 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", @@ -1384,4 +1384,4 @@ }, "nbformat": 4, "nbformat_minor": 1 -} \ No newline at end of file +} 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/finance/benefit/family/universal_credit/legacy_closure_ineligible_partner.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/legacy_closure_ineligible_partner.yaml new file mode 100644 index 0000000000..c56cb9137c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/legacy_closure_ineligible_partner.yaml @@ -0,0 +1,49 @@ +- name: A closure claimant with an ineligible partner uses the UC single claim rules + # UC Regulations 2013 reg 3(3), reg 82 and Schedule 4 paragraph 29. + # Both claims use the same single allowance and shared accommodation rent. + period: 2025 + absolute_error_margin: 0.02 + input: + people: + closure_claimant: + age: 30 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + closure_partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + ordinary_claimant: + age: 30 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + ordinary_partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + closure: + members: [closure_claimant, closure_partner] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + ordinary: + members: [ordinary_claimant, ordinary_partner] + would_claim_uc: true + would_claim_uc_at_legacy_closure: true + households: + closure_home: + members: [closure_claimant, closure_partner] + tenure_type: RENT_PRIVATELY + rent: 9_000 + brma: MAIDSTONE + ordinary_home: + members: [ordinary_claimant, ordinary_partner] + tenure_type: RENT_PRIVATELY + rent: 9_000 + brma: MAIDSTONE + output: + claims_universal_credit: [true, true] + uc_member_of_couple_claims_as_single_person: [true, true] + is_lha_shared_accommodation_rate_specified_renter: [true, true] + is_benefit_cap_single_claimant_rate: [true, true] + universal_credit: [10_139.64, 10_139.64] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/legacy_closure_pension_route.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/legacy_closure_pension_route.yaml new file mode 100644 index 0000000000..8fabfce305 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/legacy_closure_pension_route.yaml @@ -0,0 +1,155 @@ +# SI 2019/37 art. 4(3)(b)(ii), inserted by SI 2024/604 art. 2, +# preserves the Housing Benefit reclaim route for a protected mixed-age +# couple that claims Universal Credit before the final deadline but is not +# entitled. Entitlement is assessed before benefit-cap reductions. + +- name: Protected mixed-age couple claiming nil Universal Credit keeps its Housing Benefit reclaim route + # Preserve the review's 139.83 reproducer with its then-modelled 2025 + # weekly couple allowance. Main subsequently corrected that rate to 366; + # the unmodified current-policy case below has the same route outcome. + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.housing_benefit.allowances.couple.aged: 357.98 + people: + older: + age: 72 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + employment_income: 37_500 + 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] + rent: 9_000 + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: true + is_uc_eligible: true + universal_credit_pre_benefit_cap: 0 + is_uc_entitled: false + universal_credit: 0 + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + housing_benefit: 139.83 + +- name: Protected mixed-age couple declining Universal Credit keeps the same Housing Benefit reclaim route + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.housing_benefit.allowances.couple.aged: 357.98 + people: + older: + age: 72 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + employment_income: 37_500 + 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] + rent: 9_000 + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: false + is_uc_entitled: false + universal_credit: 0 + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + housing_benefit: 139.83 + +- name: Current pension-age allowance preserves Housing Benefit for a nil Universal Credit claimant + # Current 2025-26 pension-age couple allowance: 366 a week. The family + # otherwise matches the review's nil-entitlement reproducer above. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + older: + age: 72 + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + housing_benefit_reported: 4_000 + employment_income: 37_500 + 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] + rent: 9_000 + tenure_type: RENT_FROM_COUNCIL + output: + legacy_benefits_closed: true + claims_universal_credit: true + is_uc_eligible: true + universal_credit_pre_benefit_cap: 0 + is_uc_entitled: false + universal_credit: 0 + left_pension_route_at_legacy_closure: false + housing_benefit_eligible: true + housing_benefit: 410.90 + +- name: Benefit cap reducing Universal Credit to nil does not preserve the pension-age route + # Isolate the distinction between a nil means-tested entitlement and a + # positive entitlement whose payment is then reduced to zero by the cap. + 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 + uc_maximum_amount: 1_200 + uc_income_reduction: 0 + benefit_cap_reduction: 1_200 + households: + household: + members: [older, younger] + rent: 9_000 + tenure_type: RENT_FROM_COUNCIL + output: + claims_universal_credit: true + universal_credit_pre_benefit_cap: 1_200 + is_uc_entitled: true + universal_credit: 0 + left_pension_route_at_legacy_closure: true + housing_benefit_eligible: false + housing_benefit: 0 + is_pension_credit_eligible: false 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/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml new file mode 100644 index 0000000000..d2d2842137 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/legacy_closure_migrant_nations.yaml @@ -0,0 +1,257 @@ +# UC (Transitional Provisions) Regulations 2014 reg 60A waives the upper +# age limit for a single or wholly pension-age couple migrated from WTC. +# England disregards these awards for pensioner CTR from the 2025-26 scheme +# year (SI 2025/39 regs 1(3), 4); Wales from 2026-27 (WSI 2026/13 regs +# 1(3), 4). Scotland has no migrant disregard (SSI 2021/249 reg 3). + +- name: A Scottish pension-age WTC migrant uses working-age CTR in 2025 + period: 2025 + input: + people: + claimant: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + benunits: + family: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [claimant] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: false + council_tax_reduction_pensioner_earnings_disregard: 0 + +- name: A Scottish pension-age WTC migrant still uses working-age CTR in 2026 + period: 2026 + input: + people: + claimant: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + benunits: + family: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [claimant] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: false + council_tax_reduction_pensioner_earnings_disregard: 0 + +- name: A Welsh pension-age WTC migrant uses working-age CTR in 2025 + period: 2025 + input: + people: + claimant: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + benunits: + family: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [claimant] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: false + council_tax_reduction_pensioner_earnings_disregard: 0 + +- name: A Welsh pension-age WTC migrant uses pensioner CTR from the 2026 scheme + period: 2026 + input: + people: + claimant: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + benunits: + family: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [claimant] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: true + +- name: An English pension-age WTC migrant uses pensioner CTR from the 2025 scheme + period: 2025 + input: + people: + claimant: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + benunits: + family: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [claimant] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: true + +- name: An English mixed-age WTC migrant has no reg 60A pensioner CTR exemption + period: 2025 + input: + people: + older: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + younger: + age: 40 + benunits: + family: + members: [older, younger] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + has_mixed_age_couple_pension_credit_saving: true + households: + household: + members: [older, younger] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: false + +- name: A Welsh mixed-age WTC migrant has no reg 60A exemption in the 2026 scheme + period: 2026 + input: + people: + older: + age: 70 + employment_income: 8_000 + weekly_hours: 30 + working_tax_credit_reported: 2_000 + younger: + age: 40 + benunits: + family: + members: [older, younger] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + has_mixed_age_couple_pension_credit_saving: true + households: + household: + members: [older, younger] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_uc_eligible: true + is_uc_entitled: true + council_tax_reduction_pensioner: false + +- name: A Scottish pensioner with a non-migrant UC run-on award keeps pensioner CTR + # An input award without a working-age claimant or reg 60A migration + # retains the existing approximation for an award awaiting termination, + # or awarded in error: SSI 2021/249 reg 3(2). + period: 2025 + input: + people: + claimant: + age: 70 + benunits: + family: + members: [claimant] + universal_credit_pre_benefit_cap: 4_000 + households: + household: + members: [claimant] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + claims_uc_at_legacy_closure: false + is_uc_entitled: true + council_tax_reduction_pensioner: true + +- name: A Welsh pensioner with a non-migrant UC input keeps the existing CTR classification + period: 2025 + input: + people: + claimant: + age: 70 + benunits: + family: + members: [claimant] + universal_credit_pre_benefit_cap: 4_000 + households: + household: + members: [claimant] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + claims_uc_at_legacy_closure: false + is_uc_entitled: true + council_tax_reduction_pensioner: true 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..2e95eed8c1 --- /dev/null +++ b/policyengine_uk/tests/test_uc_legacy_closure_properties.py @@ -0,0 +1,431 @@ +"""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 having nil Universal Credit entitlement). A family that leaves + that route gets no Pension Credit either, and any closure claimant + with a qualifying claimant and 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 or age waiver: 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 and has a qualifying working-age claimant. 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, including when a + partner cannot be a joint claimant. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +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", + "universal_credit_pre_benefit_cap", + "uc_assessable_capital", + "uc_housing_costs_element", + "is_lha_shared_accommodation_rate_specified_renter", + "is_benefit_cap_single_claimant_rate", +] +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()), + ineligible_partner=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"), + "uc_is_ineligible_partner": every_year( + role == "partner" and unit.get("ineligible_partner", False) + ), + "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 + ) + + +def has_working_age_claimant(unit): + return any( + role != "child" + and age <= 60 + and not (role == "partner" and unit.get("ineligible_partner", False)) + for role, age in unit["members"] + ) + + +@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]) + # A working-age claimant, or the reg 60A upper-age waiver for + # WTC migrants, is still needed. A pensioner with an ineligible + # working-age partner cannot claim on an IS/JSA closure alone. + age_qualified = ( + has_working_age_claimant(unit) + or unit["reported"]["working_tax_credit_reported"] > 0 + ) + if claims_at_closure: + expected_eligible = ( + age_qualified and v["uc_assessable_capital"][i] <= 16_000 + ) + assert uc_eligible == expected_eligible, (year, unit) + left = ( + pension_age_route + and claims_at_closure + and v["universal_credit_pre_benefit_cap"][i] > 0 + ) + 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)) +@example( + [ + dict( + shape="mixed_age", + members=[("claimant", 70), ("partner", 40)], + reported={ + name: 2_000.0 if name == "working_tax_credit_reported" else 0.0 + for name in LEGACY + }, + saving=False, + tenure="RENT_FROM_COUNCIL", + rent=9_000.0, + earnings=0.0, + savings=0.0, + would_claim_uc=True, + claims_at_closure=True, + ineligible_partner=True, + ) + ] +) +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. + # A sole pension-age claimant also needs the reg 60A age waiver, + # even without a protected Pension Credit route. + age_waiver_decides = unit["reported"][ + "working_tax_credit_reported" + ] > 0 and not has_working_age_claimant(unit) + closed = bool(drawn[year]["legacy_benefits_closed"][i]) + decides = closed and ( + not unit["would_claim_uc"] + or bool(drawn[year]["meets_pension_credit_age_conditions"][i]) + or age_waiver_decides + ) + if closed and age_waiver_decides: + for values, closure_flag in ( + (drawn, unit["claims_at_closure"]), + (negated, not unit["claims_at_closure"]), + ): + expected_eligible = ( + closure_flag + and values[year]["uc_assessable_capital"][i] <= 16_000 + ) + assert ( + bool(values[year]["is_uc_eligible"][i]) == expected_eligible + ), ( + year, + unit, + ) + 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)) +@example( + [ + dict( + shape="couple", + members=[("claimant", 30), ("partner", 30)], + reported={ + name: 2_000.0 if name == "working_tax_credit_reported" else 0.0 + for name in LEGACY + }, + saving=False, + tenure="RENT_PRIVATELY", + rent=9_000.0, + earnings=0.0, + savings=0.0, + would_claim_uc=False, + claims_at_closure=True, + ineligible_partner=True, + ) + ] +) +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 + [ + "uc_housing_costs_element", + "is_lha_shared_accommodation_rate_specified_renter", + "is_benefit_cap_single_claimant_rate", + ]: + assert ( + abs(float(v[benefit][i]) - float(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/tests/test_uc_non_dep_pension_credit.py b/policyengine_uk/tests/test_uc_non_dep_pension_credit.py new file mode 100644 index 0000000000..b47fec46ad --- /dev/null +++ b/policyengine_uk/tests/test_uc_non_dep_pension_credit.py @@ -0,0 +1,115 @@ +"""Pension Credit receipt for UC non-dependants stays acyclic and current.""" + +import numpy as np +import pytest + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEAR = 2025 + + +def mixed_families(): + people = {} + for name, age, head, wtc, pension in [ + ("tenant", 40, True, 2_000, 0), + ("pc_older", 72, False, 2_000, 12_000), + ("pc_younger", 40, False, 0, 0), + ("working_non_dep", 30, False, 0, 0), + ("renter_older", 72, True, 2_000, 0), + ("renter_younger", 40, False, 0, 0), + ]: + people[name] = { + "age": {YEAR: age}, + "is_household_head": {YEAR: head}, + "is_claimant_or_partner": {YEAR: True}, + "working_tax_credit_reported": {YEAR: wtc}, + "state_pension_reported": {YEAR: pension}, + "employment_income": {YEAR: 0}, + } + benunits = {} + for name, members, saving, claims in [ + ("tenant", ["tenant"], False, True), + ("pc_non_deps", ["pc_older", "pc_younger"], True, True), + ("working_non_dep", ["working_non_dep"], False, False), + ("protected_renters", ["renter_older", "renter_younger"], True, True), + ]: + benunits[name] = { + "members": members, + "has_mixed_age_couple_pension_credit_saving": {YEAR: saving}, + "would_claim_uc": {YEAR: False}, + "would_claim_uc_at_legacy_closure": {YEAR: claims}, + } + households = { + "with_non_deps": { + "members": ["tenant", "pc_older", "pc_younger", "working_non_dep"], + "rent": {YEAR: 9_000}, + "tenure_type": {YEAR: "RENT_FROM_COUNCIL"}, + }, + "protected_renters": { + "members": ["renter_older", "renter_younger"], + "rent": {YEAR: 9_000}, + "tenure_type": {YEAR: "RENT_FROM_COUNCIL"}, + }, + } + return {"people": people, "benunits": benunits, "households": households} + + +def test_non_dependants_pension_credit_exempts_only_its_own_family(): + sim = Simulation(situation=mixed_families()) + # Calculate UC first: the renters' UC must not ask for their own PC + # through the non-dependant exemption and recurse into UC entitlement. + uc = np.asarray(sim.calculate("universal_credit", YEAR)) + pc = np.asarray(sim.calculate("pension_credit", YEAR)) + np.testing.assert_array_equal( + sim.calculate("uc_non_dep_receives_pension_credit", YEAR), + [False, True, False, False], + ) + assert pc[1] > 0 + assert uc[1] == 0 + assert uc[0] > 0 and uc[3] > 0 + assert pc[0] == 0 and pc[3] == 0 + # Both members of the protected PC family are exempt. Only the separate + # working-age non-dependant contributes 12 x 93.02 in 2025-26. + np.testing.assert_allclose( + sim.calculate("uc_non_dep_deductions", YEAR), + [1_116.24, 0, 0, 0], + atol=0.01, + ) + + +def test_non_dependant_exemption_recalculates_after_earnings_change(): + sim = Simulation(situation=mixed_families()) + assert sim.calculate("uc_non_dep_receives_pension_credit", YEAR)[1] + sim.set_input("employment_income", YEAR, [0, 50_000, 0, 0, 0, 0]) + sim.drop_computed_arrays() + assert sim.calculate("pension_credit", YEAR)[1] == 0 + assert not sim.calculate("uc_non_dep_receives_pension_credit", YEAR)[1] + # The two former PC recipients now contribute alongside the other + # non-dependant. A stale counterfactual branch would keep them exempt. + np.testing.assert_allclose( + sim.calculate("uc_non_dep_deductions", YEAR), + [3_348.72, 0, 0, 0], + atol=0.01, + ) + + +def test_non_dependant_exemption_keeps_frozen_pension_credit_receipt(): + remove_couple_guarantee = { + "gov.dwp.pension_credit.guarantee_credit.minimum_guarantee.COUPLE": { + "2025-01-01": 0 + } + } + unfrozen = Simulation(situation=mixed_families(), reform=remove_couple_guarantee) + frozen = Simulation( + situation=mixed_families(), + reform={ + **remove_couple_guarantee, + "gov.contrib.freeze_pension_credit": {"2025-01-01": True}, + }, + ) + assert unfrozen.calculate("pension_credit", YEAR)[1] == 0 + assert not unfrozen.calculate("uc_non_dep_receives_pension_credit", YEAR)[1] + assert frozen.calculate("pension_credit", YEAR)[1] > 0 + assert frozen.calculate("uc_non_dep_receives_pension_credit", YEAR)[1] diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index 4df69d4cbc..5c58eaa0d2 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -38,13 +38,13 @@ 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 + does 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. + Benefit award before its legacy benefits close. A family claiming + Universal Credit, including at a legacy closure, uses the UC rules. """ return benunit("claims_legacy_benefits", period) & ~benunit( - "would_claim_uc", period + "claims_universal_credit", period ) 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..fab1207355 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,15 @@ 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 entitled before benefit-cap reductions + # and deductions (left_pension_route_at_legacy_closure). One that does + # not claim, or claims with nil entitlement, keeps it; a protected + # mixed-age couple can reclaim Housing Benefit and keep its saving + # (SI 2019/37 art. 4(3)(b)(ii), inserted by SI 2024/604 art. 2). + 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 +57,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..c30929212c 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,12 @@ 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). While entitled, they leave the pension-age route and get " + "no Housing Benefit under either set of regulations; a claim with " + "nil entitlement keeps that route. The cap does not apply to their " + "Universal Credit awards (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..8b24f90e10 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/left_pension_route_at_legacy_closure.py @@ -0,0 +1,45 @@ +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 entitled to it " + "before benefit-cap reductions and deductions. A family that does " + "not claim, or claims but has nil entitlement, 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.legislation.gov.uk/uksi/2024/604/article/2", + "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, or claimed + # but was not entitled, reclaim Housing Benefit within three months + # of its award ending and keep the saving (DWP HB circular A9/2024 + # paras 12-13). Entitlement is read before benefit-cap reductions + # and deductions, which can reduce an entitled family's payment to nil. + return ( + moved + & benunit("claims_uc_at_legacy_closure", period) + & benunit("is_uc_entitled", 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_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py index ea5db9b881..6c81d42cad 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py @@ -18,7 +18,7 @@ class uc_non_dep_deduction_exempt(Variable): def formula(person, period, parameters): return ( - (person.benunit("pension_credit", period) > 0) + person.benunit("uc_non_dep_receives_pension_credit", period) | person("dla_sc_middle_plus", period) | (person("pip_dl", period) > 0) | (person("attendance_allowance", period) > 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py new file mode 100644 index 0000000000..483b5c7cd1 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py @@ -0,0 +1,45 @@ +from policyengine_uk.model_api import * + + +class uc_non_dep_receives_pension_credit(Variable): + value_type = bool + entity = BenUnit + label = ( + "Non-rent-liable family receives Pension Credit for UC housing contributions" + ) + documentation = ( + "Whether a family not liable for rent receives Pension Credit, for the " + "Universal Credit non-dependant housing contribution exemption. Such " + "a family's own Universal Credit housing element is zero. Calculating " + "its Pension Credit in a branch with that element fixed at zero " + "therefore preserves its entitlement, including legacy-closure " + "routing, take-up, explicit inputs and frozen Pension Credit reforms. " + "It avoids evaluating a renter's own Pension Credit while calculating " + "their Universal Credit housing element. Rent-liable families' " + "branch results are disregarded because they are not non-dependants." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/16", + ) + + def formula(benunit, period, parameters): + # A non-dependant is not liable for the rent (Sch 4 para 9(2)(d)), + # so their own housing element is already zero. This branch keeps + # their actual Pension Credit calculation while breaking the cycle + # through the renter's UC entitlement and pension-age route. + simulation = benunit.simulation + name = "uc_non_dep_pension_credit" + while name in simulation.branches or name == simulation.branch_name: + name += "_" + branch = simulation.get_branch(name) + try: + branch.set_input( + "uc_housing_costs_element", period, np.zeros(benunit.count) + ) + pension_credit = branch.calculate("pension_credit", period) + finally: + # A later period or changed input must start from current inputs. + del simulation.branches[name] + return (pension_credit > 0) & ~benunit("benunit_is_rent_liable", period) 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..1f8b18f5a2 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): @@ -40,10 +43,25 @@ def formula(benunit, period, parameters): # No family gets both Pension Credit and Universal Credit (UC # (Transitional Provisions) Regs 2014 reg 5(1)(d)). A mixed-age couple # keeping the SI 2019/37 saving could still choose Universal Credit - # jointly (UC Regs 2013 reg 3(2)(a)) and lose the saving; the model + # jointly (UC Regs 2013 reg 3(2)(a)) and leave the saving while entitled; + # the model # keeps it on the Pension Credit route, anchored to its reported # Pension Credit or Housing Benefit. That, and routing mixed-age # 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 (reg 3(2)(a)), leaving the pension-age route only + # while entitled, 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..f070259c7d 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 @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.household.demographic.country import Country class council_tax_reduction_pensioner(Variable): @@ -12,7 +13,13 @@ class council_tax_reduction_pensioner(Variable): "on Income Support, income-based Jobseeker's Allowance or " "income-related Employment and Support Allowance, or has an award of " "Universal Credit. A mixed-age couple on Universal Credit is therefore " - "not a pensioner in England, Wales or Scotland. Each applicant's " + "not a pensioner in England, Wales or Scotland. Wholly pension-age " + "tax credit migrants whose upper UC age limit is waived under " + "regulation 60A retain pensioner treatment in England from the " + "2025-26 scheme year and Wales from 2026-27; Scotland treats their " + "UC awards under the working-age rules. Other wholly pension-age " + "UC input awards retain the model's approximation for an award " + "awaiting termination or made in error. Each applicant's " "scheme follows their own family, so where families share the rent " "and each claims, a working-age family is assessed under the " "working-age rules even if the household head's family is " @@ -23,8 +30,12 @@ class council_tax_reduction_pensioner(Variable): definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2012/2885/regulation/3", + "https://www.legislation.gov.uk/uksi/2025/39/regulation/1", + "https://www.legislation.gov.uk/uksi/2025/39/regulation/4", "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/3", "https://www.legislation.gov.uk/wsi/2013/3029/regulation/3", + "https://www.legislation.gov.uk/wsi/2026/13/regulation/1", + "https://www.legislation.gov.uk/wsi/2026/13/regulation/4", "https://www.legislation.gov.uk/ssi/2021/249/regulation/3", "https://www.legislation.gov.uk/ssi/2012/319/regulation/12", "https://www.legislation.gov.uk/ssi/2012/303/regulation/12", @@ -57,26 +68,42 @@ def formula(benunit, period, parameters): & ~head_applies_alone ) # The award before the benefit cap: the cap reduces an award - # (UC Regs 2013 reg 81) rather than removing it. An award counts only - # where the applicant or partner is under the qualifying age, which - # Universal Credit needs (UC Regs 2013 reg 3(2)(a)). A dependant is + # (UC Regs 2013 reg 81) rather than removing it. An ordinary award + # counts where the applicant or partner is under the qualifying age + # (UC Regs 2013 reg 3(2)(a)). A dependant is # neither: outside a head who applies alone, the applicant and partner # are the benefit unit's claimant and partner, and # is_claimant_or_partner presumes a member under 20 and at least 16 # years younger than the claimant to be their child, so a pensioner's # 18- or 19-year-old does not count, whether or not is_parent # identifies the pensioner as the parent. - # This also stands in for the rules that disregard an award held after - # both members reach the qualifying age (SI 2012/2885 reg 3(2); - # WSI 2013/3029 reg 3(2); SSI 2021/249 reg 3(2)). England, and Wales - # for schemes from 2026-27, also disregard a tax credit migrant's award - # (UC (TP) Regs 2014 reg 60A) and Scotland does not; the model does not - # model those migrants. working_age_applicant = benunit.any(applicant_or_partner & ~over_qualifying_age) + # The model recognises reg 60A awards through the closure claim and + # reported WTC. The waiver applies to a single pension-age claimant + # or joint claimants both pension age, not mixed-age couples. + pension_age_tax_credit_migrant = ( + benunit("claims_uc_at_legacy_closure", period) + & (add(benunit, period, ["working_tax_credit_reported"]) > 0) + & ~working_age_applicant + ) + country = benunit.household("country", period) + # Commencement alone does not determine the scheme: SI 2025/39 + # reg 1(3) applies from the financial year beginning April 2025; + # WSI 2026/13 reg 1(3) from April 2026. Model years represent those + # scheme years. SSI 2021/249 reg 3 has no reg 60A migrant exception. + migrant_award_disregarded = ( + (country == Country.ENGLAND) & (period.start.year >= 2025) + ) | ((country == Country.WALES) & (period.start.year >= 2026)) + # Other awards with no working-age applicant retain the existing + # approximation for a final UC assessment-period run-on or an award + # made in error (SI 2012/2885 reg 3(2); SSI 2021/249 reg 3(2)). universal_credit_award = ( benunit("is_uc_entitled", period) & ~head_applies_alone - & working_age_applicant + & ( + working_age_applicant + | (pension_age_tax_credit_migrant & ~migrant_award_disregarded) + ) ) return ( attained_qualifying_age & ~income_related_benefit & ~universal_credit_award