diff --git a/changelog.d/2804.fixed.md b/changelog.d/2804.fixed.md new file mode 100644 index 0000000000..8a73e94f80 --- /dev/null +++ b/changelog.d/2804.fixed.md @@ -0,0 +1 @@ +Excluded from the Universal Credit qualifying young person test a person who reports their own income-related ESA, income-based JSA, Income Support, Housing Benefit, Child Tax Credit or Working Tax Credit in a year the model pays that benefit, unless the model's legacy closure, or for a claimant or partner the family's Universal Credit claim, has ended the award on that report or a reform neutralises it, and a claimant or partner on the couple's tax credit award (SI 2014/1230 reg. 28). The Universal Credit education condition reads that test as it stands on the first day of a claimant's Universal Credit award, while a Housing Benefit award (in Northern Ireland, a rent rebate or allowance) or an income-related ESA award the claim ends still runs, and reads every reported existing benefit for the migration day's reg. 60(1) protection, and the formula for Scotland's working-age Council Tax Reduction reads the Universal Credit Regulations 2013 reg. 5 test without reg. 28. The supplied input `housing_benefit_report_is_rate_rebate_only` marks a Northern Ireland report that covers only a rate rebate, which the claim ends the day before the award (SR 2016/226 reg. 6(2)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_esa_income_abolition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_esa_income_abolition.yaml new file mode 100644 index 0000000000..8ede79769f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_esa_income_abolition.yaml @@ -0,0 +1,175 @@ +# The Secretary of State appointed 1 July 2026 for the abolition of the +# remaining old style income-related ESA awards in Great Britain (SI +# 2025/1148 art. 3A(1)), apart from awards saved for an appointee (art. +# 3A(2)). The model pays each claim for the share of the fiscal year before +# that date (esa_income_payable_share, from gov.dwp.ESA.income.awards_payable): +# 1 up to 2025-26, 86/365 = 0.2356 in 2026-27 and 0 from 2027-28, or 1 for a +# saved claim. Nobody is entitled to a claim that pays nothing, so a young +# person's own income-related ESA report excludes them from the UC qualifying +# young person test (SI 2014/1230 reg. 28) only while the share exceeds 0. +# +# The first four cases use one household: a parent aged 45 and a 17-year-old on a +# non-advanced course in the parent's benefit unit, North East, owned +# outright, no income. The parent claims Universal Credit as a single +# claimant over 25 (standard_allowance SINGLE_OLD: £424.90 a month in +# 2026-27, £443.2099 projected for 2027-28) with the first child higher +# amount for a qualifying young person (£364.1952 projected for 2027-28): +# 2027 with the child element: 12 x (443.2099 + 364.1952) = 9,688.86 +# 2027 without the child element: 12 x 443.2099 = 5,318.52 +# 2026 without the child element: 12 x 424.90 = 5,098.80 + +- name: A young person's own income-related ESA report after the claim stops paying leaves the child element. + # The claim pays nothing in 2027-28, so nobody is entitled to it. + period: 2027 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + esa_income_payable_share: [0, 0] + is_qualifying_young_person_for_universal_credit: [false, true] + universal_credit: 9_688.86 + +- name: A young person's own income-related ESA report saved for an appointee removes the child element. + # An award saved for an appointee stays payable (SI 2025/1148 art. 3A(2)). + period: 2027 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + esa_income_appointee_saving: true + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + esa_income_payable_share: [0, 1] + is_qualifying_young_person_for_universal_credit: [false, false] + universal_credit: 5_318.52 + +- name: A young person's own income-related ESA report in the year the claim stops paying removes the child element. + # The claim pays for 86 of the 365 days of 2026-27 in Great Britain. + period: 2026 + absolute_error_margin: 0.0001 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + esa_income_payable_share: [0.2356, 0.2356] + is_qualifying_young_person_for_universal_credit: [false, false] + universal_credit: 5_098.80 + +- name: A young person's own income-related ESA report after the claim stops paying leaves the child element while their legacy awards stay open. + # The young person also reports Pension Credit, so the model's migration + # proxy leaves their legacy awards open (person_legacy_benefits_closed is + # false), but the claim still pays nothing in 2027-28 + # (esa_income_payable_share 0) and nobody is entitled to it. + period: 2027 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + pension_credit_reported: 1_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + esa_income_payable_share: [0, 0] + person_legacy_benefits_closed: [false, false] + is_qualifying_young_person_for_universal_credit: [false, true] + universal_credit: 9_688.86 + +- name: An 18-year-old claimant's own income-related ESA report after the claim stops paying leaves them receiving education while their legacy awards stay open. + # A single 18-year-old on a full-time non-advanced course with no + # maintenance funding reports their own income-related ESA and Pension + # Credit, and claims Universal Credit. The Pension Credit report leaves + # their legacy awards open (person_legacy_benefits_closed false), but the + # ESA claim pays nothing in 2027-28 (esa_income_payable_share 0), so on the + # first day of their Universal Credit award they aren't entitled to it + # either. They count as a qualifying young person for the education + # condition and receive education (UC Regs 2013 reg. 12(1A)), with no + # exception, so they get no Universal Credit. + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + is_claimant_or_partner: true + esa_income_reported: 3_000 + pension_credit_reported: 1_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + region: NORTH_EAST + output: + esa_income_payable_share: [0] + person_legacy_benefits_closed: [false] + is_qualifying_young_person_for_uc_education: [true] + uc_receiving_education: [true] + is_uc_eligible: false + universal_credit: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_existing_benefits.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_existing_benefits.yaml new file mode 100644 index 0000000000..8966b13925 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_existing_benefits.yaml @@ -0,0 +1,448 @@ +# Under the Universal Credit (Transitional Provisions) Regulations 2014 +# reg. 28, DWP doesn't count a person entitled to an existing benefit (reg. +# 2(1)) as a UC qualifying young person: income-based JSA, income-related +# ESA, Income Support, Housing Benefit, Child Tax Credit and Working Tax +# Credit. The predicate reads the person's own reports and, for a claimant or +# partner, the couple's joint tax credit award (Tax Credits Act 2002 s.3(3)). +# It reads a report only in a year the model pays that benefit, only until +# the model's legacy closure, or for a claimant or partner the family's +# Universal Credit claim, ends the award on it, and only if no reform +# neutralises it. The model pays Income Support and income-based +# JSA until 31 March 2026 and tax credits until 5 April 2025, has no +# closure date for Housing Benefit, and pays an income-related ESA claim +# only while it stays payable (esa_income_payable_share). It also ends a +# person's legacy awards when its annual migration proxy closes them, and +# the predicate follows that closure +# (uc_qualifying_young_person_legacy_closure.yaml covers it). In these cases +# a young person's own report closes their legacy awards only where that +# benefit's year gate has already closed, so the closure decides no case. +# The predicate follows the family's Universal Credit claim only for a +# claimant's or partner's own award +# (uc_qualifying_young_person_universal_credit_claim.yaml covers it), so +# the cases that test a claimant's own Child Tax Credit or Housing Benefit +# report claim no Universal Credit (would_claim_uc). The first six cases, +# and the four after the seventh, share one household: a parent aged 45 and +# a 17-year-old on a non-advanced course, North East, owned outright, no +# income. + +- name: A parent's young person who reports their own Income Support attracts no child element. + period: 2025 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + income_support_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 4_801.68 + +- name: A parent's young person who reports their own Housing Benefit attracts no child element. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 5_098.80 + +- name: A parent's young person who reports their own income-related ESA attracts no child element. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 5_098.80 + +- name: A parent's young person who reports their own income-based JSA attracts no child element. + period: 2025 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + jsa_income_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 4_801.68 + +- name: A parent's young person who reports no award attracts the child element. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_222.56 + universal_credit: 9_321.36 + +- name: A parent's young person flagged as receiving benefits in their own right attracts no child element. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + receives_benefits_in_own_right: true + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 5_098.80 + +- name: An 18-year-old claimant on a non-advanced course who reports Housing Benefit is not a qualifying young person. + # The claimant doesn't claim Universal Credit, so no claim ends their own + # Housing Benefit (uc_qualifying_young_person_universal_credit_claim.yaml + # covers a claim). They get no Universal Credit. + period: 2026 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false] + is_child_or_qualifying_young_person_for_universal_credit: [false] + uc_child_element: 0 + universal_credit: 0 + +- name: A young person's own Income Support report after Income Support stops paying leaves the child element. + # Income Support stops paying on 1 April 2026 (gov.dwp.income_support.active), so nobody is entitled to it in 2026. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + income_support_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_222.56 + universal_credit: 9_321.36 + +- name: A young person's own income-based JSA report after income-based JSA stops paying leaves the child element. + # Income-based JSA stops paying on 1 April 2026 (gov.dwp.JSA.income.active). + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + jsa_income_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_222.56 + universal_credit: 9_321.36 + +- name: A young person's own Child Tax Credit report while tax credits pay removes the child element. + period: 2024 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + child_tax_credit_reported: 1_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 4_721.40 + +- name: A young person's own Child Tax Credit report after tax credits stop paying leaves the child element. + # Tax credits stop paying on 6 April 2025 (gov.dwp.tax_credits.active). + period: 2025 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + child_tax_credit_reported: 1_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_068 + universal_credit: 8_869.68 + +- name: A young person's own Child Tax Credit or Working Tax Credit report excludes them. + # Each person forms their own benefit unit as its claimant, and none + # claims Universal Credit, so no claim ends their Child Tax Credit. + period: 2024 + input: + age: [17, 17, 17] + current_education: [UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY] + child_tax_credit_reported: [0.01, 0, 0] + working_tax_credit_reported: [0, 0.01, 0] + would_claim_uc: [false, false, false] + output: + is_qualifying_young_person_for_universal_credit: [false, false, true] + +- name: A parent's own reports of every existing benefit leave their young person a qualifying young person. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + income_support_reported: 3_000 + housing_benefit_reported: 3_000 + child_tax_credit_reported: 3_000 + working_tax_credit_reported: 3_000 + esa_income_reported: 3_000 + jsa_income_reported: 3_000 + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_universal_credit: [false, true] + +- name: A couple's Working Tax Credit award reported by one partner excludes both 18-year-old partners. + # Tax Credits Act 2002 s.3(3): the members of a couple claim jointly. + period: 2024 + input: + people: + claimant: + age: 18 + current_education: UPPER_SECONDARY + is_claimant_or_partner: true + working_tax_credit_reported: 2_000 + partner: + age: 18 + current_education: UPPER_SECONDARY + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + output: + is_qualifying_young_person_for_universal_credit: [false, false] + +- name: A couple's Child Tax Credit award does not exclude a dependant who reports none. + # The couple doesn't claim Universal Credit, so no claim ends its award. + period: 2024 + input: + people: + claimant: + age: 18 + current_education: UPPER_SECONDARY + is_claimant_or_partner: true + child_tax_credit_reported: 2_000 + partner: + age: 18 + current_education: UPPER_SECONDARY + is_claimant_or_partner: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, partner, young_person] + would_claim_uc: false + output: + is_qualifying_young_person_for_universal_credit: [false, false, true] + +- name: A partner's own Housing Benefit report leaves an 18-year-old claimant a qualifying young person. + # The partner reports their own Housing Benefit, and the formula reads + # only tax credits across the couple. + period: 2024 + input: + people: + claimant: + age: 18 + current_education: UPPER_SECONDARY + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [claimant, partner] + output: + is_qualifying_young_person_for_universal_credit: [true, false] + +- name: A dependant's own Child Tax Credit award leaves an 18-year-old claimant a qualifying young person. + # Tax Credits Act 2002 s.3(3)(b): the dependant claims as a single person, + # so their award is not the claimant's. + period: 2024 + input: + people: + claimant: + age: 18 + current_education: UPPER_SECONDARY + is_claimant_or_partner: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + is_claimant_or_partner: false + child_tax_credit_reported: 1_000 + benunits: + benunit: + members: [claimant, young_person] + output: + is_qualifying_young_person_for_universal_credit: [true, false] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_legacy_closure.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_legacy_closure.yaml new file mode 100644 index 0000000000..450b158b04 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_legacy_closure.yaml @@ -0,0 +1,432 @@ +# The model ends a person's legacy awards once its annual migration proxy +# closes them, and the UC qualifying young person test (SI 2014/1230 reg. 28) +# follows that closure: nobody is entitled to an award the model has ended. +# The model ends a person's own Income Support, income-based JSA and +# income-related ESA when person_legacy_benefits_closed holds +# (income_support, jsa_income, esa_income_report_closed), and the family's +# Housing Benefit and tax credits when legacy_benefits_closed holds +# (housing_benefit_eligible, is_CTC_eligible, is_WTC_eligible). A +# dependant's own Income Support or income-based JSA report from 2026, or own +# tax credit report from 2025, closes the dependant's legacy awards +# (gov.dwp.income_support.active, gov.dwp.JSA.income.active and +# gov.dwp.tax_credits.active turn false on 1 April 2026, 1 April 2026 and +# 6 April 2025). The predicate excludes nobody on those reports, as each +# benefit's year gate has closed. +# +# Cases 1 to 6 and 10 use one household: a parent aged 45 and a +# 17-year-old on a non-advanced course in the parent's benefit unit, North +# East, owned outright, no income. Cases 7, 8 and 11 use the same family renting +# from the council at £6,000 a year and claiming no Universal Credit, so only +# the closure can end its Housing Benefit. Case 9 has a parent aged 70. In +# every case but 7, 8 and 11 the parent claims Universal Credit as a single +# claimant over 25 (universal_credit/standard_allowance/amount.yaml +# SINGLE_OLD: £400.14 a month in 2025-26, £424.90 in 2026-27) and gets the +# first child higher amount for a qualifying young person +# (elements/child/first/higher_amount.yaml: £339.00 in 2025-26, £351.88 in +# 2026-27). So: +# 2025 with the child element: 12 x (400.14 + 339.00) = 8,869.68 +# 2026 with the child element: 12 x (424.90 + 351.88) = 9,321.36 +# 2026 without the child element: 12 x 424.90 = 5,098.80 + +- name: A young person's own income-related ESA report alone removes the child element in 2026. + # No closure applies: the young person reports no closed benefit, and the + # claim pays for 86 of the 365 days of 2026-27 (esa_income_payable_share + # 0.2356), so the model pays 3,000 x 86 / 365 = 706.85 of income-related + # ESA (esa_income) and the young person is entitled to it. UC: 12 x 424.90 + # = 5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, false] + esa_income: 706.85 + is_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 5_098.80 + +- name: A young person's own income-related ESA report with their own Income Support report in 2026 leaves the child element. + # Income Support closed on 1 April 2026, so the young person's own report + # closes their legacy awards (person_legacy_benefits_closed) and the model + # pays no income-related ESA on their report (esa_income 0). Neither report + # excludes them. UC: 12 x (424.90 + 351.88) = 9,321.36; child element + # 12 x 351.88 = 4,222.56. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + income_support_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, true] + esa_income: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_222.56 + universal_credit: 9_321.36 + +- name: A young person's own income-related ESA report with their own income-based JSA report in 2026 leaves the child element. + # Income-based JSA closed on 1 April 2026, so the young person's own report + # closes their legacy awards and the model pays no income-related ESA on + # their report. UC: 12 x (424.90 + 351.88) = 9,321.36. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + jsa_income_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, true] + esa_income: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_222.56 + universal_credit: 9_321.36 + +- name: A young person's own income-related ESA report with their own Child Tax Credit report in 2025 leaves the child element. + # Tax credits closed on 6 April 2025, so the young person's own Child Tax + # Credit report closes their legacy awards and the model pays no + # income-related ESA on their report. UC: 12 x (400.14 + 339.00) = 8,869.68; + # child element 12 x 339.00 = 4,068.00. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + esa_income_reported: 3_000 + child_tax_credit_reported: 1_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, true] + esa_income: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_068 + universal_credit: 8_869.68 + +- name: A young person's own Income Support report with their own Child Tax Credit report in 2025 leaves the child element. + # The model pays Income Support in 2025, though never on a dependant's own + # report. The young person's own Child Tax Credit report closes their + # legacy awards (person_legacy_benefits_closed), and the predicate then + # drops their Income Support report too. UC: 12 x (400.14 + 339.00) = + # 8,869.68. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + income_support_reported: 3_000 + child_tax_credit_reported: 1_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, true] + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_068 + universal_credit: 8_869.68 + +- name: A young person's own income-based JSA report with their own Child Tax Credit report in 2025 leaves the child element. + # Income-based JSA still pays in 2025, but the young person's own Child Tax + # Credit report closes their legacy awards, so the model pays no + # income-based JSA on their report (jsa_income 0). UC: 8,869.68 as above. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + jsa_income_reported: 3_000 + child_tax_credit_reported: 1_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, true] + jsa_income: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_068 + universal_credit: 8_869.68 + +- name: A young person's own Housing Benefit report after the parent's legacy awards close leaves the child element. + # The family rents from the council at £6,000 a year and claims no + # Universal Credit, so without the parent's report the model continues its + # Housing Benefit award at the full rent (6,000, no income). The parent's + # own Income Support report from 2026 closes the family's legacy awards + # (legacy_benefits_closed), and the family doesn't meet the Pension Credit + # age conditions, so the model ends that award (housing_benefit_eligible), + # the young person's report included, and the predicate drops the report. + # The young person meets reg. 5, so the family's UC child element comes to + # 12 x 351.88 = 4,222.56, though the family claims no Universal Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + income_support_reported: 3_000 + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + legacy_benefits_closed: true + claims_universal_credit: false + housing_benefit_eligible: false + housing_benefit: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_222.56 + universal_credit: 0 + +- name: A young person's own Housing Benefit report after the parent's tax credits close leaves the child element. + # The same renting family in 2025, which claims no Universal Credit. The + # parent's own Child Tax Credit report from 2025, after tax credits closed, + # closes the family's legacy awards, so the model ends its Housing Benefit + # award (6,000 without the report) and the predicate drops the young + # person's report. UC child element: 12 x 339.00 = 4,068. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + child_tax_credit_reported: 1_000 + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + legacy_benefits_closed: true + claims_universal_credit: false + housing_benefit_eligible: false + housing_benefit: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + uc_child_element: 4_068 + universal_credit: 0 + +- name: A young person's own Housing Benefit report still excludes them where the family meets the Pension Credit age conditions. + # A parent aged 70 reports their own Working Tax Credit in 2025, after tax + # credits closed, which closes the family's legacy awards. The family meets + # the Pension Credit age conditions, so the model keeps its Housing Benefit + # unless it claims Universal Credit at the closure and is entitled to it + # (left_pension_route_at_legacy_closure). This family does, so the model + # pays it no Housing Benefit. The predicate can't read + # left_pension_route_at_legacy_closure, whose is_uc_entitled reads the + # predicate through the child element, so it keeps the Housing Benefit + # report. The young person is a dependant, so the family's claim doesn't + # end their own award either. UC: 12 x 400.14 = 4,801.68. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 70 + is_parent: true + is_claimant_or_partner: true + working_tax_credit_reported: 1_000 + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + legacy_benefits_closed: true + meets_pension_credit_age_conditions: true + is_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 4_801.68 + +- name: A young person's own Income Support report from 2026 doesn't lift their Housing Benefit exclusion. + # The young person's own Income Support report from 2026, after Income + # Support closed, closes their own legacy awards (person_legacy_benefits_closed), + # but not the family's: legacy_benefits_closed reads only the claimant's + # and partner's reports. The model ends Housing Benefit on the family's + # closure, so the young person's Housing Benefit report still excludes + # them. UC: 12 x 424.90 = 5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + income_support_reported: 3_000 + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + person_legacy_benefits_closed: [false, true] + legacy_benefits_closed: false + is_qualifying_young_person_for_universal_credit: [false, false] + universal_credit: 5_098.80 + +- name: A young person's own Housing Benefit report in temporary accommodation still excludes them after the parent's legacy awards close. + # The family of case 7 in temporary accommodation. The parent's own Income + # Support report closes the family's legacy awards, but the move off legacy + # benefits doesn't end a Housing Benefit award for specified or temporary + # accommodation (SI 2014/1230 reg. 46(2)). The model keeps the award + # (housing_benefit_eligible) and the predicate keeps the young person's + # report, so the family gets no child element. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_claimant_or_partner: true + income_support_reported: 3_000 + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + in_specified_or_temporary_accommodation: true + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + legacy_benefits_closed: true + claims_universal_credit: false + housing_benefit_eligible: true + is_qualifying_young_person_for_universal_credit: [false, false] + uc_child_element: 0 + universal_credit: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_universal_credit_claim.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_universal_credit_claim.yaml new file mode 100644 index 0000000000..f348fa21c5 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_qualifying_young_person_universal_credit_claim.yaml @@ -0,0 +1,427 @@ +# A claim for Universal Credit ends the claimant's own tax credits, Income +# Support and Housing Benefit, and either joint claimant's tax credits (SI +# 2014/1230 reg. 8(2) and (2A)), and the claimant's and partner's +# income-related ESA (SI 2013/983 art. 4). It ends nobody else's award. The +# model ends the family's continuing Housing Benefit award +# (housing_benefit_eligible) and its Child Tax Credit (is_CTC_eligible), and +# the claimant's and partner's income-related ESA (esa_income_report_closed), +# when the family claims (claims_universal_credit). Nobody is entitled to an +# award the claim has ended, so for a claimant or partner the UC qualifying +# young person test (SI 2014/1230 reg. 28) follows those three limbs. For a +# dependant it doesn't: a parent's claim leaves the dependant's own award. +# The model ends no Working Tax Credit, Income Support or income-based JSA +# award on the claim, so the predicate follows no claim limb for them. +# +# would_claim_uc decides the claim here. A case without a claim also sets +# would_claim_uc_at_legacy_closure false, so the family claims nothing when +# DWP moves it off its legacy benefits either. The reg. 5 variable +# reads universal_credit_reported and receives_benefits_in_own_right for reg. +# 5(5), not a calculated Universal Credit award, and these cases leave both +# at their defaults, so a claimant who claims Universal Credit still meets +# reg. 5. +# +# Rates (universal_credit/standard_allowance/amount.yaml, elements/child/): +# SINGLE_YOUNG £311.68 a month in 2024-25 and £338.58 in 2026-27; +# COUPLE_OLD £617.60 in 2024-25; the child amount for a child born after +# 5 April 2017 £287.92 in 2024-25 (elements/child/amount.yaml). +# Child Tax Credit child element £3,455 a year in 2024-25 +# (tax_credits/child_tax_credit/elements/child_element.yaml). + +- name: An 18-year-old claimant's own income-related ESA report leaves them a qualifying young person once they claim Universal Credit. + # The claimant claims Universal Credit, which ends their own income-related + # ESA (SI 2013/983 art. 4; esa_income_report_closed), so the model pays no + # income-related ESA on their report and they aren't entitled to it. UC: 12 + # x 338.58 = 4,062.96. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + households: + household: + members: [claimant] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + esa_income_report_closed: [true] + esa_income: 0 + is_qualifying_young_person_under_universal_credit_regulation_5: [true] + is_qualifying_young_person_for_universal_credit: [true] + universal_credit: 4_062.96 + +- name: An 18-year-old claimant's own income-related ESA report excludes them while they don't claim Universal Credit. + # Nothing ends the award: the claimant doesn't claim Universal Credit and + # reports no closed benefit, and the claim pays for 86 of the 365 days of + # 2026-27 (esa_income_payable_share 0.2356), so the model pays 3,000 x 86 / + # 365 = 706.85 of income-related ESA on their report and they're entitled + # to it. They get no Universal Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [claimant] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + esa_income_report_closed: [false] + esa_income: 706.85 + is_qualifying_young_person_for_universal_credit: [false] + universal_credit: 0 + +- name: An 18-year-old claimant's own Housing Benefit report leaves them a qualifying young person once they claim Universal Credit. + # The claimant rents from the council at £6,000 a year and claims Universal + # Credit, which ends their own Housing Benefit (SI 2014/1230 reg. 8(2A); + # housing_benefit_eligible), so the model pays no Housing Benefit on their + # report. UC: 12 x 338.58 + 6,000 housing element = 10,062.96. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + households: + household: + members: [claimant] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + housing_benefit_eligible: false + housing_benefit: 0 + is_qualifying_young_person_for_universal_credit: [true] + universal_credit: 10_062.96 + +- name: An 18-year-old claimant's own Housing Benefit report excludes them while they don't claim Universal Credit. + # The claimant rents from the council at £6,000 a year, has no income and + # doesn't claim Universal Credit, so the model continues their Housing + # Benefit award at the full rent (6,000) and they're entitled to it. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [claimant] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + housing_benefit_eligible: true + housing_benefit: 6_000 + is_qualifying_young_person_for_universal_credit: [false] + universal_credit: 0 + +- name: An 18-year-old lone parent's own Child Tax Credit report leaves them a qualifying young person once they claim Universal Credit. + # The claimant claims Universal Credit, which ends their tax credits (SI + # 2014/1230 reg. 8(2); is_CTC_eligible), so the model pays no Child Tax + # Credit. UC: 12 x (311.68 + 287.92) = 7,195.20. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + child_tax_credit_reported: 1_000 + child: + age: 1 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, child] + would_claim_uc: true + households: + household: + members: [claimant, child] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_CTC_eligible: false + child_tax_credit: 0 + is_qualifying_young_person_for_universal_credit: [true, false] + universal_credit: 7_195.20 + +- name: An 18-year-old lone parent's own Child Tax Credit report excludes them while they don't claim Universal Credit. + # Tax credits pay in 2024 and the claimant doesn't claim Universal Credit, + # so the model pays Child Tax Credit for the child born after 5 April 2017: + # the child element of 3,455, with no family element and no income to taper + # it. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + child_tax_credit_reported: 1_000 + child: + age: 1 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [claimant, child] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_CTC_eligible: true + child_tax_credit: 3_455 + is_qualifying_young_person_for_universal_credit: [false, false] + universal_credit: 0 + +- name: An 18-year-old claimant whose partner reports the couple's Child Tax Credit stays a qualifying young person once the couple claims Universal Credit. + # The members of a couple claim tax credits jointly (Tax Credits Act 2002 + # s.3(3)), so the partner's report gives the claimant the couple's award. + # The couple's claim for Universal Credit ends a joint claimant's tax + # credits (SI 2014/1230 reg. 8(2); is_CTC_eligible), so nobody is entitled + # to the award. UC: 12 x (617.60 + 287.92) = 10,866.24. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + partner: + age: 45 + is_claimant_or_partner: true + child_tax_credit_reported: 1_000 + child: + age: 1 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, partner, child] + would_claim_uc: true + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_CTC_eligible: false + child_tax_credit: 0 + is_qualifying_young_person_for_universal_credit: [true, false, false] + universal_credit: 10_866.24 + +- name: An 18-year-old claimant whose partner reports the couple's Child Tax Credit isn't a qualifying young person while the couple doesn't claim Universal Credit. + # Tax credits pay in 2024 and the couple doesn't claim Universal Credit, so + # the model pays the couple's Child Tax Credit (child element 3,455) and the + # claimant shares the joint award. + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + partner: + age: 45 + is_claimant_or_partner: true + child_tax_credit_reported: 1_000 + child: + age: 1 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, partner, child] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_CTC_eligible: true + child_tax_credit: 3_455 + is_qualifying_young_person_for_universal_credit: [false, false, false] + universal_credit: 0 + +- name: An 18-year-old lone parent's own Working Tax Credit report excludes them though they claim Universal Credit. + # Tax credits pay in 2024. The model ends Working Tax Credit when the + # family's legacy awards close, but not on its Universal Credit claim + # (is_WTC_eligible reads legacy_benefits_closed, not + # claims_universal_credit). The claimant works 16 hours a week and looks + # after a child, so they meet the model's lone parent hours test (16 hours + # from age 16), and the model keeps their award beside the claim. The + # predicate follows no claim limb for Working Tax Credit, so the + # claimant's own report still excludes them. + period: 2024 + input: + people: + claimant: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + weekly_hours: 16 + working_tax_credit_reported: 1_000 + child: + age: 1 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, child] + would_claim_uc: true + households: + household: + members: [claimant, child] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + claims_universal_credit: true + is_WTC_eligible: true + is_qualifying_young_person_under_universal_credit_regulation_5: [true, false] + is_qualifying_young_person_for_universal_credit: [false, false] + +- name: An 18-year-old partner's own Housing Benefit report excludes them while their mixed-age couple keeps the Pension Credit route. + # A claimant aged 75 who reports Pension Credit and an 18-year-old partner + # on a non-advanced course rent from the council at £6,000 a year and claim + # Universal Credit. The couple keeps the mixed-age Pension Credit saving + # (has_mixed_age_couple_pension_credit_saving), so it meets the Pension + # Credit age conditions. The model keeps such a family's Housing Benefit + # while it claims, unless it claims at a legacy closure and is entitled to + # Universal Credit (left_pension_route_at_legacy_closure), and nothing + # closes this family's legacy awards. So the model pays the full rent + # (6,000, no income) as Housing Benefit, the partner stays entitled to it, + # and their own report excludes them. The predicate can't read + # left_pension_route_at_legacy_closure, so it keeps any Housing Benefit + # report while the family meets those conditions. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 75 + is_claimant_or_partner: true + pension_credit_reported: 1_000 + partner: + age: 18 + current_education: POST_SECONDARY + is_claimant_or_partner: true + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [claimant, partner] + would_claim_uc: true + households: + household: + members: [claimant, partner] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + output: + meets_pension_credit_age_conditions: true + claims_universal_credit: true + housing_benefit_eligible: true + housing_benefit: 6_000 + is_qualifying_young_person_for_universal_credit: [false, false] + +- name: A parent's claim for Universal Credit leaves their young person's own Housing Benefit report excluding them. + # A parent aged 45 and a 17-year-old on a non-advanced course, owned + # outright. The parent's claim ends the parent's own awards, not the young + # person's (SI 2014/1230 reg. 8(2A) names the claimant's award), so the + # young person's own report still excludes them. UC: 12 x 424.90 = 5,098.80 + # (SINGLE_OLD, 2026-27). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_claimant_or_partner: true + is_parent: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + would_claim_uc: true + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + claims_universal_credit: true + is_qualifying_young_person_for_universal_credit: [false, false] + universal_credit: 5_098.80 + +- name: A young person's own Housing Benefit report excludes them while their parent doesn't claim Universal Credit. + # The same household without a claim: the young person's own report excludes + # them as with the claim, and the parent gets no Universal Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_claimant_or_partner: true + is_parent: true + young_person: + age: 17 + current_education: POST_SECONDARY + is_claimant_or_partner: false + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [parent, young_person] + would_claim_uc: false + would_claim_uc_at_legacy_closure: false + households: + household: + members: [parent, young_person] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + claims_universal_credit: false + is_qualifying_young_person_for_universal_credit: [false, false] + universal_credit: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml index 3fd69d83e7..2ca48da78e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml @@ -1,9 +1,22 @@ -# UC reg. 5(5) and SPC reg. 4A(5) exclude own awards from both QYP routes. -# The reports identify the recipient whether receives_benefits_in_own_right -# is omitted or explicitly false. SPC reg. 4A(5) also lists income support, -# which UC reg. 5(5) does not: is_on_income_support reads it, from the -# calculated award for a claimant or partner and from their own report for -# anyone else. +# The Secretary of State excludes a person receiving Universal Credit, +# contributory ESA or contribution-based JSA (UC reg. 5(5)), and defines ESA +# and JSA without their income-related and income-based forms (UC Regs reg. +# 2). DWP doesn't count a person entitled to income-related ESA, income-based +# JSA or Income Support as a UC qualifying young person (SI 2014/1230 reg. +# 28). The UC predicate reads a report only in a year the model pays that +# benefit (income-based JSA and Income Support until 31 March 2026), reads an +# income-related ESA, income-based JSA or Income Support report only until +# the person's legacy awards close (person_legacy_benefits_closed), and +# reads none that a reform neutralises. The model also ends a claimant's or +# partner's own income-related ESA when the family claims Universal Credit +# (esa_income_report_closed), so the cases that test a claimant's own +# income-related ESA report claim no Universal Credit (would_claim_uc). The +# Secretary of State excludes own awards +# from the Pension Credit routes, including income support (SPC reg. 4A(5)): +# is_on_income_support reads it, from the calculated award for a claimant or +# partner and from their own report for anyone else. The reports identify the +# recipient whether receives_benefits_in_own_right is omitted or explicitly +# false. - name: A 16-year-old reporting income-related ESA does not attract child additions for a parent aged 40. period: 2026 @@ -41,7 +54,8 @@ child_minimum_guarantee_addition: 0 - name: A 16-year-old reporting income-based JSA does not attract child additions for a parent aged 40. - period: 2026 + # Income-based JSA pays until 31 March 2026 (gov.dwp.JSA.income.active). + period: 2025 input: people: parent: @@ -216,7 +230,8 @@ child_minimum_guarantee_addition: 0 - name: A 16-year-old reporting income-based JSA does not attract child additions for a parent aged 70. - period: 2026 + # Income-based JSA pays until 31 March 2026 (gov.dwp.JSA.income.active). + period: 2025 input: people: parent: @@ -356,8 +371,12 @@ child_minimum_guarantee_addition: 0 - name: A reported award also excludes the education route. - period: 2026 + # Income-based JSA pays until 31 March 2026 (gov.dwp.JSA.income.active). + # Each person forms their own benefit unit as its claimant, and none claims + # Universal Credit, so no claim ends their income-related ESA. + period: 2025 input: + would_claim_uc: [false, false, false, false, false] age: [18, 18, 18, 18, 18] current_education: [UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY] universal_credit_reported: [5_000, 0, 0, 0, 0] @@ -370,8 +389,12 @@ is_qualifying_young_person_for_pension_credit: [false, false, false, false, false] - name: An explicit false own-right flag does not erase a reported own ESA award. + # The person claims no Universal Credit, now or when DWP moves them off + # their legacy benefits, so no claim ends their award. period: 2026 input: + would_claim_uc: false + would_claim_uc_at_legacy_closure: false age: 16 current_education: NOT_IN_EDUCATION receives_benefits_in_own_right: false @@ -428,9 +451,10 @@ is_qualifying_young_person_for_pension_credit: false is_qualifying_young_person_for_child_benefit: true -- name: A 16-year-old reporting their own Income Support does not attract the Pension Credit child addition, but stays a UC young person. - # SPC reg. 4A(5) lists income support; UC reg. 5(5) does not. Income - # Support is in payment in 2025 (gov.dwp.income_support.active). +- name: A 16-year-old reporting their own Income Support counts as neither a Pension Credit nor a UC young person. + # SPC reg. 4A(5) lists income support; for UC, TP reg. 28 excludes a + # person entitled to it. Income Support is in payment in 2025 + # (gov.dwp.income_support.active). period: 2025 input: people: @@ -459,9 +483,9 @@ is_qualifying_young_person_for_pension_credit: [false, false] is_child_or_qualifying_young_person_for_pension_credit: [false, false] child_minimum_guarantee_addition: 0 - is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] -- name: A member's own reported Income Support excludes the Pension Credit education route only. +- name: A member's own reported Income Support excludes the Pension Credit and UC education routes. period: 2025 input: people: @@ -484,11 +508,12 @@ output: is_on_income_support: [false, true] is_qualifying_young_person_for_pension_credit: [false, false] - is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] -- name: A member's Income Support report leaves the Pension Credit route available once Income Support stops paying. +- name: A member's Income Support report leaves the Pension Credit and UC routes available once Income Support stops paying. # Income Support stops paying in 2026 (gov.dwp.income_support.active), so a - # report then is no award in payment. + # report then is no award in payment for Pension Credit, and nobody is + # entitled to it for TP reg. 28. period: 2026 input: people: @@ -513,7 +538,7 @@ is_qualifying_young_person_for_pension_credit: [false, true] is_qualifying_young_person_for_universal_credit: [false, true] -- name: A claimant's calculated Income Support excludes them from the Pension Credit status. +- name: A claimant's calculated Income Support excludes them from the Pension Credit and UC statuses. period: 2025 input: age: 17 @@ -524,9 +549,9 @@ income_support: 3_790.80 is_on_income_support: true is_qualifying_young_person_for_pension_credit: false - is_qualifying_young_person_for_universal_credit: true + is_qualifying_young_person_for_universal_credit: false -- name: A claimant's report without a calculated award leaves the Pension Credit status available. +- name: A claimant's report without a calculated award leaves the Pension Credit status available but not the UC one. # The claimant falls within no prescribed category, so the model calculates # no Income Support for them. period: 2025 @@ -538,7 +563,7 @@ income_support: 0 is_on_income_support: false is_qualifying_young_person_for_pension_credit: true - is_qualifying_young_person_for_universal_credit: true + is_qualifying_young_person_for_universal_credit: false - name: The partner of an Income Support claimant keeps the Pension Credit status. # A couple's award goes to the claimant: the partner who reports nothing is @@ -569,8 +594,11 @@ is_qualifying_young_person_for_pension_credit: [false, true] - name: A report of one penny of a listed award other than Income Support excludes the young person. + # Each person forms their own benefit unit as its claimant, and none claims + # Universal Credit, so no claim ends their income-related ESA. period: 2025 input: + would_claim_uc: [false, false, false, false, false] age: [16, 16, 16, 16, 16] current_education: [NOT_IN_EDUCATION, NOT_IN_EDUCATION, NOT_IN_EDUCATION, NOT_IN_EDUCATION, NOT_IN_EDUCATION] universal_credit_reported: [0.01, 0, 0, 0, 0] @@ -582,7 +610,7 @@ is_qualifying_young_person_for_universal_credit: [false, false, false, false, false] is_qualifying_young_person_for_pension_credit: [false, false, false, false, false] -- name: A member's report of one penny of Income Support excludes them from the Pension Credit status alone. +- name: A member's report of one penny of Income Support excludes them from the Pension Credit and UC statuses. period: 2025 input: people: @@ -605,4 +633,4 @@ output: is_on_income_support: [false, true] is_qualifying_young_person_for_pension_credit: [false, false] - is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_receiving_education.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_receiving_education.yaml index bc1ed54e69..35386bd3d5 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_receiving_education.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_receiving_education.yaml @@ -815,6 +815,13 @@ # Housing Benefit (reg. 2(1)), doesn't count as a qualifying young person # for the Universal Credit Regulations. With no maintenance funding, reg. # 12(2) doesn't apply either, so the claimant doesn't receive education. + # The claimant claims Universal Credit, which ends their own Housing + # Benefit two weeks after the award starts (reg. 8(2A)). + # is_qualifying_young_person_for_universal_credit follows that claim limb + # and reads reg. 5(5) only from reported Universal Credit, so it gives true + # for this claimant. The condition reads the status on the first day of the + # award, when the Housing Benefit still runs + # (is_qualifying_young_person_for_uc_education). period: 2026 input: people: @@ -886,17 +893,21 @@ members: [person1, person2] region: NORTH_EAST output: - is_qualifying_young_person_for_universal_credit: [true, true] + is_qualifying_young_person_for_universal_credit: [false, false] is_qualifying_young_person_for_uc_education: [false, false] uc_receiving_education: [false, false] - name: Case 34, the 18-year-old of case 32 claims when DWP moves them off legacy benefits and keeps the award. # The claimant reports Housing Benefit, so under SI 2014/1230 reg. 28 they - # don't count as a qualifying young person, and they receive education + # don't count as a qualifying young person for the condition + # (is_qualifying_young_person_for_uc_education), and they receive education # under reg. 12(2)(b), not reg. 12(1A). They fail the condition on the # migration day "because the person is undertaking a full-time course (see # regulation 12(2) and 13 ...)", so under reg. 60(1) it doesn't apply to - # them while they continue the course. + # them while they continue the course. Their claim ends their own Housing + # Benefit, so is_qualifying_young_person_for_universal_credit, which + # follows that claim limb and reads reg. 5(5) only from reported + # Universal Credit, gives true. period: 2026 input: people: @@ -1256,9 +1267,150 @@ output: legacy_benefits_closed: true claims_uc_at_legacy_closure: true - is_qualifying_young_person_for_uc_education: [false] + is_qualifying_young_person_for_uc_education: [true] + is_qualifying_young_person_on_uc_migration_day: [false] uc_receiving_education: [true] uc_is_excepted_from_education_condition: [true] meets_uc_education_condition: [true] is_uc_eligible: true universal_credit: 4_062.96 + +- name: Case 43, the 18-year-old of case 31 in Northern Ireland, whose family rents, keeps the ordinary route. + # SR 2016/226 reg. 6(2A): a claim ends a rent rebate or allowance two weeks + # after the award starts, so on the first day the claimant keeps an existing + # benefit and reg. 29 keeps them out of the definition, as in case 31. + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTHERN_IRELAND + tenure_type: RENT_PRIVATELY + output: + is_qualifying_young_person_for_universal_credit: [true] + is_qualifying_young_person_for_uc_education: [false] + uc_receiving_education: [false] + is_uc_eligible: true + +- name: Case 44, the same 18-year-old in Northern Ireland, whose report covers only a rate rebate, receives education. + # SR 2016/226 reg. 6(2): the claim ends a rate rebate the day before the award + # starts. On the first day the claimant has no existing benefit, so they count + # as a qualifying young person and receive education (Universal Credit + # Regulations (Northern Ireland) 2016 reg. 12(1)). No exception applies, so they + # get no Universal Credit. + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + housing_benefit_reported: 6_000 + housing_benefit_report_is_rate_rebate_only: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTHERN_IRELAND + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [true] + is_qualifying_young_person_for_uc_education: [true] + uc_receiving_education: [true] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 45, the same 18-year-old in England in a home the family owns keeps the ordinary route. + # SI 2014/1230 reg. 8(2A) runs on an award of Housing Benefit whatever its + # form, so reg. 28 keeps the claimant out of the definition on the first day. + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_universal_credit: [true] + is_qualifying_young_person_for_uc_education: [false] + uc_receiving_education: [false] + is_uc_eligible: true + +- name: Case 46, the 18-year-old of case 31 in temporary accommodation stays outside the definition all year. + # SI 2014/1230 reg. 8(3): a claim doesn't end a Housing Benefit award for + # specified or temporary accommodation, so reg. 28 keeps the claimant out of + # the definition after the first day as well, and they take the ordinary + # route, as in case 31. + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [person1] + in_specified_or_temporary_accommodation: true + households: + household: + members: [person1] + region: NORTH_EAST + tenure_type: RENT_PRIVATELY + output: + is_qualifying_young_person_for_universal_credit: [false] + is_qualifying_young_person_for_uc_education: [false] + uc_receiving_education: [false] + is_uc_eligible: true + +- name: Case 47, the same 18-year-old in a caravan the family owns in Northern Ireland, with a rent allowance, keeps the ordinary route. + # Housing Benefit Regulations (Northern Ireland) 2006 reg. 13(1)(f) and (g): a + # rent rebate or allowance covers mooring charges for a houseboat and site + # payments for a caravan or mobile home, so an owner can get one. SR 2016/226 + # reg. 6(2A) runs it on for two weeks, so on the first day reg. 29 keeps the + # claimant out of the definition, as in case 43. They get the standard allowance + # for a single person under 25: 12 x 338.58 (SR 2026/54 reg. 3(3)(a)). + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTHERN_IRELAND + tenure_type: OWNED_OUTRIGHT + accommodation_type: MOBILE + output: + is_qualifying_young_person_for_universal_credit: [true] + is_qualifying_young_person_for_uc_education: [false] + uc_receiving_education: [false] + is_uc_eligible: true + universal_credit: 4_062.96 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml index 1abdbeba87..a1672030f8 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml @@ -1,6 +1,6 @@ # Each Council Tax Reduction scheme's own "young person". Scotland's -# working-age scheme, from 1 April 2022, uses the Universal Credit qualifying -# young person (SSI 2021/249 reg. 6(1); UC Regs 2013 reg. 5). England, Wales +# working-age scheme, from 1 April 2022, uses the qualifying young person of +# UC Regs 2013 reg. 5 (SSI 2021/249 reg. 6(1)). England, Wales # and Scotland's pension-age scheme use the Child Benefit qualifying young # person (SI 2012/2885 reg. 2; WSI 2013/3029 reg. 2; SSI 2012/319 reg. 4), as # did Scotland's working-age scheme until 31 March 2022 (SSI 2012/303 reg. 4). @@ -351,3 +351,99 @@ is_council_tax_reduction_applicant_or_partner: [true, false, false] is_child_or_young_person_for_council_tax_reduction: [true, false, false] council_tax_reduction_applicable_income: 10_000 + +- name: Case 11, Scotland working age, a 19-year-old's own Housing Benefit leaves them a young person. + # The Secretary of State excludes a person entitled to an existing benefit + # from the qualifying young person only for the purposes of the UC + # Regulations and WRA 2012 Part 1 (SI 2014/1230 reg. 28). The Scottish + # Ministers define the young person by UC Regs reg. 5 (SSI 2021/249 reg. + # 6(1)), so the 19-year-old stays in the family. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: true + housing_benefit_reported: 3_000 + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + is_qualifying_young_person_under_universal_credit_regulation_5: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_young_person_for_council_tax_reduction: [false, true] + is_council_tax_reduction_lone_parent: true + # Lone parent aged 18 or over: 95.55 * 52. + council_tax_reduction_applicable_amount: 4_968.60 + +- name: Case 12, Scotland working age, a 17-year-old's own Income Support, income-based JSA or income-related ESA leaves them a young person, and contribution-based JSA does not. + period: 2025 + input: + age: [17, 17, 17, 17] + current_education: [UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY] + is_claimant_or_partner: [false, false, false, false] + income_support_reported: [3_000, 0, 0, 0] + jsa_income_reported: [0, 3_000, 0, 0] + esa_income_reported: [0, 0, 3_000, 0] + jsa_contrib_reported: [0, 0, 0, 3_000] + country: [SCOTLAND, SCOTLAND, SCOTLAND, SCOTLAND] + output: + is_qualifying_young_person_for_universal_credit: [false, false, false, false] + # The Secretary of State excludes a person receiving contribution-based + # JSA (reg. 5(5)), so only that report removes the young person. + is_qualifying_young_person_under_universal_credit_regulation_5: [true, true, true, false] + is_child_or_young_person_for_council_tax_reduction: [true, true, true, false] + +- name: Case 13, Scotland working age, a 19-year-old's own income-related ESA leaves the earning applicant a lone parent and raises the award. + # The Scottish Ministers define the young person by UC Regs reg. 5 (SSI + # 2021/249 reg. 6(1)), so the 19-year-old stays in the family and the + # 22-year-old applicant is a lone parent: 95.55 * 52 = 4,968.60. Applicable + # income is the applicant's earnings, 5,000, and the family's Child Benefit, + # 27.05 * 52 = 1,406.60: 6,406.60. The formula withdraws 20% of the excess, + # 0.2 * (6,406.60 - 4,968.60) = 287.60, from the 1,800 liability. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + employment_income: 5_000 + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + would_claim_council_tax_reduction: true + households: + household: + members: [claimant, student] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + is_child_or_young_person_for_council_tax_reduction: [false, true] + is_council_tax_reduction_lone_parent: true + council_tax_reduction_applicable_amount: 4_968.60 + council_tax_reduction_applicable_income: 6_406.60 + council_tax_reduction: 1_512.40 diff --git a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py index 076d8ffa2c..058700ec0c 100644 --- a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py @@ -31,7 +31,8 @@ Each scheme applies its own exceptions. UC reg 83(1)(f) names a "child or qualifying young person"; HB reg 75F(1)(e) names a "child or young person". UC includes a 16-year-old school leaver (reg 5(1)(a)) who does not receive -benefits in their own right (reg 5(5)) and is not looked after (reg 4(6)). +benefits in their own right (reg 5(5); SI 2014/1230 reg. 28) and is not +looked after (reg 4(6)). HB young persons follow Child Benefit (reg 19), so only the education-based young-person role counts for Housing Benefit below. UC reg 83(1)(c) names "a claimant" for attendance allowance, which includes armed forces @@ -86,6 +87,7 @@ from policyengine_uk import Simulation from policyengine_uk.model_api import BenUnit, Variable from policyengine_uk.scenarios.uc_reform import REBALANCING_YEARS +from policyengine_uk.tax_benefit_system import system as baseline_system from policyengine_uk.utils.supplied_inputs import set_model_value, supplied_input YEAR = 2025 @@ -277,9 +279,20 @@ def own_esa(m): return m["esa_contrib_reported"] > 0 or m["esa_income_reported"] > 0 -def own_benefits(m): - # UC reg 5(5): a personal report identifies an award in the person's own right. - return any(m.get(report, 0) > 0 for report in OWN_RIGHT_REPORTS) +def own_benefits(m, year=YEAR): + # UC reg. 5(5) (UC, contributory ESA, contribution-based JSA) and SI + # 2014/1230 reg. 28 (income-related ESA, and income-based JSA while it + # pays, as in 2025): a personal report identifies an award in the + # person's own right. The Secretary of State ties reg. 28's exclusion to + # entitlement, so the oracle counts an income-based JSA report only in a + # year the model pays it. + dwp = baseline_system.parameters(f"{year}-01-01").gov.dwp + reports = [ + report + for report in OWN_RIGHT_REPORTS + if report != "jsa_income_reported" or dwp.JSA.income.active + ] + return any(m.get(report, 0) > 0 for report in reports) def reference_specified_benefit(members, benunit_inputs, scheme): diff --git a/policyengine_uk/tests/test_council_tax_reduction_severe_disability_family.py b/policyengine_uk/tests/test_council_tax_reduction_severe_disability_family.py index 4334138476..f2e0d4279e 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_severe_disability_family.py +++ b/policyengine_uk/tests/test_council_tax_reduction_severe_disability_family.py @@ -1,10 +1,11 @@ """CTR family-mask input contracts, separate from derived eligibility tests. SSI 2021/249 regs 6 and 8(2)(a), Sch 1 para 13 retain an applicant's -UC young person in their family. These finite grids deliberately supply the -two young-person statuses to exercise both directions independently of the -upstream eligibility rules. The policy YAML uses school/course inputs and -the annual terminal-date input, plus actual PIP, pension and council tax. +young person under UC Regs 2013 reg. 5 in their family. These finite grids +deliberately supply the two young-person statuses to exercise both directions +independently of the upstream eligibility rules. The policy YAML uses +school/course inputs and the annual terminal-date input, plus actual PIP, +pension and council tax. Looked-after and other-benefit-unit treatment remains the existing model baseline, not a certification of reg 8(2)(b) and its paragraph (3) overrides. @@ -31,7 +32,9 @@ def input_grid(rows, country="SCOTLAND", year=2026, applicant_age=45): "age": row.get("age", 19), "is_claimant_or_partner": False, "is_child_or_qualifying_young_person_for_child_benefit": row["cb"], - "is_qualifying_young_person_for_universal_credit": row["uc"], + # SSI 2021/249 reg. 6(1) reads UC Regs reg. 5, without SI + # 2014/1230 reg. 28. + "is_qualifying_young_person_under_universal_credit_regulation_5": row["uc"], "is_looked_after_by_local_authority": row.get("looked_after", False), "is_blind": row.get("blind", False), "pip_dl_category": "STANDARD" if row.get("benefit", False) else "NONE", diff --git a/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py b/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py index 1bf2efbf8f..07f0378bf4 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py @@ -1,8 +1,11 @@ """Each Council Tax Reduction scheme's own young person. -Scotland's working-age scheme, from 1 April 2022, defines a young person as a -Universal Credit qualifying young person (Council Tax Reduction (Scotland) -Regulations 2021 reg. 6(1); UC Regs 2013 reg. 5). England, Wales and +Scotland's working-age scheme, from 1 April 2022, defines a young person by +the qualifying young person of UC Regs 2013 reg. 5 (Council Tax Reduction +(Scotland) Regulations 2021 reg. 6(1)). The Secretary of State excludes a +person entitled to an existing benefit only for the purposes of the UC +Regulations and WRA 2012 Part 1 (SI 2014/1230 reg. 28), so that exclusion +stays out. England, Wales and Scotland's pension-age scheme use the Child Benefit qualifying young person (SI 2012/2885 reg. 2; WSI 2013/3029 reg. 2; SSI 2012/319 reg. 4), as Scotland's working-age scheme did until 31 March 2022 (SSI 2012/303 reg. 4). Every scheme @@ -158,7 +161,9 @@ def assert_scheme_selection(units, year): universal_credit = ( ( under_16 - | sim.calculate("is_qualifying_young_person_for_universal_credit", year) + | sim.calculate( + "is_qualifying_young_person_under_universal_credit_regulation_5", year + ) ) & ~looked_after & ~claimant_or_partner @@ -255,7 +260,9 @@ def test_scheme_selection_over_every_combination(): # Not vacuous: some of those lone claimants are qualifying young # persons by status, in Scotland's working-age scheme too. sim = simulate(alone, year) - status = sim.calculate("is_qualifying_young_person_for_universal_credit", year) + status = sim.calculate( + "is_qualifying_young_person_under_universal_credit_regulation_5", year + ) scottish_alone = np.array( [follows_universal_credit(unit, year) for unit in alone] ) @@ -280,7 +287,7 @@ def test_schemes_differ_only_where_the_two_definitions_differ(units): sim = simulate(variants, year) flag = sim.calculate("is_child_or_young_person_for_council_tax_reduction", year) agree = sim.calculate( - "is_qualifying_young_person_for_universal_credit", year + "is_qualifying_young_person_under_universal_credit_regulation_5", year ) == sim.calculate("is_qualifying_young_person_for_child_benefit", year) offset = 0 for unit in units: diff --git a/policyengine_uk/tests/test_disable_simulated_benefits.py b/policyengine_uk/tests/test_disable_simulated_benefits.py index 31875f8ea3..292a35cc03 100644 --- a/policyengine_uk/tests/test_disable_simulated_benefits.py +++ b/policyengine_uk/tests/test_disable_simulated_benefits.py @@ -838,3 +838,134 @@ def test_no_person_is_moved_off_their_legacy_benefits(reformed, year): # reported ESA in household income while incorrectly ending their status. closed = by_person(reformed, "person_legacy_benefits_closed", year) assert closed == dict.fromkeys(closed, False) + + +# A parent aged 45 and a 17-year-old on a non-advanced course in each family; +# the 17-year-old reports £3,000 of one existing benefit (SI 2014/1230 reg. +# 2(1)) in the dataset year. +YOUNG_PERSON_REPORTS = { + "income_support_reported": "income_support", + "jsa_income_reported": "jsa_income", + "esa_income_reported": "esa_income", + "housing_benefit_reported": "housing_benefit", + "child_tax_credit_reported": "child_tax_credit", + "working_tax_credit_reported": "working_tax_credit", +} + + +def young_person_situation(): + people, benunits, households = {}, {}, {} + for report in YOUNG_PERSON_REPORTS: + parent, young = f"{report}_parent", f"{report}_young" + people[parent] = dict(adult(45, True), is_parent=every_year(True)) + people[young] = { + "age": every_year(17), + "is_claimant_or_partner": every_year(False), + "current_education": every_year("POST_SECONDARY"), + report: {DATASET_YEAR: 3_000}, + } + benunits[report] = {"members": [parent, young]} + households[report] = {"members": [parent, young]} + return {"people": people, "benunits": benunits, "households": households} + + +@pytest.mark.parametrize("year", YEARS) +def test_a_young_person_on_a_benefit_the_reform_pays_is_no_qualifying_young_person( + year, +): + # The reform pays each benefit at its dataset-year report after the + # model's closure dates (income-based JSA from 2026, tax credits from + # 2025), so the young person stays entitled to it, and the Secretary of + # State excludes them from the UC qualifying young person test (reg. 28), + # in each of the reform's years. Income + # Support is the claimant's family's award, so the model pays none on a + # dependant's own report, with the reform or without it; the predicate + # reads that report in the years the model pays Income Support to anyone. + reformed = Simulation(situation=young_person_situation(), scenario=REFORM) + unreformed = Simulation(situation=young_person_situation()) + predicate = "is_qualifying_young_person_for_universal_credit" + paid = [ + float(reformed.calculate(benefit, year)[i]) + for i, benefit in enumerate(YOUNG_PERSON_REPORTS.values()) + ] + assert paid == [0, 3_000, 3_000, 3_000, 3_000, 3_000] + assert reformed.calculate(predicate, year)[1::2].tolist() == [False] * 6 + # Without the reform, the model stops paying tax credits from 2025 and + # Income Support and income-based JSA from 2026, and those reports + # exclude nobody. + tax_credits_closed = year >= 2025 + legacy_closed = year >= 2026 + assert unreformed.calculate(predicate, year)[1::2].tolist() == [ + legacy_closed, + legacy_closed, + year >= 2027, + False, + tax_credits_closed, + tax_credits_closed, + ] + + +# An 18-year-old claimant on a non-advanced course, a lone parent of a child +# aged 1, renting from the council at £6,000 a year, who reports £3,000 of +# their own award in the dataset year. The family claims Universal Credit +# (would_claim_uc defaults to true). +CLAIMANT_REPORTS = { + "esa_income_reported": "esa_income", + "housing_benefit_reported": "housing_benefit", + "child_tax_credit_reported": "child_tax_credit", +} + + +def claimant_situation(): + people, benunits, households = {}, {}, {} + for report in CLAIMANT_REPORTS: + claimant, child = f"{report}_claimant", f"{report}_child" + people[claimant] = { + "age": every_year(18), + "is_claimant_or_partner": every_year(True), + "current_education": every_year("POST_SECONDARY"), + report: {DATASET_YEAR: 3_000}, + } + people[child] = { + "age": every_year(1), + "is_claimant_or_partner": every_year(False), + } + benunits[report] = {"members": [claimant, child]} + households[report] = { + "members": [claimant, child], + "region": every_year("NORTH_EAST"), + "tenure_type": every_year("RENT_FROM_COUNCIL"), + "rent": every_year(6_000), + } + return {"people": people, "benunits": benunits, "households": households} + + +@pytest.mark.parametrize("year", YEARS) +@pytest.mark.parametrize("index, report", list(enumerate(CLAIMANT_REPORTS))) +def test_a_claimant_on_a_benefit_the_reform_pays_beside_universal_credit_is_no_qualifying_young_person( + year, index, report +): + # A claim for Universal Credit ends the claimant's own income-related + # ESA, Housing Benefit and Child Tax Credit (SI 2014/1230 reg. 8(2) and + # (2A); SI 2013/983 art. 4), and without the reform the model ends each + # award on the claim, so the predicate doesn't read the report (the + # model's year gate closes the Child Tax Credit report from 2025 too). + # In its years, the dataset year and the nine after it, the reform pays + # each report as reported, claim or none, so the claimant stays entitled + # to it and the predicate excludes them. + reformed = Simulation(situation=claimant_situation(), scenario=REFORM) + unreformed = Simulation(situation=claimant_situation()) + predicate = "is_qualifying_young_person_for_universal_credit" + benefit = CLAIMANT_REPORTS[report] + person = 2 * index + + def paid(simulation): + amounts = simulation.calculate(benefit, year) + return float(amounts[person] if benefit == "esa_income" else amounts[index]) + + assert reformed.calculate("claims_universal_credit", year)[index] + assert paid(reformed) == 3_000 + assert not reformed.calculate(predicate, year)[person] + assert unreformed.calculate("claims_universal_credit", year)[index] + assert paid(unreformed) == 0 + assert unreformed.calculate(predicate, year)[person] diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py index 602a71ac26..de0081f978 100644 --- a/policyengine_uk/tests/test_legacy_award_readers_properties.py +++ b/policyengine_uk/tests/test_legacy_award_readers_properties.py @@ -690,7 +690,9 @@ def test_other_members_awards_never_change_the_readers_for_young_singles(drawn): @pytest.mark.parametrize("report", ["esa_income_reported", "jsa_income_reported"]) def test_sixteen_year_old_own_award_leaves_young_single_readers_unchanged(report): # A natural route-(a) case: no route or own-right Boolean is supplied. - # UC reg 5(5) and SPC reg 4A(5) still exclude the member's own award. + # The Secretary of State excludes the member's own income-related ESA or + # income-based JSA (paying in 2025) from the UC test (SI 2014/1230 reg. + # 28) and from the Pension Credit test (SPC reg. 4A(5)). family = ( [ { diff --git a/policyengine_uk/tests/test_pension_credit_young_person_income_support.py b/policyengine_uk/tests/test_pension_credit_young_person_income_support.py index 18e2464fd6..7f31244d41 100644 --- a/policyengine_uk/tests/test_pension_credit_young_person_income_support.py +++ b/policyengine_uk/tests/test_pension_credit_young_person_income_support.py @@ -3,8 +3,10 @@ A 16-year-old outside education, in a 70-year-old's benefit unit, reports their own Income Support in 2025, when it is in payment. They are not the claimant's qualifying young person, so the claimant gets no child addition, -until a reform removes Income Support. UC reg. 5(5) does not list income -support, so they stay a UC qualifying young person throughout. +until a reform removes Income Support. DWP doesn't count them as a UC +qualifying young person while they're entitled to Income Support (SI +2014/1230 reg. 28), so the reform that removes Income Support restores both +statuses. A young carer who claims Income Support in their own benefit unit follows the calculated award. While they receive it they are no qualifying young @@ -188,10 +190,12 @@ def test_own_income_support_ends_the_young_person_until_a_reform_removes_it(): ).tolist() == [False, True] assert kept.calculate("child_minimum_guarantee_addition", YEAR).tolist() == [0] assert removed.calculate("child_minimum_guarantee_addition", YEAR)[0] > 0 - for sim in (kept, removed): - assert sim.calculate( - "is_qualifying_young_person_for_universal_credit", YEAR - ).tolist() == [False, True] + assert kept.calculate( + "is_qualifying_young_person_for_universal_credit", YEAR + ).tolist() == [False, False] + assert removed.calculate( + "is_qualifying_young_person_for_universal_credit", YEAR + ).tolist() == [False, True] @pytest.mark.usefixtures("cloned_uk_tax_benefit_system") diff --git a/policyengine_uk/tests/test_uc_qualifying_young_person_existing_benefits_properties.py b/policyengine_uk/tests/test_uc_qualifying_young_person_existing_benefits_properties.py new file mode 100644 index 0000000000..1561c2270d --- /dev/null +++ b/policyengine_uk/tests/test_uc_qualifying_young_person_existing_benefits_properties.py @@ -0,0 +1,570 @@ +"""Properties of the UC qualifying young person test under SI 2014/1230 reg. 28. + +DWP doesn't count a person entitled to an existing benefit (SI 2014/1230 +reg. 2(1)) as a UC qualifying young person (reg. 28). The predicate reads the +person's own reports of income-based JSA, income-related ESA, Income Support, +Housing Benefit, Child Tax Credit and Working Tax Credit and, for a claimant +or partner, the couple's joint tax credit award (Tax Credits Act 2002 +s.3(3)). It reads a report only in a year the model pays that benefit and +until the model's legacy closure, or for a claimant or partner the family's +Universal Credit claim, ends the award on it, and only if no reform +neutralises it; these tests run without a reform, and +test_uc_qualifying_young_person_neutralised_benefits.py covers reforms. The +model stops paying a benefit when its year gate closes, and ends a person's +legacy awards when its annual migration proxy closes them: their own Income +Support, income-based JSA and income-related ESA when +person_legacy_benefits_closed holds, the family's tax credits when +legacy_benefits_closed holds, and its Housing Benefit when +legacy_benefits_closed holds and the family doesn't meet the Pension Credit +age conditions. A claim for Universal Credit ends the claimant's and +partner's own awards (SI 2014/1230 reg. 8(2) and (2A); SI 2013/983 art. 4), +and the model ends the family's continuing Housing Benefit award and its +Child Tax Credit, and the claimant's and partner's income-related ESA, when +the family claims (claims_universal_credit). The oracle reads those closure +variables from the model, and each family draws would_claim_uc. These tests +check that: + +- any positive own report of an existing benefit whose year gate, legacy + closure and, for a claimant or partner, claim limb leave it open makes the + predicate false; +- with every such report at zero, the predicate equals the UC Regs 2013 + reg. 5 test less the person's reported UC, ESA and JSA awards; +- compared with the same family without the claimant's and partner's + reports, those reports change a dependant's predicate only where the + proxy closes the family's legacy awards on them, and then only to lift an + exclusion; +- a person whose legacy awards have closed is never excluded through their + own income-related ESA report, nor through a Housing Benefit report once + the family's awards close outside the Pension Credit age conditions; +- a parent's Universal Credit never rises when one existing benefit report + of their young person turns positive and the young person reports no + other; +- the predicate equals the reg. 5 test less reg. 28's exclusions, exactly; +- turning would_claim_uc on never changes the predicate of anyone who isn't + the claimant or partner; +- turning would_claim_uc on never turns a claimant's or partner's predicate + from true to false. +""" + +from unittest.mock import patch + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.tax_benefit_system import system +from policyengine_uk.variables.household.demographic.highest_education import ( + EducationType, +) + +EDUCATIONS = tuple(item.name for item in EducationType) +NON_ADVANCED = ("UPPER_SECONDARY", "POST_SECONDARY") +YEARS = (2024, 2025, 2026) +EXISTING_BENEFITS = ( + "jsa_income_reported", + "esa_income_reported", + "income_support_reported", + "housing_benefit_reported", + "child_tax_credit_reported", + "working_tax_credit_reported", +) +TAX_CREDITS = ("child_tax_credit_reported", "working_tax_credit_reported") +# The Secretary of State defines ESA and JSA without their income-related and +# income-based forms (UC Regs reg. 2), so reg. 5(5) reaches only these. +REG_5_5_REPORTS = ( + "universal_credit_reported", + "esa_contrib_reported", + "jsa_contrib_reported", +) +# The existing benefits the model pays in each year: tax credits until 5 April +# 2025 (gov.dwp.tax_credits.active), Income Support and income-based JSA until +# 31 March 2026 (gov.dwp.income_support.active, gov.dwp.JSA.income.active). +PAYING = { + 2024: set(EXISTING_BENEFITS), + 2025: set(EXISTING_BENEFITS) - set(TAX_CREDITS), + 2026: {"esa_income_reported", "housing_benefit_reported"}, +} +SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + +amount = st.floats(0.01, 20_000) + + +@st.composite +def sometimes(draw): + """A positive amount one time in three, otherwise zero.""" + return draw(amount) if draw(st.sampled_from((False, False, True))) else 0.0 + + +@st.composite +def member( + draw, + ages, + training, + educations=EDUCATIONS, + own_right=(False, True), + reg_5_5_reports=True, + one_existing_benefit=False, +): + """A person who usually reports no UC, ESA or JSA award under reg. 5(5). + + Reg. 5(5) excludes anyone with such a report, so drawing them rarely + leaves people for reg. 28 to exclude; with reg_5_5_reports false, the + person reports none. Each existing benefit report is positive one time in + three; with one_existing_benefit true, the person reports at most one, + and none one time in three. + """ + reg_5_5_report = ( + draw(st.sampled_from((None,) * 4 + REG_5_5_REPORTS)) + if reg_5_5_reports + else None + ) + return { + "age": draw(ages), + "current_education": draw(st.sampled_from(educations)), + "is_in_approved_training": draw(training), + "receives_benefits_in_own_right": draw( + st.sampled_from((False,) * 3 + own_right[1:]) + ), + **{ + name: draw(amount) if name == reg_5_5_report else 0.0 + for name in REG_5_5_REPORTS + }, + **existing_benefit_reports(draw, one_existing_benefit), + } + + +def existing_benefit_reports(draw, one_existing_benefit): + if not one_existing_benefit: + return {name: draw(sometimes()) for name in EXISTING_BENEFITS} + report = draw(st.sampled_from((None,) * 3 + EXISTING_BENEFITS)) + return {name: draw(amount) if name == report else 0.0 for name in EXISTING_BENEFITS} + + +young_member = member(st.integers(15, 20), st.booleans()) +# Claimants and partners aged 16 to 19 can be qualifying young people. +adult = member(st.one_of(st.integers(16, 19), st.integers(20, 70)), st.just(False)) +# Someone who meets reg. 5 unless reg. 28 excludes them: 16 to 18, on a +# non-advanced course, not flagged as receiving benefits in their own right +# and reporting no UC, ESA or JSA award under reg. 5(5). A 19-year-old needs +# the entry and terminal-date inputs (reg. 5(1)(b) and (2)), which these +# draws don't set. They report at most one existing benefit, so reg. 28 +# often leaves them in the test. +qualifying = member( + st.integers(16, 18), + st.just(False), + NON_ADVANCED, + own_right=(False,), + reg_5_5_reports=False, + one_existing_benefit=True, +) +INPUTS = ( + ( + "age", + "current_education", + "is_in_approved_training", + "receives_benefits_in_own_right", + "is_claimant_or_partner", + "is_parent", + ) + + REG_5_5_REPORTS + + EXISTING_BENEFITS +) + + +UNITS = 4 + + +@st.composite +def families(draw): + """A claimant, then a partner or a dependant, then two dependants.""" + couple = draw(st.booleans()) + claimant = dict( + draw(adult), + is_claimant_or_partner=True, + is_parent=True, + would_claim_uc=draw(st.booleans()), + ) + second = draw(adult if couple else young_member) + second = dict(second, is_claimant_or_partner=couple, is_parent=couple) + dependants = [ + dict(draw(young_member), is_claimant_or_partner=False, is_parent=False) + for _ in range(2) + ] + return [claimant, second] + dependants + + +@st.composite +def qualifying_families(draw): + """Families where reg. 28 alone decides most members' status. + + Each slot is drawn from qualifying, which meets reg. 5, or from the + general strategies. Each slot meets reg. 5 in about three draws in + five. A partner is otherwise an older adult whose + own reports reg. 28 must not pass to the claimant, except for the + couple's tax credit award. + """ + couple = draw(st.booleans()) + claimant = draw(st.one_of(qualifying, qualifying, adult)) + claimant = dict( + claimant, + is_claimant_or_partner=True, + is_parent=True, + would_claim_uc=draw(st.booleans()), + ) + second = draw(st.one_of(qualifying, qualifying, adult if couple else young_member)) + second = dict(second, is_claimant_or_partner=couple, is_parent=couple) + dependants = [ + dict( + draw(st.one_of(qualifying, qualifying, young_member)), + is_claimant_or_partner=False, + is_parent=False, + ) + for _ in range(2) + ] + return [claimant, second] + dependants + + +# Someone who meets reg. 5 unless reg. 28 excludes them and reports each +# existing benefit one time in three, so a report that closes their legacy +# awards often sits beside an income-related ESA or Housing Benefit report. +reporting_qualifying = member( + st.integers(16, 18), + st.just(False), + NON_ADVANCED, + own_right=(False,), + reg_5_5_reports=False, +) + + +@st.composite +def closure_families(draw): + """A claimant and a second member who meet reg. 5 one time in two, and + two dependants who always do, each reporting any existing benefit.""" + couple = draw(st.booleans()) + claimant = draw(st.one_of(reporting_qualifying, adult)) + claimant = dict( + claimant, + is_claimant_or_partner=True, + is_parent=True, + would_claim_uc=draw(st.booleans()), + ) + second = draw(st.one_of(reporting_qualifying, adult if couple else young_member)) + second = dict(second, is_claimant_or_partner=couple, is_parent=couple) + dependants = [ + dict(draw(reporting_qualifying), is_claimant_or_partner=False, is_parent=False) + for _ in range(2) + ] + return [claimant, second] + dependants + + +units_strategy = st.lists(families(), min_size=UNITS, max_size=UNITS) +closure_units_strategy = st.lists(closure_families(), min_size=UNITS, max_size=UNITS) +qualifying_units_strategy = st.lists( + qualifying_families(), min_size=UNITS, max_size=UNITS +) +_CACHE = {} + + +@pytest.fixture(autouse=True, scope="module") +def _release_simulations(): + """Drop the cloned system and cached simulations when the module ends.""" + yield + _CACHE.clear() + + +def simulate(units, year): + """Set the inputs on one cached simulation per shape and year. Each + family claims Universal Credit (would_claim_uc) as its first member's + draw says, or by default.""" + shape = (tuple(len(members) for members in units), year) + if shape not in _CACHE: + situation = {"people": {}, "benunits": {}, "households": {}} + for i, members in enumerate(units): + names = [f"p{i}_{j}" for j in range(len(members))] + for name in names: + situation["people"][name] = {} + situation["benunits"][f"b{i}"] = {"members": names} + situation["households"][f"h{i}"] = {"members": names} + if "system" not in _CACHE: + _CACHE["system"] = system.clone() + with patch( + "policyengine_uk.simulation.CountryTaxBenefitSystem", + side_effect=_CACHE["system"].clone, + ): + _CACHE[shape] = Simulation(situation=situation) + sim = _CACHE[shape] + people = flatten(units) + for variable in INPUTS: + sim.set_input(variable, year, [p[variable] for p in people]) + sim.set_input( + "would_claim_uc", + year, + [members[0].get("would_claim_uc", True) for members in units], + ) + sim.input_variables = list(INPUTS) + ["would_claim_uc"] + sim.reset_calculations() + return sim + + +def flatten(units): + return [p for members in units for p in members] + + +def claiming(units, claims): + """The same families, each with would_claim_uc set to claims.""" + return [ + [dict(members[0], would_claim_uc=claims)] + members[1:] for members in units + ] + + +def with_zero(units, names, who): + return [ + [dict(p, **{n: 0.0 for n in names}) if who(p) else p for p in members] + for members in units + ] + + +def predicate(sim, year): + return sim.calculate("is_qualifying_young_person_for_universal_credit", year).copy() + + +def reg_5_test(sim, year): + """UC Regs 2013 reg. 5 less the person's reported UC, ESA and JSA awards.""" + + def get(name): + return sim.calculate(name, year) + + routes = get("is_before_first_september_following_16th_birthday") | ( + get( + "meets_qualifying_young_person_education_or_training_condition_for_universal_credit" + ) + & get("meets_qualifying_young_person_entry_condition_for_universal_credit") + & get( + "meets_qualifying_young_person_terminal_date_condition_for_universal_credit" + ) + ) + excluded = get("receives_benefits_in_own_right") + for name in REG_5_5_REPORTS: + excluded = excluded | (get(name) > 0) + return ( + get("meets_qualifying_young_person_age_condition_for_universal_credit") + & routes + & ~excluded + ) + + +def open_awards(sim, year, claim_limbs=True): + """For each report, whether the model's legacy closure and, with + claim_limbs, the family's Universal Credit claim leave the award on it + open, person by person. The claim ends the claimant's and partner's own + income-related ESA, Housing Benefit outside the Pension Credit age + conditions and Child Tax Credit, and nobody else's.""" + own_open = ~sim.calculate("person_legacy_benefits_closed", year) + family_closed = sim.calculate("legacy_benefits_closed", year, map_to="person") + pension_age = sim.calculate( + "meets_pension_credit_age_conditions", year, map_to="person" + ) + claim_ends = np.zeros_like(own_open) + if claim_limbs: + claim_ends = sim.calculate( + "claims_universal_credit", year, map_to="person" + ) & sim.calculate("is_claimant_or_partner", year) + return { + "income_support_reported": own_open, + "jsa_income_reported": own_open, + "esa_income_reported": own_open & ~claim_ends, + "housing_benefit_reported": pension_age | (~family_closed & ~claim_ends), + "child_tax_credit_reported": ~family_closed & ~claim_ends, + "working_tax_credit_reported": ~family_closed, + } + + +def reg_28_exclusions(units, year, sim): + """Who reg. 28 excludes: own reports of benefits the model pays on them + that year, and, for a claimant or partner, the couple's tax credit + award.""" + paying = PAYING[year] + award_open = open_awards(sim, year) + people = flatten(units) + own = np.array( + [ + any(p[n] > 0 and award_open[n][i] for n in paying) + for i, p in enumerate(people) + ], + dtype=bool, + ) + couple_tax_credits = np.zeros(len(people), dtype=bool) + start = 0 + for members in units: + indices = range(start, start + len(members)) + award = any( + people[j]["is_claimant_or_partner"] + and people[j][n] > 0 + and award_open[n][j] + for j in indices + for n in paying.intersection(TAX_CREDITS) + ) + for j in indices: + couple_tax_credits[j] = people[j]["is_claimant_or_partner"] and award + start += len(members) + return own | couple_tax_credits + + +@SETTINGS +@given(units=units_strategy, year=st.sampled_from(YEARS)) +def test_any_own_existing_benefit_report_excludes_the_person(units, year): + sim = simulate(units, year) + assert not np.any(predicate(sim, year) & reg_28_exclusions(units, year, sim)) + + +@SETTINGS +@given(units=units_strategy, year=st.sampled_from(YEARS)) +def test_zero_existing_benefit_reports_leave_the_reg_5_test(units, year): + zeroed = with_zero(units, EXISTING_BENEFITS, lambda p: True) + sim = simulate(zeroed, year) + assert predicate(sim, year).tolist() == reg_5_test(sim, year).tolist() + assert ( + reg_5_test(sim, year).tolist() + == sim.calculate( + "is_qualifying_young_person_under_universal_credit_regulation_5", year + ).tolist() + ) + # With reports present, reg. 28 only removes people from the test. + sim = simulate(units, year) + assert not np.any(predicate(sim, year) & ~reg_5_test(sim, year)) + + +@SETTINGS +@given(units=units_strategy, year=st.sampled_from(YEARS)) +def test_a_claimants_reports_change_a_dependant_only_by_closing_the_family(units, year): + sim = simulate(units, year) + reported = predicate(sim, year) + family_closed = sim.calculate( + "legacy_benefits_closed", year, map_to="person" + ).copy() + cleared = predicate( + simulate( + with_zero(units, EXISTING_BENEFITS, lambda p: p["is_claimant_or_partner"]), + year, + ), + year, + ) + dependant = np.array([not p["is_claimant_or_partner"] for p in flatten(units)]) + # Where the couple's reports leave the family's legacy awards open, they + # never change a dependant's predicate. + kept = dependant & ~family_closed + assert reported[kept].tolist() == cleared[kept].tolist() + # Where they close them, they can only lift a dependant's exclusion. + closed = dependant & family_closed + assert not np.any(~reported[closed] & cleared[closed]) + + +@SETTINGS +@given(units=closure_units_strategy, year=st.sampled_from((2025, 2026))) +def test_a_closed_legacy_award_never_excludes(units, year): + # The model ends a person's own income-related ESA once their legacy + # awards close (person_legacy_benefits_closed, read by + # esa_income_report_closed), and the family's Housing Benefit once the + # claimant's and partner's close outside the Pension Credit age + # conditions (housing_benefit_eligible). Clearing those reports leaves + # every predicate as it was. Neither report enters legacy_award_is_closed. + # They reach the closures only through a mixed-age couple's Pension + # Credit saving, which clearing both reports of a closed family can't + # create. Tax credits close from 2025 and Income Support and income-based + # JSA from 2026. The claim limbs stay out: the claim can end an award in + # a family whose legacy awards stay open. + sim = simulate(units, year) + reported = predicate(sim, year) + award_open = open_awards(sim, year, claim_limbs=False) + closed = { + name: ~award_open[name] + for name in ("esa_income_reported", "housing_benefit_reported") + } + cleared_units, index = [], 0 + for members in units: + cleared_members = [] + for p in members: + cleared_members.append( + dict(p, **{name: 0.0 for name in closed if closed[name][index]}) + ) + index += 1 + cleared_units.append(cleared_members) + cleared = predicate(simulate(cleared_units, year), year) + assert reported.tolist() == cleared.tolist() + + +@SETTINGS +@given( + parent_age=st.integers(25, 60), + young_age=st.integers(16, 19), + education=st.sampled_from(EDUCATIONS), + benefit=st.sampled_from(EXISTING_BENEFITS), + report=st.floats(0.01, 20_000), + year=st.sampled_from(YEARS), +) +def test_a_young_persons_report_never_raises_the_parents_award( + parent_age, young_age, education, benefit, report, year +): + def family(young_report): + zeros = {n: 0.0 for n in REG_5_5_REPORTS + EXISTING_BENEFITS} + parent = dict( + zeros, + age=parent_age, + current_education="NOT_IN_EDUCATION", + is_in_approved_training=False, + receives_benefits_in_own_right=False, + is_claimant_or_partner=True, + is_parent=True, + ) + young = dict( + zeros, + age=young_age, + current_education=education, + is_in_approved_training=False, + receives_benefits_in_own_right=False, + is_claimant_or_partner=False, + is_parent=False, + ) + young[benefit] = young_report + return [[parent, young]] + + without = simulate(family(0.0), year).calculate("universal_credit", year)[0] + with_report = simulate(family(report), year).calculate("universal_credit", year)[0] + assert with_report <= without + 1e-6 + + +@SETTINGS +@given(units=qualifying_units_strategy, year=st.sampled_from(YEARS)) +def test_the_predicate_is_reg_5_less_reg_28(units, year): + sim = simulate(units, year) + expected = reg_5_test(sim, year) & ~reg_28_exclusions(units, year, sim) + assert predicate(sim, year).tolist() == expected.tolist() + + +@SETTINGS +@given(units=units_strategy, year=st.sampled_from(YEARS)) +def test_a_claim_changes_no_dependants_predicate(units, year): + # A claim for Universal Credit ends the claimant's and partner's own + # awards, not a dependant's (SI 2014/1230 reg. 8(2) and (2A)). + without_claim = predicate(simulate(claiming(units, False), year), year) + with_claim = predicate(simulate(claiming(units, True), year), year) + dependant = np.array([not p["is_claimant_or_partner"] for p in flatten(units)]) + assert with_claim[dependant].tolist() == without_claim[dependant].tolist() + + +@SETTINGS +@given(units=qualifying_units_strategy, year=st.sampled_from(YEARS)) +def test_a_claim_never_excludes_a_claimant_or_partner(units, year): + # A claim only ends awards, so it can only lift a claimant's or partner's + # exclusion. + without_claim = predicate(simulate(claiming(units, False), year), year) + with_claim = predicate(simulate(claiming(units, True), year), year) + member_of_couple = np.array([p["is_claimant_or_partner"] for p in flatten(units)]) + assert not np.any(without_claim[member_of_couple] & ~with_claim[member_of_couple]) diff --git a/policyengine_uk/tests/test_uc_qualifying_young_person_legacy_closure.py b/policyengine_uk/tests/test_uc_qualifying_young_person_legacy_closure.py new file mode 100644 index 0000000000..5a8c425f47 --- /dev/null +++ b/policyengine_uk/tests/test_uc_qualifying_young_person_legacy_closure.py @@ -0,0 +1,140 @@ +"""The UC qualifying young person test follows the family's tax credit closure. + +The model pays tax credits to the family, on every member's reports, and +ends them when the claimant's and partner's legacy awards close +(legacy_benefits_closed, read by is_CTC_eligible and is_WTC_eligible). +Nobody is entitled to an award the model has ended, so the predicate reads a +young person's own tax credit report only while the family's legacy awards +stay open (SI 2014/1230 reg. 28). + +In the baseline the family's legacy awards close only once a benefit has +closed, and tax credits close first (gov.dwp.tax_credits.active turns false +on 6 April 2025, Income Support on 1 April 2026), so the year gate already +covers every closed tax credit report. These tests keep the tax credit +scheme open in 2026 by a parameter reform (gov.dwp.tax_credits.active), so +the family's closure decides. + +The household: a parent aged 45 and a 17-year-old on a non-advanced course, +North East, owned outright, no income, in 2026. The parent claims Universal +Credit as a single claimant over 25: £424.90 a month (standard_allowance +SINGLE_OLD) and £351.88 a month for a qualifying young person (child element +first higher amount), so 12 x (424.90 + 351.88) = £9,321.36 with the child +element and 12 x 424.90 = £5,098.80 without it. +""" + +import pytest + +from policyengine_uk import Simulation + +YEAR = 2026 +TAX_CREDITS_PAYING = {"gov.dwp.tax_credits.active": {"2020-01-01.2100-12-31": True}} + + +def household(young_report, parent_report=None): + parent = { + "age": {YEAR: 45}, + "is_parent": {YEAR: True}, + "is_claimant_or_partner": {YEAR: True}, + } + if parent_report: + parent[parent_report] = {YEAR: 3_000} + return { + "people": { + "parent": parent, + "young_person": { + "age": {YEAR: 17}, + "current_education": {YEAR: "POST_SECONDARY"}, + "is_claimant_or_partner": {YEAR: False}, + young_report: {YEAR: 1_000}, + }, + }, + "benunits": {"benunit": {"members": ["parent", "young_person"]}}, + "households": { + "household": { + "members": ["parent", "young_person"], + "region": {YEAR: "NORTH_EAST"}, + "tenure_type": {YEAR: "OWNED_OUTRIGHT"}, + } + }, + } + + +def outcome(situation, reform): + sim = Simulation(situation=situation, reform=reform) + return ( + sim.calculate("legacy_benefits_closed", YEAR).tolist(), + sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR).tolist(), + round(float(sim.calculate("universal_credit", YEAR)[0]), 2), + ) + + +@pytest.mark.parametrize( + "report", ["child_tax_credit_reported", "working_tax_credit_reported"] +) +def test_a_young_persons_tax_credit_report_excludes_while_the_family_stays_open( + report, +): + # Tax credits pay in 2026 under the reform, and nothing closes the + # family's legacy awards, so the young person's own report excludes them. + assert outcome(household(report), TAX_CREDITS_PAYING) == ( + [False], + [False, False], + 5_098.80, + ) + + +@pytest.mark.parametrize( + "report", ["child_tax_credit_reported", "working_tax_credit_reported"] +) +def test_the_parents_closed_legacy_award_ends_the_young_persons_tax_credits( + report, +): + # The parent's own Income Support report in 2026, after Income Support + # closed, closes the family's legacy awards, which ends the family's tax + # credits, the young person's report included. + situation = household(report, "income_support_reported") + assert outcome(situation, TAX_CREDITS_PAYING) == ( + [True], + [False, True], + 9_321.36, + ) + + +def test_the_familys_closure_ends_the_couples_joint_tax_credit_award(): + # An 18-year-old claimant on a non-advanced course and a partner aged 45 + # with a child aged 1. The partner reports the couple's Working Tax Credit + # (Tax Credits Act 2002 s.3(3)) and their own Income Support in 2026, + # after Income Support closed, which closes the family's legacy awards + # and ends the couple's tax credits. So the couple's award doesn't + # exclude the claimant. Universal Credit can't show this: the claimant's + # status doesn't enter the award, so the test reads the predicate. + members = ["claimant", "partner", "child"] + situation = { + "people": { + "claimant": { + "age": {YEAR: 18}, + "current_education": {YEAR: "POST_SECONDARY"}, + "is_claimant_or_partner": {YEAR: True}, + }, + "partner": { + "age": {YEAR: 45}, + "is_claimant_or_partner": {YEAR: True}, + "income_support_reported": {YEAR: 3_000}, + "working_tax_credit_reported": {YEAR: 1_000}, + }, + "child": {"age": {YEAR: 1}, "is_claimant_or_partner": {YEAR: False}}, + }, + "benunits": {"benunit": {"members": members}}, + "households": { + "household": { + "members": members, + "region": {YEAR: "NORTH_EAST"}, + "tenure_type": {YEAR: "OWNED_OUTRIGHT"}, + } + }, + } + sim = Simulation(situation=situation, reform=TAX_CREDITS_PAYING) + assert sim.calculate("legacy_benefits_closed", YEAR).tolist() == [True] + assert sim.calculate( + "is_qualifying_young_person_for_universal_credit", YEAR + ).tolist() == [True, False, False] diff --git a/policyengine_uk/tests/test_uc_qualifying_young_person_neutralised_benefits.py b/policyengine_uk/tests/test_uc_qualifying_young_person_neutralised_benefits.py new file mode 100644 index 0000000000..041d6d3af3 --- /dev/null +++ b/policyengine_uk/tests/test_uc_qualifying_young_person_neutralised_benefits.py @@ -0,0 +1,174 @@ +"""The UC qualifying young person test under reforms that neutralise a benefit. + +The Secretary of State excludes a person entitled to an existing benefit +from the UC qualifying young person test (SI 2014/1230 regs. 2(1) and 28). +A reform that neutralises a benefit's variable abolishes that benefit, so +nobody can be entitled to it and reg. 28 can't exclude on it. The predicate +checks each report against its own variable: + +- Income Support: ``income_support``; +- income-based JSA: ``jsa_income``; +- income-related ESA: ``esa_income``; +- Housing Benefit: ``housing_benefit``; +- Child Tax Credit: ``child_tax_credit`` or ``tax_credits``; +- Working Tax Credit: ``working_tax_credit`` or ``tax_credits``. + +The model pays all six in 2024, so the tests read 2024 unless they name +another year. +""" + +import pytest +from policyengine_core.reforms import Reform + +from policyengine_uk import Simulation + +PREDICATE = "is_qualifying_young_person_for_universal_credit" +# Each report, and the variables whose neutralisation lifts its exclusion. +ROWS = { + "income_support_reported": {"income_support"}, + "jsa_income_reported": {"jsa_income"}, + "esa_income_reported": {"esa_income"}, + "housing_benefit_reported": {"housing_benefit"}, + "child_tax_credit_reported": {"child_tax_credit", "tax_credits"}, + "working_tax_credit_reported": {"working_tax_credit", "tax_credits"}, +} +NEUTRALISED = sorted(set().union(*ROWS.values())) + + +def neutralising(*names): + class neutralise(Reform): + def apply(self): + for name in names: + self.neutralize_variable(name) + + return neutralise + + +def young_people(reports, year, reform=None, amount=3_000): + """One household per report: a parent aged 45 and a 17-year-old on a + non-advanced course who reports that benefit (£3,000 a year unless the + test says otherwise). North East, owned outright, no income.""" + people, benunits, households = {}, {}, {} + for index, report in enumerate(reports): + parent, young = f"parent_{index}", f"young_{index}" + people[parent] = { + "age": {year: 45}, + "is_parent": {year: True}, + "is_claimant_or_partner": {year: True}, + } + people[young] = { + "age": {year: 17}, + "current_education": {year: "POST_SECONDARY"}, + "is_claimant_or_partner": {year: False}, + report: {year: amount}, + } + benunits[f"benunit_{index}"] = {"members": [parent, young]} + households[f"household_{index}"] = { + "members": [parent, young], + "region": {year: "NORTH_EAST"}, + "tenure_type": {year: "OWNED_OUTRIGHT"}, + } + return Simulation( + situation={"people": people, "benunits": benunits, "households": households}, + reform=reform, + ) + + +def young_flags(sim, year): + return sim.calculate(PREDICATE, year)[1::2].tolist() + + +def parent_awards(sim, year): + return [round(float(x), 2) for x in sim.calculate("universal_credit", year)] + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +@pytest.mark.parametrize("neutralised", [None] + NEUTRALISED) +def test_a_reform_lifts_only_the_exclusions_on_the_benefit_it_neutralises( + neutralised, +): + year = 2024 + reform = neutralising(neutralised) if neutralised else None + sim = young_people(list(ROWS), year, reform) + expected = [neutralised in variables for variables in ROWS.values()] + assert young_flags(sim, year) == expected + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +@pytest.mark.parametrize("neutralised", [None] + NEUTRALISED) +def test_a_reform_lifts_a_couples_tax_credit_exclusion_credit_by_credit( + neutralised, +): + """Two 18-year-olds on non-advanced courses form a couple, and the + partner reports the couple's Child Tax Credit or Working Tax Credit + award (Tax Credits Act 2002 s.3(3)(a)). The couple doesn't claim + Universal Credit, so no claim ends its Child Tax Credit.""" + year = 2024 + people, benunits, households = {}, {}, {} + credits = ("child_tax_credit_reported", "working_tax_credit_reported") + for index, report in enumerate(credits): + claimant, partner = f"claimant_{index}", f"partner_{index}" + for name in (claimant, partner): + people[name] = { + "age": {year: 18}, + "current_education": {year: "UPPER_SECONDARY"}, + "is_claimant_or_partner": {year: True}, + } + people[partner][report] = {year: 1_000} + benunits[f"benunit_{index}"] = { + "members": [claimant, partner], + "would_claim_uc": {year: False}, + } + households[f"household_{index}"] = {"members": [claimant, partner]} + sim = Simulation( + situation={"people": people, "benunits": benunits, "households": households}, + reform=neutralising(neutralised) if neutralised else None, + ) + lifted = [neutralised in ROWS[report] for report in credits] + expected = [flag for flag in lifted for _ in range(2)] + assert sim.calculate(PREDICATE, year).tolist() == expected + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +def test_neutralising_child_tax_credit_alone_leaves_a_working_tax_credit_exclusion(): + year = 2024 + credits = ["child_tax_credit_reported", "working_tax_credit_reported"] + alone = young_people(credits, year, neutralising("child_tax_credit")) + both = young_people(credits, year, neutralising("tax_credits")) + assert young_flags(alone, year) == [True, False] + assert young_flags(both, year) == [True, True] + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +def test_neutralising_housing_benefit_restores_an_own_housing_benefit_young_person(): + """Household G: the 17-year-old reports their own Housing Benefit in 2026.""" + year = 2026 + kept = young_people(["housing_benefit_reported"], year) + removed = young_people( + ["housing_benefit_reported"], year, neutralising("housing_benefit") + ) + assert young_flags(kept, year) == [False] + assert young_flags(removed, year) == [True] + assert parent_awards(kept, year)[0] == pytest.approx(5_098.80) + assert parent_awards(removed, year)[0] == pytest.approx(9_321.36) + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +def test_neutralising_child_tax_credit_restores_an_own_child_tax_credit_young_person(): + """Household D: the 17-year-old reports their own Child Tax Credit in + 2024, £1,000.""" + year = 2024 + sims = {} + for name, reform in ( + ("kept", None), + ("child_tax_credit", neutralising("child_tax_credit")), + ("tax_credits", neutralising("tax_credits")), + ): + sims[name] = young_people( + ["child_tax_credit_reported"], year, reform, amount=1_000 + ) + assert young_flags(sims["kept"], year) == [False] + assert parent_awards(sims["kept"], year)[0] == pytest.approx(4_721.40) + for name in ("child_tax_credit", "tax_credits"): + assert young_flags(sims[name], year) == [True] + assert parent_awards(sims[name], year)[0] == pytest.approx(8_721.36) diff --git a/policyengine_uk/tests/test_uc_receiving_education_properties.py b/policyengine_uk/tests/test_uc_receiving_education_properties.py index 44c304d9d7..a211977060 100644 --- a/policyengine_uk/tests/test_uc_receiving_education_properties.py +++ b/policyengine_uk/tests/test_uc_receiving_education_properties.py @@ -101,16 +101,34 @@ YEARS = [2019, 2021, 2024, 2026] REGIONS = ["NORTH_EAST", "LONDON", "SCOTLAND", "NORTHERN_IRELAND"] EDUCATION = ["TERTIARY", "POST_SECONDARY", "NOT_IN_EDUCATION"] -# SI 2014/1230 reg. 2(1), as reported receipt. +TAX_CREDITS = ["child_tax_credit_reported", "working_tax_credit_reported"] +# SI 2014/1230 reg. 2(1), as reported receipt, for the migration day: the +# report stands for the award the closure claim ended. EXISTING_BENEFITS = [ "jsa_income_reported", "esa_income_reported", "income_support_reported", "housing_benefit_reported", - "child_tax_credit_reported", - "working_tax_credit_reported", + *TAX_CREDITS, ] -TAX_CREDITS = ["child_tax_credit_reported", "working_tax_credit_reported"] +# Reg. 28 turns on entitlement, +# so a report counts only in a year the model pays that benefit (the +# parameter, or None where the model has no closure date) and only if no +# reform neutralises one of its variables. +REPORT_BENEFITS = { + "jsa_income_reported": ("JSA.income.active", ["jsa_income"]), + "esa_income_reported": (None, ["esa_income"]), + "income_support_reported": ("income_support.active", ["income_support"]), + "housing_benefit_reported": (None, ["housing_benefit"]), + "child_tax_credit_reported": ( + "tax_credits.active", + ["child_tax_credit", "tax_credits"], + ), + "working_tax_credit_reported": ( + "tax_credits.active", + ["working_tax_credit", "tax_credits"], + ), +} # Compare eligibility, maximum entitlement, income reduction and Universal # Credit awards. OUTPUTS = [ @@ -190,6 +208,7 @@ def adult(draw, ages): uc_is_responsible_foster_parent_of_child_under_one=draw(rarely), uc_is_foster_parent_or_new_friend_or_family_carer=draw(rarely), housing_benefit_reported=draw(st.sampled_from([0, 0, 0, 0, 6_000])), + housing_benefit_report_is_rate_rebate_only=draw(rarely), working_tax_credit_reported=draw(st.sampled_from([0, 0, 0, 0, 1_000])), employment_income=draw(st.sampled_from([0, 0, 4_000, 15_000])), is_claimant_or_partner=True, @@ -284,7 +303,12 @@ def all_flags(case): "meets_pension_credit_age_conditions", "uc_assessable_capital", "is_qualifying_young_person_for_universal_credit", + "is_qualifying_young_person_under_universal_credit_regulation_5", "is_qualifying_young_person_for_uc_education", + "is_claimant_or_partner", + "person_legacy_benefits_closed", + "legacy_benefits_closed", + "claims_universal_credit", "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", "has_attained_state_pension_credit_qualifying_age", "is_mixed_age_couple", @@ -302,6 +326,86 @@ def all_flags(case): BEFORE = (None, True) +def paying_reports(simulation, year): + """The existing benefit reports reg. 28 reads in the year: those of + benefits the model pays then and no reform neutralises.""" + system = simulation.tax_benefit_system + dwp = system.parameters(f"{year}-01-01").gov.dwp + paying = [] + for report, (active, variables) in REPORT_BENEFITS.items(): + node = dwp + for key in active.split(".") if active else []: + node = getattr(node, key) + if (active is None or bool(node)) and not any( + system.get_variable(v).is_neutralized for v in variables + ): + paying.append(report) + return paying + + +# Reports whose award the model ends on the person's own legacy closure +# (person_legacy_benefits_closed); it ends the family's Housing Benefit, +# outside the Pension Credit age conditions, and tax credits on the family's +# (legacy_benefits_closed). +OWN_CLOSURE = ["jsa_income_reported", "esa_income_reported", "income_support_reported"] + + +def excluded_on_first_day(case, result): + """Per person, whether reg. 28 keeps them out of the definition for the + condition: an own report, or for a claimant or partner the couple's tax + credit report, that the model pays that year, no reform neutralises and + the model's legacy closure leaves open, on the first day of the + Universal Credit award. That day a Housing Benefit or income-related ESA + award the claim ends still runs (SI 2014/1230 reg. 8(2A)), but the claim + has already ended a claimant's or partner's Child Tax Credit (reg. + 8(2)(a)) and, in Northern Ireland, a report of a rate rebate alone (SR + 2016/226 reg. 6(2); housing_benefit_report_is_rate_rebate_only).""" + flags = [] + index = 0 + for f, fam in enumerate(case["families"]): + members = range(index, index + len(fam["people"])) + family_closed = bool(result["legacy_benefits_closed"][f]) + pension_age = bool(result["meets_pension_credit_age_conditions"][f]) + claims = bool(result["claims_universal_credit"][f]) + + def is_open(i, name): + if fam["people"][i - index].get(name, 0) <= 0: + return False + if name not in result["paying"]: + return False + if name in OWN_CLOSURE: + return not result["person_legacy_benefits_closed"][i] + if name == "housing_benefit_reported": + rate_rebate_ended = ( + fam["household"]["region"] == "NORTHERN_IRELAND" + and fam["people"][i - index].get( + "housing_benefit_report_is_rate_rebate_only", False + ) + and claims + and result["is_claimant_or_partner"][i] + ) + return pension_age or not (family_closed or rate_rebate_ended) + if family_closed: + return False + return not ( + name == "child_tax_credit_reported" + and claims + and result["is_claimant_or_partner"][i] + ) + + couple = [i for i in members if result["is_uc_assessed_claimant"][i]] + couple_tax_credits = any( + is_open(j, name) for j in couple for name in TAX_CREDITS + ) + for i in members: + flags.append( + any(is_open(i, name) for name in EXISTING_BENEFITS) + or (i in couple and couple_tax_credits) + ) + index += len(fam["people"]) + return np.array(flags, dtype=bool) + + def run(case, variants=(DRAWN,)): """One simulation of every variant; returns one result per variant.""" simulation = Simulation(situation=situation(case, variants)) @@ -314,6 +418,7 @@ def run(case, variants=(DRAWN,)): np.array([uses_main for _, uses_main in variants for _ in case["families"]]), ) values = {name: simulation.calculate(name, year) for name in VARIABLES} + paying = paying_reports(simulation, year) limit = simulation.tax_benefit_system.parameters( f"{year}-01-01" ).gov.dwp.universal_credit.means_test.capital.limit @@ -324,7 +429,7 @@ def run(case, variants=(DRAWN,)): } results = [] for v in range(len(variants)): - result = {"capital_limit": limit} + result = {"capital_limit": limit, "paying": paying} for name, array in values.items(): entity = simulation.tax_benefit_system.variables[name].entity.key size = sizes[entity] @@ -341,10 +446,12 @@ def reference(case, result): person for the condition, and per family, eligibility with the education condition and without it. Reads from the model only the roles (who is a claimant), the age conditions, qualifying young person status before reg. - 28, the Pension Credit route, capital, responsibility for a child, limited + 28 (the reg. 5 variable), the legacy closures, the family's claim, the + Pension Credit route, capital, responsibility for a child, limited capability for work and the disability benefit amounts, which other rules settle. """ + excluded = excluded_on_first_day(case, result) receiving, excepted, qyp_for_education = [], [], [] eligible, eligible_before = [], [] index = 0 @@ -353,17 +460,27 @@ def reference(case, result): members = range(start, start + len(fam["people"])) person_of = dict(zip(members, fam["people"])) couple = [i for i in members if result["is_uc_assessed_claimant"][i]] - couple_tax_credits = any( + couple_tax_credits_on_migration_day = any( person_of[j].get(name, 0) > 0 for j in couple for name in TAX_CREDITS ) - qyp, recv = {}, {} + qyp, qyp_on_migration_day, recv = {}, {}, {} for i, person in person_of.items(): - existing_benefit = any( - person.get(name, 0) > 0 for name in EXISTING_BENEFITS - ) or (i in couple and couple_tax_credits) qyp[i] = ( + bool( + result[ + "is_qualifying_young_person_under_universal_credit_regulation_5" + ][i] + ) + and not excluded[i] + ) + # Reg. 28 on the migration day, when the award the closure claim + # ended still existed. + existing_benefit_on_migration_day = any( + person.get(name, 0) > 0 for name in EXISTING_BENEFITS + ) or (i in couple and couple_tax_credits_on_migration_day) + qyp_on_migration_day[i] = ( bool(result["is_qualifying_young_person_for_universal_credit"][i]) - and not existing_benefit + and not existing_benefit_on_migration_day ) advanced = person["current_education"] == "TERTIARY" or person.get( "in_HE", False @@ -410,7 +527,7 @@ def reference(case, result): reg_60 = ( in_couple_role and fam["benunit"]["claims_uc_at_legacy_closure"] - and not qyp[i] + and not qyp_on_migration_day[i] ) exc[i] = limb_b or limb_c or limb_d or limb_e or limb_f or reg_60 claimants = [i for i in members if result["is_uc_single_or_joint_claimant"][i]] @@ -663,16 +780,97 @@ def no_loans_or_grants(i, values): atol=0.005, err_msg=f"{name}: {context}", ) - # 8. Reg. 28: no one who reports an existing benefit counts as a - # qualifying young person for the condition. - reports = np.array( - [ - any(p.get(name, 0) > 0 for name in EXISTING_BENEFITS) - for fam in case["families"] - for p in fam["people"] - ] - ) - assert not np.any(drawn["is_qualifying_young_person_for_uc_education"][reports]) + # 8. Reg. 28: no one whose existing benefit report the model pays that + # year, no reform neutralises and the legacy closure leaves open, on the + # first day of the Universal Credit award, counts as a qualifying young + # person for the condition. + excluded = excluded_on_first_day(case, drawn) + assert not np.any(drawn["is_qualifying_young_person_for_uc_education"][excluded]) + + +# An 18-year-old on a non-advanced full-time course who claims Universal +# Credit: in 2026, after the family's legacy awards closed on their tax +# credit report, which ended their Housing Benefit; and in 2024, reporting +# their own Child Tax Credit, which the claim ends the day before the award, +# with no child to except them. In Northern Ireland in 2026, one owner reports +# a rate rebate alone, which the claim ends the day before the award (SR +# 2016/226 reg. 6(2)). Another owner and a tenant report a rent rebate or +# allowance, which runs on (reg. 6(2A)): an owner can get one on site or +# mooring payments (SR 2006/405 reg. 13(1)(f) and (g)). +CLAIM_EXAMPLES = [ + dict( + year=2026, + families=[ + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + housing_benefit_reported=6_000, + working_tax_credit_reported=1_000, + ) + ] + ) + ], + ), + dict( + year=2024, + families=[ + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + child_tax_credit_reported=2_000, + ) + ] + ) + ], + ), + dict( + year=2026, + families=[ + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + housing_benefit_reported=6_000, + housing_benefit_report_is_rate_rebate_only=True, + ) + ], + region="NORTHERN_IRELAND", + ), + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + housing_benefit_reported=6_000, + ) + ], + region="NORTHERN_IRELAND", + ), + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + housing_benefit_reported=6_000, + ) + ], + region="NORTHERN_IRELAND", + tenure_type="RENT_FROM_COUNCIL", + rent=6_000, + ), + ], + ), +] @PROPERTY_SETTINGS @@ -680,6 +878,9 @@ def no_loans_or_grants(i, values): @example(EXAMPLES[1]) @example(EXAMPLES[2]) @example(EXAMPLES[3]) +@example(CLAIM_EXAMPLES[0]) +@example(CLAIM_EXAMPLES[1]) +@example(CLAIM_EXAMPLES[2]) @given(cases()) def test_education_condition_invariants(case): check(case) @@ -708,7 +909,12 @@ def test_examples_reach_the_cases(): drawn["uc_is_excepted_from_education_condition"][1:5], [True, False, True, False], ) - # People 9 and 10: the young Housing Benefit claimants. + # People 9 and 10: the young Housing Benefit claimants. Each claims + # Universal Credit, which ends their own Housing Benefit two weeks after + # the award starts (SI 2014/1230 reg. 8(2A)). The UC predicate follows + # that claim limb and reads reg. 5(5) only from reported Universal + # Credit, so it gives true; the condition reads the status on the first + # day of the award, when the Housing Benefit still runs. young = slice(9, 11) np.testing.assert_array_equal( drawn["is_qualifying_young_person_for_universal_credit"][young], [True, True] diff --git a/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py b/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py index 6c02b921d2..80cbc3dec7 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py @@ -21,7 +21,8 @@ class esa_income_report_closed(Variable): "in Northern Ireland, Welfare Reform (Northern Ireland) Order 2015 " "art. 39(1)(b) and SR 2017/190 art. 6(1) to (3) and (6)). For claims " "from 22 July 2020 the award runs on for two weeks after that day (SI " - "2019/1152 reg. 5), which this annual model does not represent. A " + "2019/1152 reg. 5; in Northern Ireland, SR 2019/152 reg. 4), which this " + "annual model does not represent. A " "contributory allowance in the award continues as new style ESA " "(esa_contrib). A member who is neither the claimant nor the partner, " "such as a non-dependent adult, claims in their own right and is not " @@ -48,6 +49,7 @@ class esa_income_report_closed(Variable): "https://www.legislation.gov.uk/uksi/2025/1148/article/3A", "https://www.legislation.gov.uk/nisi/2015/2006/article/39", "https://www.legislation.gov.uk/nisr/2017/190/article/6", + "https://www.legislation.gov.uk/nisr/2019/152/regulation/4", "https://www.legislation.gov.uk/nisr/2025/176/article/3A", ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_report_is_rate_rebate_only.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_report_is_rate_rebate_only.py new file mode 100644 index 0000000000..ccb2eb3350 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_report_is_rate_rebate_only.py @@ -0,0 +1,35 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_report_is_rate_rebate_only(Variable): + value_type = bool + entity = Person + label = "Housing Benefit report covers only a Northern Ireland rate rebate" + documentation = ( + "Whether this person's housing_benefit_reported covers only a rate " + "rebate on the home they occupy in Northern Ireland, with no rent " + "rebate or allowance. The Housing Benefit Regulations (Northern " + "Ireland) 2006 (SR 2006/405) and their twin for people at the " + "qualifying age for State Pension Credit (SR 2006/406) pay a rate " + "rebate on rates (reg. 12(1)) and a rent rebate or allowance on rent " + "and similar payments (reg. 13(1)), including mooring charges for a " + "houseboat and site payments for a caravan or mobile home (reg. " + "13(1)(f) and (g)). So tenure doesn't settle the form: an owner can " + "get a rent allowance. A Universal Credit claim ends a rate rebate the " + "day before the award starts (SR 2016/226 reg. 6(2)) and runs a rent " + "rebate or allowance on for two weeks (reg. 6(2A)). Only the first-day " + "test that is_qualifying_young_person_for_uc_education reads uses this " + "status, and only in Northern Ireland. A person with both forms leaves " + "it false, because their rent rebate or allowance runs on. It has no " + "formula and defaults to False, so the model reads a report as a rent " + "rebate or allowance unless this input says otherwise." + ) + definition_period = YEAR + default_value = False + reference = ( + "https://www.legislation.gov.uk/nisr/2006/405/regulation/12", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/13", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/12", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/13", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/6", + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py index 4281403300..b770f9000e 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py @@ -6,55 +6,249 @@ class is_qualifying_young_person_for_universal_credit(Variable): entity = Person label = "Qualifying young person for Universal Credit" documentation = ( - "A person aged 16 to 19 who is either before the 1 September following " - "their 16th birthday, whatever their education (reg. 5(1)(a)), or in " - "non-advanced education or approved training that meets the entry and " - "terminal-date conditions (reg. 5(1)(b), (2) and (3)), and who is not " - "receiving Universal Credit, Employment and Support Allowance or " - "Jobseeker's Allowance (reg. 5(5))." + "A qualifying young person under UC Regs 2013 reg. 5 " + "(is_qualifying_young_person_under_universal_credit_regulation_5): a " + "person aged 16 to 19 who meets reg. 5(1)(a) or (b) and receives no " + "Universal Credit, contributory ESA or contribution-based JSA (reg. " + "5(5)) and no existing benefit: income-based JSA, income-related ESA, " + "Income Support, Housing Benefit, Child Tax Credit or Working Tax " + "Credit (SI 2014/1230 regs. 2(1) and 28; SR 2016/226 regs. 2(1) and " + "29). The Secretary of State ties the exclusion to entitlement (reg. " + "28), so the formula reads a report only in a year the model pays that " + "benefit, only until the model's legacy closure, or for a claimant or " + "partner the family's Universal Credit claim, ends the award on it, and" + " only if no reform neutralises it. It doesn't apply the model's other " + "conditions of payment, so a report can exclude a person even where the" + " model pays nothing on that report. For example, the model pays Income" + " Support only on the claimant's and partner's reports, Housing Benefit" + " only to a family that rents, and income-based JSA and income-related " + "ESA only after their capital and work screens. The model pays Income " + "Support, income-based JSA and tax credits only while " + "gov.dwp.income_support.active, gov.dwp.JSA.income.active and " + "gov.dwp.tax_credits.active hold, has no closure date for Housing " + "Benefit, and pays an income-related ESA claim only while it stays " + "payable (esa_income_payable_share), so the formula reads its report " + "only then. The formula also reads the Income Support, income-based " + "JSA and tax credit reports while " + "gov.contrib.policyengine.disable_simulated_benefits is on. The reform " + "that switch turns on pays them at their reported amounts in the " + "dataset year and the nine years after it, and the formula still reads " + "them in later years. In those years the reform also pays a claimant's " + "or partner's own income-related ESA, Housing Benefit and Child Tax " + "Credit report whether or not the family claims Universal Credit, so " + "while the switch is on the formula leaves out the Universal Credit " + "claim limbs below. It leaves them out in later years too, when the " + "model again ends those awards on the family's claim. The model also ends" + " a person's legacy awards once its annual migration proxy closes them:" + " it ends their Income Support, income-based JSA and income-related ESA" + " when person_legacy_benefits_closed holds, the family's tax credits " + "when legacy_benefits_closed holds, and its Housing Benefit when " + "legacy_benefits_closed holds and the family doesn't meet the Pension " + "Credit age conditions. The formula follows both closures. A claim for " + "Universal Credit ends the claimant's and partner's own awards (SI " + "2014/1230 reg. 8(2) and (2A); SI 2013/983 art. 4; in Northern Ireland, " + "SR 2016/226 reg. 6(2) and (2A) and SR 2017/190 art. 6), and the model " + "ends the family's continuing Housing Benefit award " + "(housing_benefit_eligible) and its Child Tax Credit (is_CTC_eligible)," + " and the claimant's and partner's income-related ESA " + "(esa_income_report_closed), when the family claims " + "(claims_universal_credit). For a claimant or partner, the formula " + "follows those limbs too. The family's claim doesn't end a dependant's " + "own award, so for a dependant the formula doesn't. The model ends no " + "Working Tax Credit, Income Support or income-based JSA award on the " + "claim, so the formula follows no claim limb for them. It can't read " + "housing_benefit_eligible: that variable's Pension Credit route reads " + "left_pension_route_at_legacy_closure, which reads this variable " + "through is_uc_entitled and the child element. So the formula keeps a " + "Housing Benefit report while the family meets the Pension Credit age " + "conditions. It also keeps the report while the family lives in " + "specified or temporary accommodation " + "(in_specified_or_temporary_accommodation), since neither the claim nor " + "the closure ends that award (SI 2014/1230 regs. 8(3) and 46(2); SR " + "2016/226 regs. 6(3) and 47(2)). The formula checks each report against " + "its own variable: " + "income_support, jsa_income, esa_income, housing_benefit, " + "child_tax_credit or tax_credits for Child Tax Credit, and " + "working_tax_credit or tax_credits for Working Tax Credit. A reform " + "that changes or zeroes a benefit's formula without neutralising its " + "variable leaves the exclusion on that benefit's reports in place. For " + "a claimant or partner it also reads the couple's joint tax credit " + "award (Tax Credits Act 2002 s.3(3)), each credit by its own check." ) definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/5" + reference = [ + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/28", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/2", + "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/2013/983/article/4", + "https://www.legislation.gov.uk/uksi/2019/1152/regulation/5", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/29", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/2", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/6", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/47", + "https://www.legislation.gov.uk/nisr/2017/190/article/6", + "https://www.legislation.gov.uk/nisr/2019/152/regulation/4", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/12", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/13", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/12", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/13", + "https://www.legislation.gov.uk/ukpga/2002/21/section/3", + ] def formula(person, period, parameters): - # Reg. 5(1)(a): no education or training condition. - before_first_september_following_16th_birthday = person( - "is_before_first_september_following_16th_birthday", period - ) - # Reg. 5(1)(b), (2) and (3). - in_education_or_training = ( - person( - "meets_qualifying_young_person_education_or_training_condition_for_universal_credit", - period, - ) - & person( - "meets_qualifying_young_person_entry_condition_for_universal_credit", - period, - ) - & person( - "meets_qualifying_young_person_terminal_date_condition_for_universal_credit", - period, - ) + return qualifying_young_person(person, period, parameters) + + +def qualifying_young_person(person, period, parameters, at_claim_start=False): + """The UC qualifying young person test with SI 2014/1230 reg. 28 + applied, as is_qualifying_young_person_for_universal_credit documents + it. With at_claim_start it gives a claimant's or partner's status on the + first day of their Universal Credit award + (is_qualifying_young_person_for_uc_education reads it). A Housing + Benefit or income-related ESA award the claim ends still runs that day + (reg. 8(2A); esa_income_report_closed's run-on), so the formula leaves + out those two claim limbs. The claim has already ended a tax credit + award (reg. 8(2)(a)), so the formula keeps the Child Tax Credit limb. + In Northern Ireland the claim has also already ended a rate rebate (SR + 2016/226 reg. 6(2)), and only a rent rebate or allowance runs on (reg. + 6(2A)), so there the formula keeps the Housing Benefit limb for a report + of a rate rebate alone (housing_benefit_report_is_rate_rebate_only). The + year gates and the legacy closures apply either way.""" + # The Secretary of State excludes a person entitled to an existing + # benefit (TP regs. 2(1) and 28). The formula reads raw person-level + # reports, the model's legacy closure proxy and the family's Universal + # Credit claim, so it calculates no family benefit that depends on QYP + # status. + p = parameters(period).gov + dwp = p.dwp + # The reform pays each listed benefit at its dataset-year report in + # that year and the nine after it, closure dates included, and holds + # both legacy closures false in those years. The formula reads the + # switch, not those years, so in a later year it still reads a tax + # credit report that the model no longer pays, and still leaves out + # the claim limbs below. + paid_as_reported = bool(p.contrib.policyengine.disable_simulated_benefits) + system = person.simulation.tax_benefit_system + + def paying(active, *names): + # Nobody is entitled to a benefit the model no longer pays, or one + # a reform neutralises. + return bool(active or paid_as_reported) and not any( + system.get_variable(name).is_neutralized for name in names ) - # The exclusions apply to both routes (UC reg. 5(5); SPC reg. 4A(5)). - # Raw person-level reports identify own awards without calculating - # family benefits, which depend on QYP status and could recurse. - receives_own_benefits = ( - person("receives_benefits_in_own_right", period) - | (person("universal_credit_reported", period) > 0) - | (person("esa_income_reported", period) > 0) - | (person("esa_contrib_reported", period) > 0) - | (person("jsa_income_reported", period) > 0) - | (person("jsa_contrib_reported", period) > 0) + + # The model ends a person's Income Support, income-based JSA and + # income-related ESA when their legacy awards close: the claimant's + # and partner's together (legacy_benefits_closed, read through + # income_support), and anyone else's on their own reports + # (jsa_income, esa_income). person_legacy_benefits_closed and + # legacy_benefits_closed read reports, is_claimant_or_partner, the + # Pension Credit age conditions and parameters, and no Universal + # Credit variable. + own_award_open = ~person("person_legacy_benefits_closed", period) + # A claim for Universal Credit ends the claimant's and partner's own + # awards (SI 2014/1230 reg. 8(2) and (2A); SI 2013/983 art. 4; in + # Northern Ireland, SR 2016/226 reg. 6(2) and (2A) and SR 2017/190 art. + # 6), and nobody else's. The model ends the claimant's and partner's + # income-related ESA when the family claims (esa_income_report_closed), + # and the formula follows it. claims_universal_credit reads + # would_claim_uc, would_claim_uc_at_legacy_closure and + # legacy_benefits_closed, not this variable. In its years the reform + # behind disable_simulated_benefits pays each report as reported + # whether or not the family claims, and holds the legacy closures + # false, so while the switch is on the formula keeps the person's own + # closure alone. + claimant_or_partner = person("is_claimant_or_partner", period) + # The Secretary of State (SI 2025/1148 art. 3A) and, in Northern + # Ireland, the Department for Communities (SR 2025/176 art. 3A) end old + # style income-related ESA claims on the appointed date unless an + # appointee saving keeps them (esa_income_payable_share). + esa_income_open = ( + ~person("esa_income_report_closed", period) + | (own_award_open & (paid_as_reported or at_claim_start)) + ) & ((person("esa_income_payable_share", period) > 0) | paid_as_reported) + claim_ends_own_award = ( + claimant_or_partner + & person.benunit("claims_universal_credit", period) + & (not paid_as_reported) + ) + # On the first day of the award a Housing Benefit award the claim ends + # still runs (reg. 8(2A)). In Northern Ireland only a rent rebate or + # allowance still runs (SR 2016/226 reg. 6(2A)): the claim has already + # ended a rate rebate (reg. 6(2)). The report doesn't show the form, so + # the formula reads housing_benefit_report_is_rate_rebate_only. + if at_claim_start: + country = person.household("country", period) + claim_has_ended_housing_benefit = ( + claim_ends_own_award + & (country == country.possible_values.NORTHERN_IRELAND) + & person("housing_benefit_report_is_rate_rebate_only", period) ) - return ( - person( - "meets_qualifying_young_person_age_condition_for_universal_credit", - period, - ) - & ( - before_first_september_following_16th_birthday - | in_education_or_training - ) - & ~receives_own_benefits + else: + claim_has_ended_housing_benefit = claim_ends_own_award + # The model pays Housing Benefit and tax credits to the family, on + # every member's reports, and ends them when the claimant's and + # partner's legacy awards close (housing_benefit_eligible, + # is_CTC_eligible, is_WTC_eligible). It also ends the family's + # continuing Housing Benefit award and its Child Tax Credit when the + # family claims Universal Credit, and the formula follows that limb + # for a claimant or partner, whose own award the claim ends. A family + # that meets the Pension Credit age conditions keeps Housing Benefit + # unless it claims Universal Credit at the closure and is entitled to + # it (left_pension_route_at_legacy_closure). That variable reads this + # one through is_uc_entitled and the child element, so the formula + # keeps the Housing Benefit report for such a family. Neither the claim nor + # the closure ends an award for specified or temporary accommodation (SI + # 2014/1230 regs. 8(3) and 46(2); SR 2016/226 regs. 6(3) and 47(2)), so the + # formula keeps that report too, as housing_benefit_eligible keeps the award. + family_closed = person.benunit("legacy_benefits_closed", period) + family_award_open = ~family_closed + housing_benefit_open = ( + person.benunit("meets_pension_credit_age_conditions", period) + | person.benunit("in_specified_or_temporary_accommodation", period) + | (~family_closed & ~claim_has_ended_housing_benefit) + ) + + income_support_paying = ( + paying(dwp.income_support.active, "income_support") & own_award_open + ) + jsa_income_paying = paying(dwp.JSA.income.active, "jsa_income") & own_award_open + esa_income_paying = paying(True, "esa_income") & esa_income_open + housing_benefit_paying = paying(True, "housing_benefit") & housing_benefit_open + child_tax_credit_paying = ( + paying(dwp.tax_credits.active, "child_tax_credit", "tax_credits") + & family_award_open + & ~claim_ends_own_award + ) + working_tax_credit_paying = ( + paying(dwp.tax_credits.active, "working_tax_credit", "tax_credits") + & family_award_open + ) + tax_credits_reported = child_tax_credit_paying * person( + "child_tax_credit_reported", period + ) + working_tax_credit_paying * person("working_tax_credit_reported", period) + # Tax Credits Act 2002 s.3(3): the members of a couple claim jointly, + # so the couple's award belongs to the claimant and the partner. A + # dependant claims as a single person, so their award stays theirs. + couple_tax_credits_reported = person.benunit.sum( + where(claimant_or_partner, tax_credits_reported, 0) + ) + receives_tax_credits = (tax_credits_reported > 0) | ( + claimant_or_partner & (couple_tax_credits_reported > 0) + ) + entitled_to_existing_benefit = ( + (esa_income_paying & (person("esa_income_reported", period) > 0)) + | (jsa_income_paying & (person("jsa_income_reported", period) > 0)) + | (income_support_paying & (person("income_support_reported", period) > 0)) + | (housing_benefit_paying & (person("housing_benefit_reported", period) > 0)) + | receives_tax_credits + ) + return ( + person( + "is_qualifying_young_person_under_universal_credit_regulation_5", + period, ) + & ~entitled_to_existing_benefit + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_under_universal_credit_regulation_5.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_under_universal_credit_regulation_5.py new file mode 100644 index 0000000000..882f7cf52a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_under_universal_credit_regulation_5.py @@ -0,0 +1,71 @@ +from policyengine_uk.model_api import * + + +class is_qualifying_young_person_under_universal_credit_regulation_5(Variable): + value_type = bool + entity = Person + label = "Qualifying young person under Universal Credit Regulations reg. 5" + documentation = ( + "A person aged 16 to 19 who is either before the 1 September following " + "their 16th birthday, whatever their education (reg. 5(1)(a)), or in " + "non-advanced education or approved training that meets the entry and " + "terminal-date conditions (reg. 5(1)(b), (2) and (3)), and who receives " + "no Universal Credit, contributory ESA or contribution-based JSA (reg. " + "5(5)). The Secretary of State defines employment and support " + "allowance and jobseeker's allowance without their income-related and " + "income-based forms (UC Regs 2013 reg. 2), so this test leaves those to " + "SI 2014/1230 reg. 28, which is_qualifying_young_person_for_universal_credit " + "applies. Northern Ireland's twin is reg. 6 of the Universal Credit " + "Regulations (Northern Ireland) 2016 (SR 2016/216). The Scottish " + "Ministers define the young person for Scotland's working-age Council " + "Tax Reduction by this regulation (SSI 2021/249 reg. 6(1))." + ) + definition_period = YEAR + reference = [ + "https://www.legislation.gov.uk/uksi/2013/376/regulation/2", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/nisr/2016/216/regulation/6", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/6", + ] + + def formula(person, period, parameters): + # Reg. 5(1)(a): no education or training condition. + before_first_september_following_16th_birthday = person( + "is_before_first_september_following_16th_birthday", period + ) + # Reg. 5(1)(b), (2) and (3). + in_education_or_training = ( + person( + "meets_qualifying_young_person_education_or_training_condition_for_universal_credit", + period, + ) + & person( + "meets_qualifying_young_person_entry_condition_for_universal_credit", + period, + ) + & person( + "meets_qualifying_young_person_terminal_date_condition_for_universal_credit", + period, + ) + ) + # The Secretary of State excludes a person receiving Universal Credit, + # contributory ESA or contribution-based JSA (reg. 5(5)), on both + # routes. The formula reads raw person-level reports, so it calculates + # no family benefit that depends on qualifying young person status. + receives_reg_5_5_benefit = ( + person("receives_benefits_in_own_right", period) + | (person("universal_credit_reported", period) > 0) + | (person("esa_contrib_reported", period) > 0) + | (person("jsa_contrib_reported", period) > 0) + ) + return ( + person( + "meets_qualifying_young_person_age_condition_for_universal_credit", + period, + ) + & ( + before_first_september_following_16th_birthday + | in_education_or_training + ) + & ~receives_reg_5_5_benefit + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_for_uc_education.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_for_uc_education.py index 39307c22a0..cee52f742f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_for_uc_education.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_for_uc_education.py @@ -1,15 +1,7 @@ from policyengine_uk.model_api import * - -# Universal Credit (Transitional Provisions) Regulations 2014 reg. 2(1). -EXISTING_BENEFITS = [ - "jsa_income_reported", - "esa_income_reported", - "income_support_reported", - "housing_benefit_reported", - "child_tax_credit_reported", - "working_tax_credit_reported", -] -TAX_CREDITS = ["child_tax_credit_reported", "working_tax_credit_reported"] +from policyengine_uk.variables.gov.dwp.universal_credit.child_element.is_qualifying_young_person_for_universal_credit import ( + qualifying_young_person, +) class is_qualifying_young_person_for_uc_education(Variable): @@ -19,50 +11,83 @@ class is_qualifying_young_person_for_uc_education(Variable): documentation = ( "Whether this person counts as a qualifying young person for the " "Universal Credit condition that a claimant is not receiving " - "education: they meet is_qualifying_young_person_for_universal_credit " - "and have no entitlement to an existing benefit. Under the Universal " - "Credit (Transitional Provisions) Regulations 2014 reg. 28, a person " - "entitled to an existing benefit doesn't count as a qualifying young " - "person, and the Secretary of State lists the existing benefits in " - "reg. 2(1): income-based Jobseeker's Allowance, income-related " - "Employment and Support Allowance, Income Support, Housing Benefit, " - "Child Tax Credit and Working Tax Credit. The model reads entitlement " - "from the year's reported receipt: the person's own report of any of " - "them, or, for a claimant or partner, either member's report of tax " - "credits, which a couple claims jointly. A person outside the " - "definition under reg. 28 takes the ordinary route under the Universal " - "Credit Regulations 2013 reg. 12(2). The model's other rules read " - "is_qualifying_young_person_for_universal_credit, which leaves reg. 28 " - "out. In Northern Ireland the decision maker applies the same rule " - "under reg. 29 of the Universal Credit (Transitional Provisions) " - "Regulations (Northern Ireland) 2016, with reg. 6 of the Universal " - "Credit Regulations (Northern Ireland) 2016 in place of the Universal " - "Credit Regulations 2013 reg. 5." + "education. Under the Universal Credit (Transitional Provisions) " + "Regulations 2014 reg. 28, a person entitled to an existing benefit " + "doesn't count as a qualifying young person, and the Secretary of " + "State lists the existing benefits in reg. 2(1): income-based " + "Jobseeker's Allowance, income-related Employment and Support " + "Allowance, Income Support, Housing Benefit, Child Tax Credit and " + "Working Tax Credit. It reads " + "is_qualifying_young_person_for_universal_credit, which applies reg. " + "28, but for a claimant or partner it reads the status on the first " + "day of their Universal Credit award. A claim ends a tax credit award " + "the day before the award starts (reg. 8(2)(a)), but a Housing " + "Benefit award only two weeks after (reg. 8(2A)), and the model's " + "esa_income_report_closed describes the same run-on for " + "income-related ESA. The Secretary of State inserted reg. 8(2A) from " + "11 April 2018 (SI 2018/65), and SI 2019/1152 reg. 5 gives the " + "income-related ESA run-on only to claims made from 22 July 2020; " + "the annual model applies both run-ons in earlier years too. So the " + "Secretary of State still excludes a " + "claimant on such an award that day (reg. 28), and once they receive " + "Universal Credit, as a person receiving it (Universal Credit " + "Regulations 2013 reg. 5(5)), which the model's reg. 5 test reads " + "only from reported Universal Credit. The formula therefore leaves " + "out the Housing Benefit and income-related ESA claim limbs that " + "is_qualifying_young_person_for_universal_credit follows, and keeps " + "the Child Tax Credit one, the year gates and the legacy closures. A " + "claimant whose Universal Credit comes to nil receives none, so after " + "the run-on the law counts them as a qualifying young person again; " + "the annual model keeps them outside the definition all year. A " + "person outside the definition under reg. 28 takes the ordinary " + "route under the Universal Credit Regulations 2013 reg. 12(2). For " + "the reg. 60(1) protection the model reads the status on the " + "migration day instead (is_qualifying_young_person_on_uc_migration_day). " + "In Northern Ireland the decision maker applies the same rule under " + "reg. 29 of the Universal Credit (Transitional Provisions) Regulations " + "(Northern Ireland) 2016, with reg. 6 of the Universal Credit " + "Regulations (Northern Ireland) 2016 in place of the Universal Credit " + "Regulations 2013 reg. 5. SR 2016/226 reg. 6 ends awards on the same " + "days as reg. 8, and SR 2019/152 reg. 4 gives the income-related ESA " + "run-on to claims made from 22 July 2020, with one difference: a claim " + "ends a rate rebate the day before the award starts (reg. 6(2)), and " + "only a rent rebate or allowance runs on (reg. 6(2A)). " + "housing_benefit_reported doesn't show the form, so the formula reads " + "housing_benefit_report_is_rate_rebate_only: a claimant with that " + "status and no other existing benefit counts as a qualifying young " + "person on the first day. It defaults to False, so the formula reads " + "any other report as a rent rebate or allowance." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2014/1230/regulation/28", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/2", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", + "https://www.legislation.gov.uk/uksi/2013/983/article/4", + "https://www.legislation.gov.uk/uksi/2019/1152/regulation/5", "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", "https://www.legislation.gov.uk/ukpga/2002/21/section/3", "https://www.legislation.gov.uk/nisr/2016/226/regulation/29", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/6", "https://www.legislation.gov.uk/nisr/2016/216/regulation/6", + "https://www.legislation.gov.uk/nisr/2017/190/article/6", + "https://www.legislation.gov.uk/nisr/2019/152/regulation/4", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/12", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/13", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/12", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/13", ) def formula(person, period, parameters): - qualifying_young_person = person( - "is_qualifying_young_person_for_universal_credit", period - ) - # SI 2014/1230 reg. 28: where a person who would be a qualifying - # young person "is entitled to an existing benefit", "that person is - # not a qualifying young person for the purposes of the Universal - # Credit Regulations". - own_award = add(person, period, EXISTING_BENEFITS) > 0 - # Tax Credits Act 2002 s. 3(3)(a): the members of a couple claim a - # tax credit jointly, so the award belongs to both. - claimant_or_partner = person("is_claimant_or_partner", period) - joint_tax_credits = claimant_or_partner & ( - person.benunit.sum(claimant_or_partner * add(person, period, TAX_CREDITS)) - > 0 + # Those whom the Housing Benefit and income-related ESA claim limbs + # readmit: on the first day of their award the Secretary of State + # still excludes them through reg. 28, and once they receive + # Universal Credit through reg. 5(5). Removing only them keeps any + # input or reform that sets the predicate. + readmitted = qualifying_young_person( + person, period, parameters + ) & ~qualifying_young_person(person, period, parameters, at_claim_start=True) + return ( + person("is_qualifying_young_person_for_universal_credit", period) + & ~readmitted ) - return qualifying_young_person & ~(own_award | joint_tax_credits) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_on_uc_migration_day.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_on_uc_migration_day.py new file mode 100644 index 0000000000..076ac7d474 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_on_uc_migration_day.py @@ -0,0 +1,69 @@ +from policyengine_uk.model_api import * + +# Universal Credit (Transitional Provisions) Regulations 2014 reg. 2(1). +EXISTING_BENEFITS = [ + "jsa_income_reported", + "esa_income_reported", + "income_support_reported", + "housing_benefit_reported", + "child_tax_credit_reported", + "working_tax_credit_reported", +] +TAX_CREDITS = ["child_tax_credit_reported", "working_tax_credit_reported"] + + +class is_qualifying_young_person_on_uc_migration_day(Variable): + value_type = bool + entity = Person + label = "Qualifying young person on the Universal Credit migration day" + documentation = ( + "Whether this person counted as a qualifying young person for the " + "Universal Credit condition that a claimant is not receiving " + "education on the migration day of a closure claim, the day before " + "the Universal Credit award starts (Universal Credit (Transitional " + "Provisions) Regulations 2014 reg. 49). Under reg. 28, a person " + "entitled to an existing benefit doesn't count as a qualifying young " + "person, and the Secretary of State lists the existing benefits in " + "reg. 2(1). Under reg. 8(2)(a), a claim for Universal Credit ends a " + 'tax credit award "on the day before the first date on which the ' + 'claimant is entitled to universal credit", so on the migration day ' + "reg. 28 still applied to that award. The model reads the year's " + "reported receipt of an existing benefit as that award, whether or " + "not it still pays the benefit that year: the person's own report, " + "or, for a claimant or partner, either member's report of tax " + "credits, which a couple claims jointly. " + "uc_is_excepted_from_education_condition reads this status for the " + "reg. 60(1) protection. In Northern Ireland the decision maker applies " + "the same rules under regs. 6(2), 29 and 50 of the Universal Credit " + "(Transitional Provisions) Regulations (Northern Ireland) 2016." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/28", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/2", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/49", + "https://www.legislation.gov.uk/ukpga/2002/21/section/3", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/6", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/29", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/50", + ) + + def formula(person, period, parameters): + qualifying_young_person = person( + "is_qualifying_young_person_for_universal_credit", period + ) + # SI 2014/1230 reg. 28: where a person who would be a qualifying + # young person "is entitled to an existing benefit", "that person is + # not a qualifying young person for the purposes of the Universal + # Credit Regulations". The report stands for the award the closure + # claim ended, so the formula reads it in every year. + own_award = add(person, period, EXISTING_BENEFITS) > 0 + # Tax Credits Act 2002 s. 3(3)(a): the members of a couple claim a + # tax credit jointly, so the award belongs to both. + claimant_or_partner = person("is_claimant_or_partner", period) + joint_tax_credits = claimant_or_partner & ( + person.benunit.sum(claimant_or_partner * add(person, period, TAX_CREDITS)) + > 0 + ) + return qualifying_young_person & ~(own_award | joint_tax_credits) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_excepted_from_education_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_excepted_from_education_condition.py index 209eddad27..5b42c4658f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_excepted_from_education_condition.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_excepted_from_education_condition.py @@ -22,10 +22,10 @@ class uc_is_excepted_from_education_condition(Variable): "a family that claims Universal Credit when DWP moves it off its " "legacy benefits (claims_uc_at_legacy_closure), which the model takes " "as the qualifying claim under reg. 48 of the 2014 Regulations, and who " - "doesn't count " - "as a qualifying young person for the condition " - "(is_qualifying_young_person_for_uc_education, which applies reg. 28 " - "of the 2014 Regulations). The model needs no rule of its own for limb " + "didn't count as a qualifying young person for the condition on the " + "migration day (is_qualifying_young_person_on_uc_migration_day, which " + "applies reg. 28 of the 2014 Regulations to the award the claim " + "ended). The model needs no rule of its own for limb " "(e)(i): it places responsibility for a child on the benefit unit, so " "both members of the couple meet (c). Under (b), the decision maker " "must determine limited capability for work before the course starts. " @@ -63,6 +63,7 @@ class uc_is_excepted_from_education_condition(Variable): "https://www.legislation.gov.uk/uksi/2013/376/regulation/2", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/28", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/48", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/49", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60", "https://www.legislation.gov.uk/nisr/2016/216/regulation/14", "https://www.legislation.gov.uk/nisr/2016/226/regulation/29", @@ -127,12 +128,13 @@ def formula(person, period, parameters): # that condition is not to apply ... while they are continuing to # undertake that course". A qualifying young person receives # education under reg. 12(1A), not reg. 12(2); reg. 28 applies - # first, so a person entitled to an existing benefit takes reg. - # 12(2) and keeps the protection. + # first, so a person entitled to an existing benefit on the + # migration day took reg. 12(2) and keeps the protection while they + # continue the course, after that award has ended. migrated_student = ( claimant_or_partner & benunit("claims_uc_at_legacy_closure", period) - & ~person("is_qualifying_young_person_for_uc_education", period) + & ~person("is_qualifying_young_person_on_uc_migration_day", period) ) return ( disabled_with_limited_capability_for_work diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py index 476e395c44..dadb1f9777 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py @@ -9,8 +9,13 @@ class is_child_or_young_person_for_council_tax_reduction(Variable): young person: - Scotland's working-age scheme, from 1 April 2022, uses the Universal - Credit qualifying young person (Council Tax Reduction (Scotland) - Regulations 2021 reg. 6(1); UC Regs 2013 reg. 5). The applicant is + Credit child and the qualifying young person of UC Regs 2013 reg. 5 + (Council Tax Reduction (Scotland) Regulations 2021 reg. 6(1)). The + Secretary of State excludes a person entitled to an existing benefit + only for the purposes of the UC Regulations and WRA 2012 Part 1 (SI + 2014/1230 reg. 28), so the formula reads reg. 5 alone and keeps a + young person who receives Income Support, Housing Benefit, a tax + credit, income-related ESA or income-based JSA. The applicant is responsible for one who normally lives with them, never for one they live with as a couple, and no one is responsible for one placed with a foster carer (reg. 7). @@ -34,6 +39,8 @@ class is_child_or_young_person_for_council_tax_reduction(Variable): definition_period = YEAR reference = ( "https://www.legislation.gov.uk/ssi/2021/249/regulation/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/28", "https://www.legislation.gov.uk/ssi/2021/249/regulation/6", "https://www.legislation.gov.uk/ssi/2021/249/regulation/7", "https://www.legislation.gov.uk/ssi/2012/319/regulation/4", @@ -50,10 +57,18 @@ def formula(person, period, parameters): & ~person.benunit("council_tax_reduction_pensioner", period) & p.young_person_follows_universal_credit ) - # SSI 2021/249 regs 4, 6 and 7. The Universal Credit flag already - # leaves out a child or young person looked after by a local authority. - universal_credit = person( - "is_child_or_qualifying_young_person_for_universal_credit", period - ) & ~person("is_claimant_or_partner", period) + # SSI 2021/249 regs 4, 6 and 7: a child under UC Regs reg. 4, or a + # qualifying young person under reg. 5 without TP reg. 28. + universal_credit = ( + ( + person("is_child_for_universal_credit", period) + | person( + "is_qualifying_young_person_under_universal_credit_regulation_5", + period, + ) + ) + & ~person("is_looked_after_by_local_authority", period) + & ~person("is_claimant_or_partner", period) + ) child_benefit = person("is_child_or_young_person_for_legacy_benefits", period) return where(scottish_working_age, universal_credit, child_benefit)