diff --git a/changelog.d/2346.fixed.md b/changelog.d/2346.fixed.md new file mode 100644 index 0000000000..045c8dc4b9 --- /dev/null +++ b/changelog.d/2346.fixed.md @@ -0,0 +1 @@ +The model now applies the Universal Credit condition that a claimant is not receiving education, with its exceptions, to people a user or dataset flags with the new `is_in_full_time_education` input, and reads maintenance funding for non-advanced courses from the new `has_education_maintenance_funding` input. 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 new file mode 100644 index 0000000000..bc1ed54e69 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/uc_receiving_education.yaml @@ -0,0 +1,1264 @@ +# Universal Credit's basic condition that a claimant "is not receiving +# education" (Welfare Reform Act 2012 s. 4(1)(d)), with the meaning in the +# Universal Credit Regulations 2013 reg. 12, the exceptions in reg. 14(1) and +# the couple rule in reg. 3(2), which DWP applies under its Advice for +# Decision Making E2016 where one member of a couple meets all the basic +# conditions. +# +# We calculate expected amounts from DWP's monthly rates for 2026 to 2027 +# (Benefit and pension rates 2026 to 2027, gov.uk): standard allowance £338.58 +# for a single claimant under 25, £424.90 for one aged 25 or over, £528.34 for +# joint claimants both under 25 and £666.97 where one or both are 25 or over; +# child element £351.88 for a first child born before 6 April 2017 and £303.94 +# for any other child. Each award comes to 12 times the monthly amount, less +# 55% of earnings (reg. 22(1)(b)); no case has housing costs except cases 1 +# and 2. +# +# In cases 36 to 41 we test, on one student in years from 2024 to 2027, the +# protection in the Universal Credit (Transitional Provisions) Regulations +# 2014 reg. 60, and in case 41 its Northern Ireland counterpart, reg. 61 of +# SR 2016/226. For 2025 to 2026 we take DWP's monthly standard allowance for a +# single claimant aged 25 or over, £400.14, from Benefit and pension rates +# 2025 to 2026 (gov.uk). The Department for Communities set the same amount +# in Northern Ireland (Social Security Benefits Up-rating Order (Northern +# Ireland) 2025, SR 2025/64, art. 32(2) and Schedule 13). DWP had published no +# rates for 2027 to 2028 by November 2026, so we assert no amount in case 38. + +- name: Case 1, issue 2346, a full-time university student aged 19 gets no Universal Credit. + # Reg. 12(2)(a): a full-time course of advanced education (a first degree, + # reg. 12(3)(a)(ii)). No reg. 14(1) exception applies. + period: 2026 + input: + people: + person1: + age: 19 + current_education: TERTIARY + is_in_full_time_education: true + employment_income: 4_571 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: SOUTH_WEST + rent: 5_857 + tenure_type: RENT_PRIVATELY + brma: PLYMOUTH + output: + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [false] + meets_uc_education_condition: [false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 2, issue 2346, the same student not on a full-time course keeps the award. + # 12 x £338.58 = £4,062.96 standard allowance, plus the housing costs + # element at the Universal Credit LHA rate for Plymouth, £400 a month, so + # £4,800, less 55% of £4,571 of pay = £2,514.05 (no work allowance without + # a child or limited capability for work): £8,862.96 - £2,514.05 = + # £6,348.91, as on main at 2da212abc. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 19 + current_education: TERTIARY + is_in_full_time_education: false + employment_income: 4_571 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: SOUTH_WEST + rent: 5_857 + tenure_type: RENT_PRIVATELY + brma: PLYMOUTH + output: + uc_receiving_education: [false] + is_uc_eligible: true + uc_standard_allowance: 4_062.96 + uc_housing_costs_element: 4_800 + uc_income_reduction: 2_514.05 + universal_credit: 6_348.91 + +- name: Case 3, a single parent on a full-time degree keeps the award. + # Reg. 14(1)(c): responsible for a child. 12 x (£338.58 + £303.94) = + # 12 x £642.52 = £7,710.24. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 22 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 3 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_receiving_education: [true, false] + uc_is_excepted_from_education_condition: [true, false] + meets_uc_education_condition: [true, true] + is_uc_eligible: true + universal_credit: 7_710.24 + +- name: Case 4, a couple where one member is a full-time student and the other is not claim jointly. + # Reg. 3(2)(b): one member doesn't meet the condition and has no + # exception, and the other meets it, so they claim jointly at the couple + # rate: 12 x £666.97 = £8,003.64. The model does not count student income + # (reg. 68), and this couple has none. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 30 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + meets_uc_education_condition: [false, true] + is_uc_eligible: true + uc_standard_allowance: 8_003.64 + universal_credit: 8_003.64 + +- name: Case 5, a couple who are both full-time students with no exception get no Universal Credit. + # Under reg. 3(2)(b), the other member must meet the condition or have an + # exception; neither does. + period: 2026 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + meets_uc_education_condition: [false, false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 6, a student couple where the other member is responsible for a child keeps the award. + # Reg. 14(1)(e)(i): both members receive education and the other member + # is responsible for a child. The model places responsibility on the + # benefit unit, so each member also has the reg. 14(1)(c) exception. + # 12 x (£528.34 + £303.94) = 12 x £832.28 = £9,987.36. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 22 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 22 + current_education: TERTIARY + is_in_full_time_education: true + person3: + age: 5 + benunits: + benunit: + members: [person1, person2, person3] + households: + household: + members: [person1, person2, person3] + region: NORTH_EAST + output: + uc_receiving_education: [true, true, false] + uc_is_excepted_from_education_condition: [true, true, false] + is_uc_eligible: true + universal_credit: 9_987.36 + +- name: Case 7, a mixed-age couple who are both full-time students get no Universal Credit under ADM E2016. + # Under reg. 14(1)(f), the member who has reached the qualifying age for + # State Pension Credit need not meet the education condition, but that + # member doesn't meet the age condition. The younger member meets the age + # condition but receives education with no exception. Under ADM E2016, DWP + # accepts a joint claim only where one member meets all the basic + # conditions, and neither does. Since 15 May 2019 the couple cannot get + # Pension Credit either (SPCA 2002 s. 4(1A)). + period: 2026 + input: + people: + person1: + age: 70 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 60 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_receiving_education: [true, true] + uc_is_excepted_from_education_condition: [true, false] + meets_uc_education_condition: [true, false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 8, a mixed-age couple whose younger member alone is a full-time student get no Universal Credit under ADM E2016. + # The older member meets the education condition and the younger the age + # condition, but neither meets both. Under ADM E2016, DWP needs one member + # to meet both. + period: 2026 + input: + people: + person1: + age: 70 + person2: + age: 60 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + meets_uc_education_condition: [true, false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 9, a student with PIP and limited capability for work keeps the award. + # Reg. 14(1)(b): entitled to PIP, with limited capability for work. + # 12 x £424.90 = £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + pip_dl_category: STANDARD + uc_limited_capability_for_work: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 10, a student with PIP but not limited capability for work gets no Universal Credit. + # Under reg. 14(1)(b), the claimant needs both entitlement to a listed + # disability benefit and limited capability for work. + period: 2026 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + pip_dl_category: STANDARD + uc_limited_capability_for_work: false + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 11, a single foster parent on a full-time course keeps the award. + # Reg. 14(1)(d). A foster child is not a member of the benefit unit, so + # the award comes to the standard allowance alone: 12 x £424.90 = + # £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + uc_is_responsible_foster_parent_of_child_under_one: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 12, a student couple whose other member is a foster parent keeps the award. + # Under reg. 14(1)(e)(ii), the member whose partner fosters the child need + # not meet the condition, so the couple can claim jointly under reg. + # 3(2)(b). 12 x £666.97 = £8,003.64. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + uc_is_foster_parent_or_new_friend_or_family_carer: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true, false] + is_uc_eligible: true + universal_credit: 8_003.64 + +- name: Case 13, a 17-year-old qualifying young person on a full-time course gets no Universal Credit. + # Under reg. 8(1)(a), they can claim at 16 with limited capability for + # work. The decision maker treats them as receiving education under reg. + # 12(1A), and without a disability benefit they don't have the reg. + # 14(1)(b) exception. + period: 2026 + input: + people: + person1: + age: 17 + current_education: POST_SECONDARY + is_in_full_time_education: true + uc_limited_capability_for_work: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + is_qualifying_young_person_for_universal_credit: [true] + uc_receiving_education: [true] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 14, the same 17-year-old not on a full-time course keeps the award. + # 12 x £338.58 = £4,062.96. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 17 + current_education: POST_SECONDARY + is_in_full_time_education: false + uc_limited_capability_for_work: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_receiving_education: [false] + is_uc_eligible: true + universal_credit: 4_062.96 + +- name: Case 15, a claimant on a non-advanced full-time course with a maintenance grant receives education. + # Reg. 12(2)(b): "any other full-time course ... for which a student loan + # or grant is provided for the person's maintenance". + period: 2026 + input: + people: + person1: + age: 30 + current_education: POST_SECONDARY + is_in_full_time_education: true + has_education_maintenance_funding: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_receiving_education: [true] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 16, a claimant on a non-advanced full-time course with no loan or grant does not receive education. + # Neither limb of reg. 12(2) applies, and at 30 the person doesn't count as + # a qualifying young person. 12 x £424.90 = £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: POST_SECONDARY + is_in_full_time_education: true + has_education_maintenance_funding: false + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_receiving_education: [false] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 17, the model reads a full-time course in higher education as advanced education. + # Reg. 12(2)(a) and (3): the model reads in_HE when current_education has + # no input. + period: 2026 + input: + people: + person1: + age: 30 + in_HE: true + is_in_full_time_education: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_receiving_education: [true] + is_uc_eligible: false + +- name: Case 18, a member of a couple claiming as a single person must meet the condition. + # The student claims as a single person under reg. 3(3), so reg. 3(2)(b) + # doesn't apply and the claim fails. + period: 2026 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 30 + uc_is_ineligible_partner: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + meets_uc_education_condition: [false, true] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 19, a student whose family claims when DWP moves it off legacy benefits keeps the award. + # SI 2014/1230 reg. 60(1): the condition does not apply while the person + # continues the course. 12 x £424.90 = £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1] + claims_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 20, a 17-year-old in full-time education does not change a parent's award. + # The condition applies to claimants, not to the qualifying young person + # they are responsible for. 12 x (£424.90 + £351.88) = 12 x £776.78 = + # £9,321.36: a child born in 2009 counts as a first child born before 6 + # April 2017. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + person2: + age: 17 + current_education: POST_SECONDARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_receiving_education: [false, true] + is_uc_eligible: true + universal_credit: 9_321.36 + +- name: Case 21, Northern Ireland applies the same condition. + # Welfare Reform (Northern Ireland) Order 2015 art. 9(1)(d) and the + # Universal Credit Regulations (Northern Ireland) 2016 reg. 12(2)(a). + period: 2026 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTHERN_IRELAND + output: + is_uc_eligible: false + universal_credit: 0 + +- name: Case 22, the condition applies in earlier years too. + # Reg. 12 and 14 have applied since 29 April 2013, and no parameter dates + # the condition. + period: 2016 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + is_uc_eligible: false + +- name: Case 23, a mixed-age couple whose older member alone is a full-time student keep the award. + # Under reg. 14(1)(f), the older member need not meet the education + # condition, and the younger member meets the age and education + # conditions, so the couple can claim jointly under reg. 3(2)(a) (ADM + # E2016). 12 x £666.97 = £8,003.64. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + current_education: TERTIARY + is_in_full_time_education: true + person2: + age: 60 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true, false] + meets_uc_education_condition: [true, true] + is_uc_eligible: true + universal_credit: 8_003.64 + +- name: Case 24, a mixed-age couple where neither member studies keep the award. + # The younger member meets every basic condition (reg. 3(2)(a)). + # 12 x £666.97 = £8,003.64. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + person2: + age: 60 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + meets_uc_education_condition: [true, true] + is_uc_eligible: true + universal_credit: 8_003.64 + +- name: Case 25, a mixed-age couple whose younger student claims when DWP moves them off legacy benefits keep the award. + # Under SI 2014/1230 reg. 60(1), the younger member need not meet the + # education condition while they continue the course, so that member meets + # both conditions. 12 x £666.97 = £8,003.64. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + person2: + age: 60 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + claims_uc_at_legacy_closure: true + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true, true] + is_uc_eligible: true + universal_credit: 8_003.64 + +- name: Case 26, a pension-age student couple on Working Tax Credit claim under reg. 60A when DWP moves them. + # Under SI 2014/1230 reg. 60A, the Secretary of State waives the upper age + # condition. Under reg. 60(1), neither member needs to meet the education + # condition. 12 x £666.97 = £8,003.64. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + current_education: TERTIARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + person2: + age: 70 + current_education: TERTIARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + claims_uc_at_legacy_closure: true + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + uc_receiving_education: [true, true] + meets_uc_education_condition: [true, true] + is_uc_eligible: true + universal_credit: 8_003.64 + +- name: Case 27, a student with Attendance Allowance and limited capability for work keeps the award. + # Under reg. 14(1)(b), DWP counts Attendance Allowance as a listed + # disability benefit. We give a working-age claimant Attendance Allowance + # and no other listed benefit to test that benefit on its own. 12 x £424.90 + # = £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + aa_category: LOWER + uc_limited_capability_for_work: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 28, a student with Armed Forces Independence Payment and limited capability for work keeps the award. + # Under reg. 2, DWP counts Armed Forces Independence Payment as attendance + # allowance for reg. 14(1)(b), and doesn't count it as unearned income, + # since reg. 66 doesn't list it. 12 x £424.90 = £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + armed_forces_independence_payment: 8_000 + uc_limited_capability_for_work: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 29, a student with Disability Living Allowance and limited capability for work keeps the award. + # Under reg. 14(1)(b), DWP counts Disability Living Allowance as a listed + # disability benefit. 12 x £424.90 = £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + dla_sc_category: MIDDLE + uc_limited_capability_for_work: true + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 30, a claimant on a non-advanced full-time course with an equipment grant and no maintenance funding keeps the award. + # Under reg. 12(2)(b), DWP treats a claimant on a non-advanced full-time + # course as receiving education only where a loan or grant pays for their + # maintenance. The claimant can use Learner Support for materials and + # equipment (gov.uk/learner-support), and their student loan doesn't fund + # maintenance here, so they don't receive education. 12 x £424.90 = + # £5,098.80. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: POST_SECONDARY + is_in_full_time_education: true + education_grants: 100 + student_loans: 3_000 + has_education_maintenance_funding: false + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_receiving_education: [false] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 31, an 18-year-old on Housing Benefit and a non-advanced full-time course takes the ordinary route. + # SI 2014/1230 reg. 28: a person entitled to an existing benefit, here + # 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. + 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 + 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 32, the same 18-year-old with a maintenance grant receives education under reg. 12(2)(b). + # Under reg. 28, DWP doesn't treat the claimant as a qualifying young + # person, so they take the ordinary route under reg. 12(2), and the grant + # for their maintenance meets reg. 12(2)(b). No exception applies. + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + has_education_maintenance_funding: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [person1] + households: + household: + members: [person1] + region: NORTH_EAST + output: + is_qualifying_young_person_for_uc_education: [false] + uc_receiving_education: [true] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 33, a couple of 18-year-olds on a joint Working Tax Credit award both leave the qualifying young person route. + # Tax Credits Act 2002 s. 3(3)(a): a couple claims jointly, so the award + # belongs to both members, and SI 2014/1230 reg. 28 applies to each, + # whichever member reports it. + period: 2024 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + person2: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + region: NORTH_EAST + output: + is_qualifying_young_person_for_universal_credit: [true, true] + 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 + # 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. + period: 2026 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + has_education_maintenance_funding: true + housing_benefit_reported: 6_000 + benunits: + benunit: + members: [person1] + claims_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + is_qualifying_young_person_for_universal_credit: [true] + is_qualifying_young_person_for_uc_education: [false] + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [true] + meets_uc_education_condition: [true] + is_uc_eligible: true + +- name: Case 35, Working Tax Credit reported by another adult in the benefit unit doesn't lift the condition from a qualifying young person. + # The model's benefit unit can hold an adult who is neither claimant, + # partner, child nor qualifying young person, and the model sums reported + # Working Tax Credit over every member, so this family takes the reg. 60A + # route. Under reg. 60A(1) the Secretary of State waives only "the + # condition in section 4(1)(b) of the Act (claimant has not reached the + # qualifying age for state pension credit)". The claimant, an 18-year-old + # qualifying young person on a full-time non-advanced course, receives + # education under reg. 12(1A). DWP applies reg. 60(1) only to a person on a + # course under reg. 12(2), and the claimant has no reg. 14(1) exception: + # the other adult isn't a child or qualifying young person (reg. 14(1)(c)). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + is_claimant_or_partner: true + person2: + age: 25 + working_tax_credit_reported: 1_000 + is_claimant_or_partner: false + benunits: + benunit: + members: [person1, person2] + claims_uc_at_legacy_closure: true + households: + household: + members: [person1, person2] + region: NORTH_EAST + tenure_type: OWNED_OUTRIGHT + output: + is_qualifying_young_person_for_uc_education: [true, false] + uc_receiving_education: [true, false] + uc_is_excepted_from_education_condition: [false, false] + meets_uc_education_condition: [false, true] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 36, a full-time student whose family claimed when DWP moved it off tax credits keeps the award in 2025. + # We use one claimant in cases 36 to 41: aged 30 and single, with no child, + # no listed disability benefit and no earnings, so DWP applies no reg. 14(1) + # exception to them. While they had Working Tax Credit they worked at least + # 30 hours a week (Working Tax Credit (Entitlement and Maximum Rate) + # Regulations 2002 reg. 4(1)); they stopped before their Universal Credit + # award began, so they have no earned income from its first assessment period + # on. They undertake a full-time degree, so they receive education under reg. + # 12(2)(a). We set Working Tax Credit of £1,000 to stand in for the award + # that closed, and the model closes tax credits from 6 April 2025. The family + # claims only because its tax credits close (would_claim_uc false, + # would_claim_uc_at_legacy_closure true). Under SI 2014/1230, DWP issued the + # claimant a migration notice (reg. 44(1)) while they had an award of Working + # Tax Credit, an existing benefit (reg. 2(1)), so they became a notified + # person (reg. 44(6)). By claiming on or before the final deadline, the last + # day of the first assessment period of an award starting on the deadline day + # (reg. 46(4)), they make a qualifying claim (reg. 48). On the migration day, + # the day before their entitlement starts (reg. 49), they undertake the + # course, so DWP doesn't apply the condition in s. 4(1)(d) to them while they + # continue it (reg. 60(1)). No one could claim a tax credit for 2025-26 (reg. + # 6A(9)), and an award on a claim made by the final deadline starts no later + # than the deadline day (reg. 46(3)), so with a deadline day on or before 6 + # April 2025 (reg. 44(3B)) the migration day fell on or before 5 April 2025; + # in 2025-26 DWP applies the text of reg. 60(1) in force from 27 January + # 2025. We assume 12 assessment periods at the 2025 to 2026 rate and no + # transitional element (reg. 52(1)): 12 x £400.14 = £4,801.68. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 4_801.68 + +- name: Case 37, the student of case 36 continues the same course in 2026 and keeps the award. + # The claimant continues the course they undertook on the migration day, so + # DWP still doesn't apply the condition (SI 2014/1230 reg. 60(1)). Under reg. + # 60(2), DWP stops the protection only in an assessment period in which a + # transitional element would have stopped under reg. 56 or 57, and none of + # those events has happened. The claimant has no earned income and isn't in + # gainful self-employment, so the minimum income floor neither applies to + # them nor would apply but for a start-up period (Universal Credit + # Regulations 2013 reg. 62(1) and (5)); so even under reg. 56(3)(b) their + # earned income didn't reach the single administrative threshold in the first + # assessment period, and DWP can't apply reg. 56(2) (reg. 56(2)(a)(ii)). DWP + # applies reg. 56(3A) only to an award mentioned in reg. 60A(1), and the + # claimant has no such award. They haven't joined a couple (reg. 56(4)(b)), + # and their award continues, so DWP doesn't apply reg. 57 either. With no + # transitional element, as in case 36, 12 x £424.90 = £5,098.80, as in case + # 19. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 5_098.80 + +- name: Case 38, the student of case 36 finishes the course and meets the condition in 2027. + # We keep current_education TERTIARY from case 36 and set + # is_in_full_time_education false for the end of the course: the model reads + # only that input to decide whether the claimant undertakes a full-time + # course. The claimant no longer undertakes the course they undertook on the + # migration day, and DWP lifts the condition only "while they are continuing + # to undertake that course" (SI 2014/1230 reg. 60(1)), so DWP now applies it + # to them. Off the course, they don't receive education under reg. 12(2), so + # they meet s. 4(1)(d) without an exception. DWP had published no rates for + # 2027 to 2028 by November 2026, so we assert no amount. The model keeps + # uc_is_excepted_from_education_condition true here although DWP no longer + # applies reg. 60(1) to the claimant, so we don't assert it. + period: 2027 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: false + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + uc_receiving_education: [false] + meets_uc_education_condition: [true] + is_uc_eligible: true + +- name: Case 39, the student of case 36 claims after the final deadline and gets no Universal Credit. + # The family claims Universal Credit after the final deadline (SI 2014/1230 + # reg. 46(4)): it claims (would_claim_uc true), but not in answer to its + # migration notice (would_claim_uc_at_legacy_closure false). A claim after the + # final deadline doesn't count as a qualifying claim (reg. 48), so DWP doesn't + # apply reg. 60(1). The claimant receives education under reg. 12(2)(a) and + # has no reg. 14(1) exception, so they don't meet s. 4(1)(d), and DWP pays + # nothing. + period: 2025 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: true + would_claim_uc_at_legacy_closure: false + households: + household: + members: [person1] + region: NORTH_EAST + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: false + claims_universal_credit: true + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [false] + meets_uc_education_condition: [false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 40, the student of case 36 claims before the closure and gets no Universal Credit in 2024. + # The model keeps tax credits open in 2024, so legacy_benefits_closed reads + # false, and the family claims Universal Credit (would_claim_uc true) with no + # closure. The claimant claims before DWP issues them a migration notice, so + # they don't claim as a notified person (SI 2014/1230 reg. 44(6)) and have no + # deadline day or final deadline (regs. 44(1)(b) and 46(4)). Their claim + # doesn't count as a qualifying claim (reg. 48), and DWP doesn't apply reg. + # 60(1). Since 27 January 2025 DWP has applied reg. 60(1) to a person who has + # made a qualifying claim (SI 2025/3 reg. 6(9)(a) and (c)); before then it + # applied reg. 60(1) only to a notified person, so the claimant gets no + # protection under either text. They receive education under reg. 12(2)(a) + # with no reg. 14(1) exception, and DWP pays nothing. A family that claimed + # by the final deadline after DWP issued it a migration notice in 2024-25 did + # make a qualifying claim (reg. 48). Until 26 January 2025 DWP applied reg. + # 60(1) to a notified person in it who was on the course on the day their + # existing awards were to terminate as a consequence of the claim, which for + # a tax credit award ended by the claim is the migration day (regs. 8(2)(a) + # and 49); from 27 January 2025 it applied reg. 60(1) to a full-time student + # in it who was on the course on the migration day. The model, which takes + # the closure from the active parameters here, protects such a student only + # from 2025-26. + period: 2024 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: true + would_claim_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + legacy_benefits_closed: false + claims_uc_at_legacy_closure: false + claims_universal_credit: true + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [false] + meets_uc_education_condition: [false] + is_uc_eligible: false + universal_credit: 0 + +- name: Case 41, the student of case 36 in Northern Ireland keeps the award under SR 2016/226 reg. 61. + # Under the Universal Credit (Transitional Provisions) Regulations (Northern + # Ireland) 2016, the Department for Communities issued the claimant a + # migration notice while they had an award of Working Tax Credit (reg. + # 45(1)), so they became a notified person (reg. 45(6)). By claiming on or + # before the final deadline (reg. 47(4)), they make a qualifying claim (reg. + # 49). On the migration day (reg. 50) they undertake the course, so the + # Department doesn't apply the condition in the Welfare Reform (Northern + # Ireland) Order 2015 art. 9(1)(d) to them while they continue it (reg. + # 61(1)). With the assumptions of case 36, and the monthly £400.14 that the + # Department for Communities set in SR 2025/64 Schedule 13, 12 x £400.14 = + # £4,801.68. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 30 + current_education: TERTIARY + is_in_full_time_education: true + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTHERN_IRELAND + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + uc_receiving_education: [true] + uc_is_excepted_from_education_condition: [true] + is_uc_eligible: true + universal_credit: 4_801.68 + +- name: Case 42, the 18-year-old of case 34 on Working Tax Credit keeps the award when the model derives the closure. + # The single 18-year-old of case 34, with Working Tax Credit of £1,000 in + # place of Housing Benefit, so the model derives the closure from the tax + # credit report (would_claim_uc false, would_claim_uc_at_legacy_closure + # true). As in cases 37 and 38, we keep the report in 2026 to stand for the + # award that closed. DWP issued the claimant a migration notice while they + # had the Working Tax Credit award, and they claimed by the final deadline, + # so they made a qualifying claim (SI 2014/1230 reg. 48). Under reg. 8(2)(a), + # the claim ended the Working Tax Credit award "on the day before the first + # date on which the claimant is entitled to universal credit", which is the + # migration day (reg. 49). So on the migration day the claimant still had + # entitlement to an existing benefit, and under reg. 28 they didn't count as + # a qualifying young person. They had a student loan or grant for their + # maintenance on a full-time course, so they received education under the + # Universal Credit Regulations 2013 reg. 12(2)(b), and failed the condition + # in s. 4(1)(d) that day "because the person is undertaking a full-time + # course (see regulation 12(2) and 13 ...)". So DWP doesn't apply the + # condition to them while they continue the course (reg. 60(1)), even in a + # year in which no award of an existing benefit remains for reg. 28 to read. + # With no reg. 60(2) event, as in case 37, 12 assessment periods at the 2026 + # to 2027 rate for a single claimant under 25 and no transitional element + # (reg. 52(1)), the award comes to 12 x £338.58 = £4,062.96. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 18 + current_education: POST_SECONDARY + is_in_full_time_education: true + has_education_maintenance_funding: true + working_tax_credit_reported: 1_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_uc_at_legacy_closure: true + households: + household: + members: [person1] + region: NORTH_EAST + output: + legacy_benefits_closed: true + claims_uc_at_legacy_closure: true + is_qualifying_young_person_for_uc_education: [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 diff --git a/policyengine_uk/tests/test_uc_receiving_education_properties.py b/policyengine_uk/tests/test_uc_receiving_education_properties.py new file mode 100644 index 0000000000..44c304d9d7 --- /dev/null +++ b/policyengine_uk/tests/test_uc_receiving_education_properties.py @@ -0,0 +1,957 @@ +"""Universal Credit's condition that a claimant is not receiving education. + +A Universal Credit claimant must meet the basic condition in the Welfare +Reform Act 2012 s. 4(1)(d) that they are "not receiving education". In the +Universal Credit Regulations 2013, the Secretary of State sets out who +receives education (reg. 12) and who doesn't have to meet the condition (reg. +14(1)). Under reg. 3(2), a couple can claim jointly where one member doesn't +meet the age condition or this one, and DWP applies it under its Advice for +Decision Making E2016 "where one member of a couple meets all the basic UC +conditions". 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. In the model, only a person flagged +is_in_full_time_education can receive education, and only +has_education_maintenance_funding meets the funding test in reg. 12(2)(b). + +For each case, Hypothesis draws a year and up to four families, each in a +household of its own: a single adult or a couple, up to two children or young +people, and inputs that reach every limb the model applies (course level, +maintenance funding, student loans and grants, Attendance Allowance, Armed +Forces Independence Payment, DLA and PIP with or without limited capability for +work, foster flags, an ineligible partner, reported Housing Benefit and Working +Tax Credit, a legacy closure claim, earnings, rent and savings). One simulation +calculates the same people five ways: as drawn; with the eligibility formula +on main before this change (BEFORE, copied below with its comments removed; +the formatter re-wrapped two lines); with no one flagged; with the flags of +members who are neither claimant nor partner reversed; and with every student +loan and education grant removed. +Invariants: + +1. Never raises: each family gets either its Universal Credit under BEFORE or + nil, so setting the input never raises it. +2. Unflagged families don't change: with no one flagged, every output listed + in OUTPUTS equals its value under BEFORE. +3. Differential: receiving education, the exceptions, the qualifying young + person status the condition reads, and is_uc_eligible equal a reference + implementation that reads the drawn inputs. The reference applies regs. + 3(2), 12 and 14 as DWP reads them in ADM E2016, SI 2014/1230 regs. 28 and + 60, and BEFORE's routes; without the education condition it gives BEFORE's + eligibility. +4. Exceptions: a claimant who meets a modelled reg. 14 exception, or the SI + 2014/1230 reg. 60 protection, meets the condition, and a family whose + claimants all meet it gets the Universal Credit it gets under BEFORE. +5. Only the couple counts: reversing the flags of members who are neither + claimant nor partner (children and qualifying young people) changes no + family's Universal Credit. A partner who can't join the claim (reg. 3(3)) + still belongs to the couple, so whether they receive education can decide + reg. 14(1)(e) for the claimant. +6. One member meets both: every eligible family that makes no closure claim + has a claimant who meets the minimum age, hasn't reached the qualifying + age for State Pension Credit and meets the education condition. +7. Maintenance funding comes from the input: removing every student loan and + education grant changes no one's receiving education and no output in + OUTPUTS. +8. Reg. 28: a person who reports an existing benefit never counts as a + qualifying young person for the condition. + +A family in a household with other families can lose Universal Credit +because of another family's student: when the household head's family cannot +claim, its non-dependants count in the next claim by a family liable for the +rent. Under Schedule 4 para. 9(2)(f), DWP doesn't count a person as a +non-dependant in a claim if it already counts them in another claim by someone +liable for the rent, and the model assigns non-dependants to the household +head's eligible claim first. test_student_head_moves_non_dependant_to_joint_tenant +pins it; the invariants above hold for families with households of their own. + +test_reg_60_protection_follows_the_closure_dates checks two more invariants, +in model years 2020 to 2028, on single people aged 25 to 59 with no child, no +listed disability benefit, no foster role, no capital and no other benefit +report, when the model derives claims_uc_at_legacy_closure and +legacy_benefits_closed itself: uc_is_excepted_from_education_condition holds +exactly when, that year, they report a legacy benefit whose active parameter +reads false and their family claims at closure; and is_uc_eligible holds for +a full-time degree student exactly when that exception holds, and for anyone +not on a full-time course always. It reads the closure dates from the +parameters. With claims_uc_at_legacy_closure set true, which +test_education_condition_invariants draws in 2019, 2021 and 2024, the model +excepts them in any year. +""" + +import json + +import numpy as np +import pytest +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st +from policyengine_core.reforms import Reform + +from policyengine_uk import Simulation +from policyengine_uk.model_api import * + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +PROPERTY_SETTINGS = settings( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2019: before the mixed-age couple exclusion of 15 May 2019 (parameters are +# read at 30 April). 2021, 2024 and 2026 span the reg. 14(1)(b) versions. +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. +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"] +# Compare eligibility, maximum entitlement, income reduction and Universal +# Credit awards. +OUTPUTS = [ + "universal_credit", + "is_uc_eligible", + "uc_maximum_amount", + "uc_income_reduction", +] +rarely = st.integers(0, 4).map(lambda n: n == 0) +sometimes = st.integers(0, 2).map(lambda n: n == 0) + + +def main_eligibility(benunit, period, parameters): + """is_uc_eligible on main at 2da212abc, without its comments.""" + capital = benunit("uc_assessable_capital", period) + limit = parameters(period).gov.dwp.universal_credit.means_test.capital.limit + claimant = benunit.members("is_uc_single_or_joint_claimant", period) + meets_minimum_age = benunit.members("meets_uc_minimum_age_condition", period) + pension_age = benunit.members( + "has_attained_state_pension_credit_qualifying_age", period + ) + has_qualifying_claimant = benunit.any(claimant & meets_minimum_age & ~pension_age) + pension_credit_route = benunit("meets_pension_credit_age_conditions", period) + closure_claim = benunit("claims_uc_at_legacy_closure", period) + on_working_tax_credit = add(benunit, period, ["working_tax_credit_reported"]) > 0 + reg_60a = on_working_tax_credit & benunit.any(claimant & meets_minimum_age) + route = (has_qualifying_claimant & ~pension_credit_route) | ( + closure_claim & (has_qualifying_claimant | reg_60a) + ) + return route & (capital <= limit) + + +class uses_main_eligibility(Variable): + value_type = bool + entity = BenUnit + label = "Takes is_uc_eligible from main's formula (test only)" + definition_period = YEAR + default_value = False + + +class with_main_eligibility(Reform): + """Keeps this branch's is_uc_eligible, except for benefit units flagged + uses_main_eligibility, which take main's formula. One simulation then + holds the families as drawn and their copies under BEFORE.""" + + def apply(self): + head = self.variables["is_uc_eligible"].get_formula() + + class is_uc_eligible(Variable): + def formula(benunit, period, parameters): + return where( + benunit("uses_main_eligibility", period), + main_eligibility(benunit, period, parameters), + head(benunit, period, parameters), + ) + + self.add_variable(uses_main_eligibility) + self.update_variable(is_uc_eligible) + + +@st.composite +def adult(draw, ages): + full_time = draw(sometimes) + return dict( + age=draw(ages), + is_in_full_time_education=full_time, + current_education=draw(st.sampled_from(EDUCATION)), + in_HE=draw(rarely), + has_education_maintenance_funding=draw(rarely), + student_loans=draw(st.sampled_from([0, 0, 0, 3_000])), + education_grants=draw(st.sampled_from([0, 0, 0, 1_500])), + aa_category=draw(st.sampled_from(["NONE", "NONE", "NONE", "LOWER"])), + armed_forces_independence_payment=draw(st.sampled_from([0, 0, 0, 8_000])), + dla_sc_category=draw(st.sampled_from(["NONE", "NONE", "NONE", "MIDDLE"])), + pip_dl_category=draw(st.sampled_from(["NONE", "NONE", "STANDARD"])), + uc_limited_capability_for_work=draw(rarely), + 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])), + 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, + ) + + +@st.composite +def young_person(draw): + return dict( + age=draw(st.sampled_from([2, 10, 16, 17, 18])), + is_in_full_time_education=draw(st.booleans()), + current_education=draw(st.sampled_from(EDUCATION)), + has_education_maintenance_funding=draw(rarely), + is_claimant_or_partner=False, + ) + + +@st.composite +def family(draw): + ages = st.sampled_from([16, 17, 18, 19, 22, 30, 45, 61, 70, 80]) + people = [draw(adult(ages))] + if draw(st.booleans()): + partner = draw(adult(ages)) + partner["uc_is_ineligible_partner"] = draw(rarely) + people.append(partner) + people += draw(st.lists(young_person(), max_size=2)) + if len(people) > 1 and any(not p["is_claimant_or_partner"] for p in people): + for person in people: + if person["is_claimant_or_partner"]: + person["is_parent"] = True + tenure = draw(st.sampled_from(["RENT_FROM_COUNCIL", "OWNED_OUTRIGHT"])) + return dict( + people=people, + benunit=dict(claims_uc_at_legacy_closure=draw(rarely)), + household=dict( + region=draw(st.sampled_from(REGIONS)), + tenure_type=tenure, + rent=0 if tenure == "OWNED_OUTRIGHT" else 6_000, + savings=draw(st.sampled_from([0, 0, 20_000])), + ), + ) + + +@st.composite +def cases(draw): + return dict( + year=draw(st.sampled_from(YEARS)), + families=draw(st.lists(family(), min_size=1, max_size=4)), + ) + + +def situation(case, variants): + """Each family in a household of its own, once for each variant. A variant + is a pair: None for the drawn inputs, or a function that takes a person's + index and inputs and returns the inputs to use; and whether the family + takes main's eligibility formula (BEFORE).""" + year = case["year"] + people, benunits, households = {}, {}, {} + for v, (change, uses_main) in enumerate(variants): + index = 0 + for f, fam in enumerate(case["families"]): + members = [] + for person in fam["people"]: + name = f"p{v}_{index}" + values = dict(person) + if change is not None: + values = change(index, values) + people[name] = {k: {year: x} for k, x in values.items()} + members.append(name) + index += 1 + benunits[f"b{v}_{f}"] = { + "members": members, + **{k: {year: x} for k, x in fam["benunit"].items()}, + } + households[f"h{v}_{f}"] = { + "members": members, + **{k: {year: x} for k, x in fam["household"].items()}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def all_flags(case): + return [ + p["is_in_full_time_education"] for f in case["families"] for p in f["people"] + ] + + +VARIABLES = OUTPUTS + [ + "is_uc_single_or_joint_claimant", + "is_uc_assessed_claimant", + "meets_uc_minimum_age_condition", + "meets_pension_credit_age_conditions", + "uc_assessable_capital", + "is_qualifying_young_person_for_universal_credit", + "is_qualifying_young_person_for_uc_education", + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", + "has_attained_state_pension_credit_qualifying_age", + "is_mixed_age_couple", + "uc_limited_capability_for_work", + "attendance_allowance", + "dla", + "pip", + "uc_receiving_education", + "uc_is_excepted_from_education_condition", + "meets_uc_education_condition", +] + + +DRAWN = (None, False) +BEFORE = (None, True) + + +def run(case, variants=(DRAWN,)): + """One simulation of every variant; returns one result per variant.""" + simulation = Simulation(situation=situation(case, variants)) + simulation.apply_reform(with_main_eligibility) + year = case["year"] + # Benefit units follow the situation's order: each variant's families. + simulation.set_input( + "uses_main_eligibility", + year, + np.array([uses_main for _, uses_main in variants for _ in case["families"]]), + ) + values = {name: simulation.calculate(name, year) for name in VARIABLES} + limit = simulation.tax_benefit_system.parameters( + f"{year}-01-01" + ).gov.dwp.universal_credit.means_test.capital.limit + sizes = { + "person": sum(len(f["people"]) for f in case["families"]), + "benunit": len(case["families"]), + "household": len(case["families"]), + } + results = [] + for v in range(len(variants)): + result = {"capital_limit": limit} + for name, array in values.items(): + entity = simulation.tax_benefit_system.variables[name].entity.key + size = sizes[entity] + result[name] = array[v * size : (v + 1) * size] + results.append(result) + return results + + +def reference(case, result): + """Regs. 3(2), 12 and 14, SI 2014/1230 regs. 28 and 60 and ADM E2016 from + the drawn inputs. + + Returns, per person, receiving education, excepted and qualifying young + 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 + capability for work and the disability benefit amounts, which other rules + settle. + """ + receiving, excepted, qyp_for_education = [], [], [] + eligible, eligible_before = [], [] + index = 0 + for f, fam in enumerate(case["families"]): + start = index + 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( + person_of[j].get(name, 0) > 0 for j in couple for name in TAX_CREDITS + ) + qyp, 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_for_universal_credit"][i]) + and not existing_benefit + ) + advanced = person["current_education"] == "TERTIARY" or person.get( + "in_HE", False + ) + maintenance = person.get("has_education_maintenance_funding", False) + recv[i] = person["is_in_full_time_education"] and ( + advanced or maintenance or qyp[i] + ) + foster = { + i: person.get("uc_is_responsible_foster_parent_of_child_under_one", False) + or person.get("uc_is_foster_parent_or_new_friend_or_family_carer", False) + for i, person in person_of.items() + } + exc = {} + for i, person in person_of.items(): + in_couple_role = i in couple + partner = [j for j in couple if j != i] + disability_benefit = ( + result["attendance_allowance"][i] > 0 + or person.get("armed_forces_independence_payment", 0) > 0 + or result["dla"][i] > 0 + or result["pip"][i] > 0 + ) + limb_b = bool(disability_benefit) and bool( + result["uc_limited_capability_for_work"][i] + ) + limb_c = in_couple_role and bool( + result[ + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit" + ][f] + ) + limb_d = in_couple_role and len(couple) == 1 and foster[i] + limb_e = ( + in_couple_role + and len(couple) == 2 + and all(recv[j] for j in couple) + and any(foster[j] for j in partner) + ) + limb_f = ( + in_couple_role + and bool(result["has_attained_state_pension_credit_qualifying_age"][i]) + and bool(result["is_mixed_age_couple"][f]) + ) + reg_60 = ( + in_couple_role + and fam["benunit"]["claims_uc_at_legacy_closure"] + and not qyp[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]] + minimum_age = { + i: bool(result["meets_uc_minimum_age_condition"][i]) for i in claimants + } + pension_age = { + i: bool(result["has_attained_state_pension_credit_qualifying_age"][i]) + for i in claimants + } + on_working_tax_credit = any( + person.get("working_tax_credit_reported", 0) > 0 for person in fam["people"] + ) + pension_credit_route = bool(result["meets_pension_credit_age_conditions"][f]) + closure = fam["benunit"]["claims_uc_at_legacy_closure"] + capital_ok = result["uc_assessable_capital"][f] <= result["capital_limit"] + + def eligibility(meets): + # ADM E2016: one claimant meets the age and education conditions. + qualifying = any( + minimum_age[i] and not pension_age[i] and meets[i] for i in claimants + ) + reg_60a = on_working_tax_credit and any( + minimum_age[i] and meets[i] for i in claimants + ) + route = (qualifying and not pension_credit_route) or ( + closure and (qualifying or reg_60a) + ) + return bool(route and capital_ok) + + eligible.append(eligibility({i: not recv[i] or exc[i] for i in claimants})) + eligible_before.append(eligibility({i: True for i in claimants})) + receiving += [recv[i] for i in members] + excepted += [exc[i] for i in members] + qyp_for_education += [qyp[i] for i in members] + index += len(fam["people"]) + return ( + np.array(receiving), + np.array(excepted), + np.array(qyp_for_education), + np.array(eligible), + np.array(eligible_before), + ) + + +def person(age, **inputs): + values = dict( + age=age, + is_in_full_time_education=False, + current_education="NOT_IN_EDUCATION", + is_claimant_or_partner=True, + ) + values.update(inputs) + return values + + +def student(age, **inputs): + return person( + age, is_in_full_time_education=True, current_education="TERTIARY", **inputs + ) + + +def household(region="NORTH_EAST", **inputs): + values = dict(region=region, tenure_type="OWNED_OUTRIGHT", rent=0, savings=0) + values.update(inputs) + return values + + +def single_family(people, closure=False, **household_inputs): + return dict( + people=people, + benunit=dict(claims_uc_at_legacy_closure=closure), + household=household(**household_inputs), + ) + + +EXAMPLES = [ + # Issue 2346: a 19-year-old on a full-time degree, alone. + dict( + year=2026, + families=[ + single_family( + [student(19, employment_income=4_571)], + region="SOUTH_WEST", + tenure_type="RENT_FROM_COUNCIL", + rent=6_000, + ) + ], + ), + # A mixed-age couple who are both students (reg. 14(1)(f) excepts the + # older member, and no member meets both conditions), and a student + # couple with a foster parent (reg. 14(1)(e)(ii)). + dict( + year=2026, + families=[ + single_family([student(70), student(61)]), + single_family( + [ + student(30), + student(30, uc_is_foster_parent_or_new_friend_or_family_carer=True), + ], + region="NORTHERN_IRELAND", + ), + ], + ), + # The other mixed-age couples: the older member alone studies (eligible), + # and the younger member alone studies (no member meets both). + dict( + year=2026, + families=[ + single_family([student(70), person(61)]), + single_family([person(70), student(61)]), + ], + ), + # SI 2014/1230 reg. 28: an 18-year-old on a non-advanced full-time course + # who reports Housing Benefit, without and with a closure claim. + dict( + year=2026, + families=[ + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + housing_benefit_reported=6_000, + ) + ], + tenure_type="RENT_FROM_COUNCIL", + rent=6_000, + ), + single_family( + [ + person( + 18, + is_in_full_time_education=True, + current_education="POST_SECONDARY", + has_education_maintenance_funding=True, + housing_benefit_reported=6_000, + ) + ], + closure=True, + tenure_type="RENT_FROM_COUNCIL", + rent=6_000, + ), + ], + ), +] + + +def check(case): + context = json.dumps(case) + flags = all_flags(case) + # Members who are neither claimant nor partner: is_claimant_or_partner is + # supplied, and a benefit unit has at most two adults. + couple = [ + p["is_claimant_or_partner"] for f in case["families"] for p in f["people"] + ] + + def unflagged(i, values): + return {**values, "is_in_full_time_education": False} + + def others_reversed(i, values): + if couple[i]: + return values + return {**values, "is_in_full_time_education": not flags[i]} + + def no_loans_or_grants(i, values): + return {**values, "student_loans": 0, "education_grants": 0} + + drawn, no_flags, reversed_others, no_loans, before = run( + case, + variants=[ + DRAWN, + (unflagged, False), + (others_reversed, False), + (no_loans_or_grants, False), + BEFORE, + ], + ) + claimant = drawn["is_uc_single_or_joint_claimant"] + np.testing.assert_array_equal(drawn["is_uc_assessed_claimant"], couple, context) + + uc = drawn["universal_credit"] + uc_before = before["universal_credit"] + # 1. Never raises: either BEFORE's amount or nil. + unchanged = np.isclose(uc, uc_before, atol=0.005) + assert np.all(unchanged | (uc == 0)), context + assert np.all(uc <= uc_before + 0.005), context + # 2. Unflagged families don't change. + for name in OUTPUTS: + np.testing.assert_allclose( + no_flags[name].astype(float), + before[name].astype(float), + atol=0.005, + err_msg=f"{name}: {context}", + ) + # 3. Differential against the reference implementation. + receiving, excepted, qyp, eligible, eligible_before = reference(case, drawn) + np.testing.assert_array_equal(drawn["uc_receiving_education"], receiving, context) + np.testing.assert_array_equal( + drawn["uc_is_excepted_from_education_condition"], excepted, context + ) + np.testing.assert_array_equal( + drawn["is_qualifying_young_person_for_uc_education"], qyp, context + ) + np.testing.assert_array_equal(drawn["is_uc_eligible"], eligible, context) + np.testing.assert_array_equal(before["is_uc_eligible"], eligible_before, context) + # 4. Exceptions keep the claimant in, and a family whose claimants all + # meet the condition keeps BEFORE's award. + meets = drawn["meets_uc_education_condition"] + assert np.all(meets[excepted]), context + index = 0 + for f, fam in enumerate(case["families"]): + members = range(index, index + len(fam["people"])) + if all(meets[i] for i in members if claimant[i]): + assert np.isclose(uc[f], uc_before[f], atol=0.005), context + index += len(fam["people"]) + # 5. Only the couple counts. + np.testing.assert_allclose( + reversed_others["universal_credit"], uc, atol=0.005, err_msg=context + ) + np.testing.assert_array_equal( + reversed_others["is_uc_eligible"], drawn["is_uc_eligible"], context + ) + # 6. One member meets both conditions on the ordinary route. + index = 0 + for f, fam in enumerate(case["families"]): + members = range(index, index + len(fam["people"])) + if ( + drawn["is_uc_eligible"][f] + and not fam["benunit"]["claims_uc_at_legacy_closure"] + ): + assert any( + claimant[i] + and drawn["meets_uc_minimum_age_condition"][i] + and not drawn["has_attained_state_pension_credit_qualifying_age"][i] + and meets[i] + for i in members + ), context + index += len(fam["people"]) + # 7. Student loans and education grants don't decide receiving education. + np.testing.assert_array_equal( + no_loans["uc_receiving_education"], drawn["uc_receiving_education"], context + ) + for name in OUTPUTS: + np.testing.assert_allclose( + no_loans[name].astype(float), + drawn[name].astype(float), + 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]) + + +@PROPERTY_SETTINGS +@example(EXAMPLES[0]) +@example(EXAMPLES[1]) +@example(EXAMPLES[2]) +@example(EXAMPLES[3]) +@given(cases()) +def test_education_condition_invariants(case): + check(case) + + +def test_examples_reach_the_cases(): + """The examples exclude the issue's student, apply the couple rule both + ways and send the young Housing Benefit claimants down the ordinary + route, so the invariants run on each outcome. One simulation holds the + four examples, as drawn and under BEFORE.""" + assert all(example["year"] == 2026 for example in EXAMPLES) + case = dict( + year=2026, + families=[family for example in EXAMPLES for family in example["families"]], + ) + drawn, before = run(case, variants=[DRAWN, BEFORE]) + # Families: the issue's student; the student couples of EXAMPLES[1]; the + # mixed-age couples of EXAMPLES[2]; the young Housing Benefit claimants. + np.testing.assert_array_equal( + drawn["is_uc_eligible"], [False, False, True, True, False, True, True] + ) + assert drawn["universal_credit"][0] == 0 + assert before["universal_credit"][0] > 0 + # People 1 to 4: the mixed-age student couple and the foster couple. + np.testing.assert_array_equal( + drawn["uc_is_excepted_from_education_condition"][1:5], + [True, False, True, False], + ) + # People 9 and 10: the young Housing Benefit claimants. + young = slice(9, 11) + np.testing.assert_array_equal( + drawn["is_qualifying_young_person_for_universal_credit"][young], [True, True] + ) + np.testing.assert_array_equal( + drawn["is_qualifying_young_person_for_uc_education"][young], [False, False] + ) + np.testing.assert_array_equal(drawn["uc_receiving_education"][young], [False, True]) + np.testing.assert_array_equal( + drawn["uc_is_excepted_from_education_condition"][young], [False, True] + ) + assert np.all(drawn["universal_credit"][5:] > 0) + + +def test_student_head_moves_non_dependant_to_joint_tenant(): + """A family with no one flagged loses Universal Credit when the household + head studies full time. + + The head's family cannot claim, so the model counts the non-dependant in + the joint tenant's claim instead. Under paragraph 9(2)(f) of Schedule 4 to + the UC Regs 2013, DWP doesn't count a person as a non-dependant in a claim + if it already counts them in another claim by someone liable for the rent. + The model assigns non-dependants to the household head's eligible claim + first. The joint tenant's claim then has the housing cost contribution of + £96.55 a month in 2026 (Sch 4 para 14(1), as SI 2026/148 art. 32(3) sets + it): 12 x £96.55 = £1,158.60. Two copies of the household, the head + unflagged and then flagged. + """ + year = 2026 + people, benunits, households = {}, {}, {} + for copy, head_full_time in enumerate([False, True]): + head, tenant, other = (f"{role}{copy}" for role in ("head", "tenant", "nd")) + people[head] = { + "age": {year: 30}, + "current_education": {year: "TERTIARY"}, + "is_in_full_time_education": {year: head_full_time}, + } + people[tenant] = {"age": {year: 30}} + people[other] = {"age": {year: 30}, "employment_income": {year: 20_000}} + benunits[f"b_{head}"] = {"members": [head]} + benunits[f"b_{tenant}"] = { + "members": [tenant], + "liable_for_share_of_household_rent": {year: True}, + } + benunits[f"b_{other}"] = {"members": [other]} + households[f"h{copy}"] = { + "members": [head, tenant, other], + "region": {year: "NORTH_EAST"}, + "tenure_type": {year: "RENT_FROM_COUNCIL"}, + "rent": {year: 12_000}, + } + simulation = Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + np.testing.assert_array_equal( + simulation.calculate("uc_non_dependants_counted", year), + [True, False, False, False, True, False], + ) + universal_credit = simulation.calculate("universal_credit", year) + assert np.isclose(universal_credit[1] - universal_credit[4], 12 * 96.55, atol=0.01) + + +# SI 2014/1230 reg. 60(1) in the model: the legacy benefits whose closure the +# model dates with an `active` parameter under gov.dwp, keyed by the report +# that stands for each. +CLOSING_REPORTS = { + "child_tax_credit_reported": "tax_credits.active", + "working_tax_credit_reported": "tax_credits.active", + "income_support_reported": "income_support.active", + "jsa_income_reported": "JSA.income.active", +} +CLOSURE_YEARS = list(range(2020, 2029)) +CLOSURE_SETTINGS = settings( + max_examples=40, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@st.composite +def single_people_at_closure(draw): + """A model year from 2020 to 2028 and single people aged 25 to 59 with no + child, no listed disability benefit, no foster role, no capital and no + other benefit report, each in a household of their own. For each person, + Hypothesis draws the four reports, both claim flags, a region and whether + they take a full-time degree.""" + year = draw(st.sampled_from(CLOSURE_YEARS)) + people = draw( + st.lists( + st.fixed_dictionaries( + dict( + age=st.integers(25, 59), + is_in_full_time_education=st.booleans(), + region=st.sampled_from(REGIONS), + would_claim_uc=st.booleans(), + would_claim_uc_at_legacy_closure=st.booleans(), + **{ + report: st.sampled_from([0, 0, 1, 3_000]) + for report in CLOSING_REPORTS + }, + ) + ), + min_size=1, + max_size=24, + ) + ) + return dict(year=year, people=people) + + +# Child Tax Credit with Income Support: in 2025 the model has closed the +# first and keeps the second open. +TWO_REPORTS = ("child_tax_credit_reported", "income_support_reported") + + +def closure_grid(year): + """No report, each report alone, and Child Tax Credit with Income Support, + with each closure claim flag, on a full-time degree or not, in North East + England and in Northern Ireland: 48 people.""" + people = [] + for region in ("NORTH_EAST", "NORTHERN_IRELAND"): + for reports in [(), *[(r,) for r in CLOSING_REPORTS], TWO_REPORTS]: + for at_closure in (False, True): + for full_time in (False, True): + people.append( + dict( + age=25 + len(people) % 35, + is_in_full_time_education=full_time, + region=region, + would_claim_uc=not at_closure, + would_claim_uc_at_legacy_closure=at_closure, + **{ + name: 1_000 if name in reports else 0 + for name in CLOSING_REPORTS + }, + ) + ) + return dict(year=year, people=people) + + +def closure_situation(case): + year = case["year"] + people, benunits, households = {}, {}, {} + for i, drawn in enumerate(case["people"]): + name = f"p{i}" + people[name] = { + "age": {year: drawn["age"]}, + "current_education": {year: "TERTIARY"}, + "is_in_full_time_education": {year: drawn["is_in_full_time_education"]}, + **{report: {year: drawn[report]} for report in CLOSING_REPORTS}, + } + benunits[f"b{i}"] = { + "members": [name], + "would_claim_uc": {year: drawn["would_claim_uc"]}, + "would_claim_uc_at_legacy_closure": { + year: drawn["would_claim_uc_at_legacy_closure"] + }, + } + households[f"h{i}"] = {"members": [name], "region": {year: drawn["region"]}} + return {"people": people, "benunits": benunits, "households": households} + + +# 2024: tax credits, Income Support and income-based JSA open; 2025: tax +# credits closed; 2026: Income Support and income-based JSA closed too. +# test_closure_grids_straddle_each_closure checks these against the +# parameters. +ALL_OPEN, TAX_CREDITS_CLOSED, ALL_CLOSED = 2024, 2025, 2026 + + +@CLOSURE_SETTINGS +@example(closure_grid(ALL_OPEN)) +@example(closure_grid(TAX_CREDITS_CLOSED)) +@example(closure_grid(ALL_CLOSED)) +@given(single_people_at_closure()) +def test_reg_60_protection_follows_the_closure_dates(case): + """For single people aged 25 to 59 with no child, no listed disability + benefit, no foster role, no capital and no other benefit report, where the + model derives claims_uc_at_legacy_closure and legacy_benefits_closed + itself, uc_is_excepted_from_education_condition holds exactly when, in + that model year, they report a legacy benefit whose active parameter reads + false and their family claims at closure + (would_claim_uc_at_legacy_closure). None of them meets a reg. 14(1) limb, + so the model excepts them only under its reg. 60(1) rule (SR 2016/226 reg. + 61(1) in Northern Ireland), and it takes the closure from the active + parameters. The model then finds a full-time degree student among them + eligible for Universal Credit exactly when the exception holds, and anyone + among them not on a full-time course eligible either way. For a person off + a course the model keeps the exception, although DWP lifts the condition + under reg. 60(1), and the Department for Communities under reg. 61(1), + only while they continue the course (case 38 of + uc_receiving_education.yaml); this test pins that proxy.""" + year = case["year"] + simulation = Simulation(situation=closure_situation(case)) + # The formulas read parameters at 1 January of the model year, and the + # model gives that date the value in force on 30 April + # (convert_to_fiscal_year_parameters), so the 1 April 2026 Income Support + # and income-based JSA closures first count in model year 2026. + dwp = simulation.tax_benefit_system.parameters(f"{year}-01-01").gov.dwp + + def closed(path): + node = dwp + for key in path.split("."): + node = getattr(node, key) + return not bool(node) + + expected = np.array( + [ + drawn["would_claim_uc_at_legacy_closure"] + and any( + drawn[report] > 0 and closed(path) + for report, path in CLOSING_REPORTS.items() + ) + for drawn in case["people"] + ] + ) + context = json.dumps(case) + np.testing.assert_array_equal( + simulation.calculate("uc_is_excepted_from_education_condition", year), + expected, + context, + ) + full_time = np.array([p["is_in_full_time_education"] for p in case["people"]]) + np.testing.assert_array_equal( + simulation.calculate("is_uc_eligible", year), ~full_time | expected, context + ) + + +def test_closure_grids_straddle_each_closure(): + """The explicit grids' years (ALL_OPEN, TAX_CREDITS_CLOSED and ALL_CLOSED) + fall either side of each closure date in the active parameters: tax + credits open in ALL_OPEN and closed in TAX_CREDITS_CLOSED, and Income + Support and income-based JSA open in TAX_CREDITS_CLOSED and closed in + ALL_CLOSED.""" + system = Simulation(situation={"people": {"p": {}}}).tax_benefit_system + + def active(year, path): + node = system.parameters(f"{year}-01-01").gov.dwp + for key in path.split("."): + node = getattr(node, key) + return bool(node) + + assert active(ALL_OPEN, "tax_credits.active") + assert not active(TAX_CREDITS_CLOSED, "tax_credits.active") + for path in ("income_support.active", "JSA.income.active"): + assert active(TAX_CREDITS_CLOSED, path) + assert not active(ALL_CLOSED, path) 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 new file mode 100644 index 0000000000..39307c22a0 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_qualifying_young_person_for_uc_education.py @@ -0,0 +1,68 @@ +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_for_uc_education(Variable): + value_type = bool + entity = Person + label = "Qualifying young person for the Universal Credit education condition" + 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." + ) + 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/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/216/regulation/6", + ) + + 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 + ) + return qualifying_young_person & ~(own_award | joint_tax_credits) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py index bfd3232943..4cc3dfe897 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py @@ -13,7 +13,12 @@ class is_uc_eligible(Variable): "nor can a partner who cannot be a joint claimant: where a member of " "a couple claims as a single person (regulation 3(3)), that member " "must meet the conditions, and the capital tested includes the other " - "member's (regulation 18(2)). A family on the Pension Credit route " + "member's (regulation 18(2)). The qualifying claimant must also meet, " + "or fall within an exception to, the condition that they are not " + "receiving education (meets_uc_education_condition): DWP's Advice " + "for Decision Making E2016 reads regulation 3(2) as needing one " + "member of a couple to meet all the basic conditions. A family on the " + "Pension Credit route " "(meets_pension_credit_age_conditions) is not eligible unless it " "claims when DWP moves it off its legacy benefits " "(claims_uc_at_legacy_closure)." @@ -23,8 +28,10 @@ class is_uc_eligible(Variable): "https://www.legislation.gov.uk/ukpga/2012/5/section/3", "https://www.legislation.gov.uk/ukpga/2012/5/section/4", "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/14", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/5", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/60A", + "https://assets.publishing.service.gov.uk/media/6a0474b6ee62840dba48a1eb/adme2.pdf", ) def formula(benunit, period, parameters): @@ -37,8 +44,30 @@ def formula(benunit, period, parameters): pension_age = benunit.members( "has_attained_state_pension_credit_qualifying_age", period ) + # WRA 2012 s.4(1)(d): "is not receiving education", or excepted from + # it. Under UC Regs 2013 reg 3(2), a couple can claim jointly where (a) + # one member has reached the qualifying age for State Pension Credit + # "if the other member does meet that condition", "or" (b) one member + # receives education with no exception "if the other member does + # meet that condition or is excepted from it". DWP's Advice for + # Decision Making E2016 applies both "where one member of a couple + # meets all the basic UC conditions", so one claimant must meet the + # age and education conditions together. For a mixed-age couple: + # - neither studies: the younger member meets both (reg 3(2)(a)); + # - the older member studies: reg 14(1)(f) excepts them, and the + # younger member meets both (regs 14(1)(f) and 3(2)(a)); + # - the younger member studies: the older member meets the education + # condition and the younger the age condition, but neither meets + # both, so they can't claim (ADM E2016); + # - both study: reg 14(1)(f) excepts the older member, who still + # doesn't meet the age condition, so again neither meets both. + # A member of a couple who claims as a single person (reg 3(3)) must + # meet both themselves. + meets_education_condition = benunit.members( + "meets_uc_education_condition", period + ) has_qualifying_claimant = benunit.any( - claimant & meets_minimum_age & ~pension_age + claimant & meets_minimum_age & ~pension_age & meets_education_condition ) # No family gets both Pension Credit and Universal Credit (UC # (Transitional Provisions) Regs 2014 reg 5(1)(d)). A mixed-age couple @@ -80,7 +109,11 @@ def formula(benunit, period, parameters): on_working_tax_credit = ( add(benunit, period, ["working_tax_credit_reported"]) > 0 ) - reg_60a = on_working_tax_credit & benunit.any(claimant & meets_minimum_age) + # Reg 60A waives only the upper age condition, so the claimant who + # meets the minimum age must meet the education condition too. + reg_60a = on_working_tax_credit & benunit.any( + claimant & meets_minimum_age & meets_education_condition + ) route = (has_qualifying_claimant & ~pension_credit_route) | ( closure_claim & (has_qualifying_claimant | reg_60a) ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/meets_uc_education_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/meets_uc_education_condition.py new file mode 100644 index 0000000000..516fcc402c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/meets_uc_education_condition.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class meets_uc_education_condition(Variable): + value_type = bool + entity = Person + label = "Meets the Universal Credit condition of not receiving education" + documentation = ( + "Whether this person meets the basic condition in the Welfare Reform " + "Act 2012 s. 4(1)(d) that a claimant is not receiving education, or is " + "excepted from it: they don't receive education " + "(uc_receiving_education), or they fall within an exception " + "(uc_is_excepted_from_education_condition). Claimants in Northern " + "Ireland meet the same condition under the Welfare Reform (Northern " + "Ireland) Order 2015 art. 9(1)(d). The model (is_uc_eligible) needs " + "one claimant to meet it and the age condition together, as DWP reads " + "the Universal Credit Regulations 2013 reg. 3(2) in its Advice for " + "Decision Making E2016." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + "https://www.legislation.gov.uk/nisi/2015/2006/article/9", + ) + + def formula(person, period, parameters): + return ~person("uc_receiving_education", period) | person( + "uc_is_excepted_from_education_condition", period + ) 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 new file mode 100644 index 0000000000..209eddad27 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_is_excepted_from_education_condition.py @@ -0,0 +1,144 @@ +from policyengine_uk.model_api import * + + +class uc_is_excepted_from_education_condition(Variable): + value_type = bool + entity = Person + label = "Excepted from the Universal Credit condition of not receiving education" + documentation = ( + "Whether this person doesn't have to meet the basic condition in the " + "Welfare Reform Act 2012 s. 4(1)(d) (not receiving education), under " + "the Universal Credit Regulations 2013 reg. 14(1) or the Universal " + "Credit (Transitional Provisions) Regulations 2014 reg. 60. The model " + "applies these limbs: (b) entitlement to Attendance Allowance " + "(including Armed Forces Independence Payment, reg. 2), Disability " + "Living Allowance or PIP with limited capability for work " + "(uc_limited_capability_for_work); (c) responsibility for a child or " + "qualifying young person in the benefit unit; (d) a single foster " + "parent with whom a child is placed; (e)(ii) a member of a couple, " + "both of whom receive education, whose partner is such a foster " + "parent; (f) the member of a mixed-age couple who has reached the " + "qualifying age for State Pension Credit; and reg. 60(1), a member of " + "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 " + "(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. " + "The model can derive uc_limited_capability_for_work from disability " + "status (is_disabled_for_benefits) without an assessment, so it can " + "keep students who have never had a limited capability for work " + "determination, as well as students assessed after their course began. " + "The model doesn't infer limited capability for work from an ESA " + "award: a user who wants an ESA assessment to count sets " + "uc_limited_capability_for_work or uc_limited_capability_for_WRA, or a " + "user or dataset sets is_disabled_for_benefits, from which the model " + "derives limited capability for work. The model doesn't apply (a), for " + "a person under 21, or 21 having reached that age during the course, " + "in non-advanced education without parental support, since it has no " + "input for parental support; the other benefits in (b) (Scottish " + "disability assistance, constant attendance allowance and war pension " + "attendance payments), since it has no variables for them; or reg. " + "60(2), so it keeps the protection after transitional protection would " + "have ended. The model identifies foster parents through either input, " + "uc_is_responsible_foster_parent_of_child_under_one or " + "uc_is_foster_parent_or_new_friend_or_family_carer. A user can also " + "set the second for a foster parent between placements, with no child " + "placed, and for a friend or family carer, who is responsible for the " + "child and so meets (c) under the law. In Northern Ireland the " + "decision maker applies the same exceptions under reg. 14(1) of the " + "Universal Credit Regulations (Northern Ireland) 2016, and the same " + "protection under reg. 61 of the Universal Credit (Transitional " + "Provisions) Regulations (Northern Ireland) 2016, with regs. 29 and 49 " + "of those Regulations in place of regs. 28 and 48; the model doesn't " + "apply reg. 61(2) either." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/14", + "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/60", + "https://www.legislation.gov.uk/nisr/2016/216/regulation/14", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/29", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/49", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/61", + ) + + def formula(person, period, parameters): + benunit = person.benunit + claimant_or_partner = person("is_uc_assessed_claimant", period) + in_couple = benunit.sum(claimant_or_partner) == 2 + # Reg. 14(1)(b): "entitled to attendance allowance, ... disability + # living allowance, ... or personal independence payment", and the + # decision maker has determined limited capability for work. + disability_benefit = ( + (person("attendance_allowance", period) > 0) + | (person("armed_forces_independence_payment", period) > 0) + | (person("dla", period) > 0) + | (person("pip", period) > 0) + ) + disabled_with_limited_capability_for_work = disability_benefit & person( + "uc_limited_capability_for_work", period + ) + # Reg. 14(1)(c): "responsible for a child or a qualifying young + # person". + responsible_for_child = claimant_or_partner & benunit( + "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", + period, + ) + foster_parent = claimant_or_partner & ( + person("uc_is_responsible_foster_parent_of_child_under_one", period) + | person("uc_is_foster_parent_or_new_friend_or_family_carer", period) + ) + # Reg. 14(1)(d): "a single person and a foster parent with whom a + # child is placed". + single_foster_parent = foster_parent & ~in_couple + # Reg. 14(1)(e)(ii): "a member of a couple, both of whom are + # receiving education, and the other member is ... a foster parent + # with whom a child is placed". + both_receiving_education = ( + benunit.sum(claimant_or_partner & person("uc_receiving_education", period)) + == 2 + ) + partner_is_foster_parent = benunit.sum(foster_parent) - foster_parent > 0 + student_couple_with_foster_partner = ( + claimant_or_partner + & in_couple + & both_receiving_education + & partner_is_foster_parent + ) + # Reg. 14(1)(f): "has reached the qualifying age for state pension + # credit" and "is a member of a couple the other member of which has + # not reached that age". + older_member_of_mixed_age_couple = ( + claimant_or_partner + & person("has_attained_state_pension_credit_qualifying_age", period) + & benunit("is_mixed_age_couple", period) + ) + # SI 2014/1230 reg. 60(1): where a person who has made a qualifying + # claim does not meet the condition "because the person is + # undertaking a full-time course (see regulation 12(2) and 13 ...), + # 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. + migrated_student = ( + claimant_or_partner + & benunit("claims_uc_at_legacy_closure", period) + & ~person("is_qualifying_young_person_for_uc_education", period) + ) + return ( + disabled_with_limited_capability_for_work + | responsible_for_child + | single_foster_parent + | student_couple_with_foster_partner + | older_member_of_mixed_age_couple + | migrated_student + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receiving_education.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receiving_education.py new file mode 100644 index 0000000000..c6c546a1e4 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receiving_education.py @@ -0,0 +1,69 @@ +from policyengine_uk.model_api import * + + +class uc_receiving_education(Variable): + value_type = bool + entity = Person + label = "Receiving education for Universal Credit" + documentation = ( + "Whether this person receives education under the Universal Credit " + "Regulations 2013 reg. 12, and so fails the basic condition in the " + "Welfare Reform Act 2012 s. 4(1)(d) unless they fall within an " + "exception (uc_is_excepted_from_education_condition). A person on a " + "full-time course (is_in_full_time_education) receives education where " + "they take a course of advanced education (reg. 12(2)(a)), which the " + "model reads from tertiary current_education or in_HE; where they have " + "a student loan or grant for their maintenance (reg. 12(2)(b)), which " + "it reads from has_education_maintenance_funding; or where they count " + "as a qualifying young person (reg. 12(1A)) after the Universal Credit " + "(Transitional Provisions) Regulations 2014 reg. 28 " + "(is_qualifying_young_person_for_uc_education). The model treats a " + "person with is_in_full_time_education false as not receiving " + "education, so it doesn't apply reg. 12(1A) to a qualifying young " + "person outside full-time education, or reg. 12(4) to a course " + "incompatible with a work-related requirement the Secretary of State " + "imposes. Nor does it apply the exception in reg. 12(1A) for a " + "relevant training scheme. Under reg. 13 a person undertakes a course " + "from its first day to its last, vacations included, unless they " + "abandon it or the establishment dismisses them; the model applies the " + "input to the whole year. In Northern Ireland the decision maker " + "applies the same test under reg. 12 of the Universal Credit " + "Regulations (Northern Ireland) 2016, with reg. 29 of the Universal " + "Credit (Transitional Provisions) Regulations (Northern Ireland) 2016 " + "in place of reg. 28, and treats a qualifying young person in a " + "training scheme as receiving education too (NI reg. 12(1))." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/12", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/13", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/68", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/28", + "https://www.legislation.gov.uk/nisr/2016/216/regulation/12", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/29", + ) + + def formula(person, period, parameters): + full_time = person("is_in_full_time_education", period) + # Reg. 12(3): a course leading to a degree, DipHE or HND, or "of a + # standard above a general certificate of education (advanced + # level), or above a Scottish national qualification (higher or + # advanced higher)". + education = person("current_education", period) + advanced = (education == education.possible_values.TERTIARY) | person( + "in_HE", period + ) + # Reg. 12(2)(b): "any other full-time course of study or training at + # an educational establishment for which a student loan or grant is + # provided for the person's maintenance" (reg. 2 takes both terms + # from reg. 68(7)). + maintenance_funding = person("has_education_maintenance_funding", period) + # Reg. 12(1A): "A qualifying young person is to be treated as + # receiving education", whatever the course. SI 2014/1230 reg. 28 + # applies first, so a person entitled to an existing benefit takes + # the route in reg. 12(2). + qualifying_young_person = person( + "is_qualifying_young_person_for_uc_education", period + ) + return full_time & (advanced | maintenance_funding | qualifying_young_person) diff --git a/policyengine_uk/variables/household/demographic/has_education_maintenance_funding.py b/policyengine_uk/variables/household/demographic/has_education_maintenance_funding.py new file mode 100644 index 0000000000..613042744d --- /dev/null +++ b/policyengine_uk/variables/household/demographic/has_education_maintenance_funding.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class has_education_maintenance_funding(Variable): + value_type = bool + entity = Person + label = "Has a student loan or grant for maintenance on their course" + documentation = ( + "Whether this person has a student loan or grant for their maintenance " + "on their course of study or training. A person on a full-time course " + "other than a course of advanced education receives education for " + "Universal Credit when they have one (Universal Credit Regulations " + "2013 reg. 12(2)(b)). DWP counts as a student loan only a loan towards " + "a student's maintenance under the student support legislation, and as " + "a grant any educational grant or award other than a student loan or a " + "payment under a scheme that helps people under 21 finish non-advanced " + "courses (reg. 68(7), applied by reg. 2). A person whose grant pays " + "only for fees, equipment or travel doesn't meet reg. 12(2)(b). It " + "stays false until a user or dataset sets it. The model doesn't derive " + "it from student_loans or education_grants, since it can't tell from " + "either amount what the support pays for." + ) + definition_period = YEAR + set_input = set_input_dispatch_by_period + default_value = False + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/12", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/68", + ) diff --git a/policyengine_uk/variables/household/demographic/is_in_full_time_education.py b/policyengine_uk/variables/household/demographic/is_in_full_time_education.py new file mode 100644 index 0000000000..6bebaf04e0 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/is_in_full_time_education.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class is_in_full_time_education(Variable): + value_type = bool + entity = Person + label = "On a full-time course of education or training" + documentation = ( + "Whether this person undertakes a full-time course of study or " + "training, including the vacations within the course. It stays false " + "until a user or dataset sets it, and no formula derives it. It " + "records only that the course is full-time: the model reads the level " + "from current_education and in_HE, and reads this input for Universal " + "Credit's condition that a claimant is not receiving education " + "(uc_receiving_education)." + ) + definition_period = YEAR + set_input = set_input_dispatch_by_period + default_value = False + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/12", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/13", + )