diff --git a/changelog.d/uc-pc-qyp-sixteen-route.fixed.md b/changelog.d/uc-pc-qyp-sixteen-route.fixed.md new file mode 100644 index 0000000000..9ccff7ad6a --- /dev/null +++ b/changelog.d/uc-pc-qyp-sixteen-route.fixed.md @@ -0,0 +1 @@ +Counted a 16-year-old outside education as a Universal Credit and Pension Credit qualifying young person until the 1 September following their 16th birthday (UC Regs 2013 reg. 5(1)(a); SPC Regs 2002 reg. 4A(1)(a)), in any year that starts before that date. Excluded young people reporting their own Universal Credit, income-related or contributory ESA, or income-based or contribution-based JSA from both statuses, and those receiving Income Support from Pension Credit's (UC Regs 2013 reg. 5(5); SPC Regs 2002 reg. 4A(5)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml index 4c062a41fe..123f7036d9 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml @@ -79,7 +79,7 @@ output: is_uc_work_allowance_eligible: false -- name: Case 6, a nonqualifying 16-year-old does not create a work allowance. +- name: Case 6, a 16-year-old outside education past the reg. 5(1)(a) route does not create a work allowance. period: 2025 input: people: @@ -91,6 +91,7 @@ age: 16 is_in_non_advanced_education: false is_in_approved_training: false + is_before_first_september_following_16th_birthday: false uc_limited_capability_for_WRA: false benunits: benunit: @@ -98,6 +99,25 @@ output: is_uc_work_allowance_eligible: false +- name: Case 6a, a 16-year-old outside education in the reg. 5(1)(a) route creates a work allowance. + period: 2025 + input: + people: + person1: + age: 40 + is_parent: true + uc_limited_capability_for_WRA: false + person2: + age: 16 + is_in_non_advanced_education: false + is_in_approved_training: false + uc_limited_capability_for_WRA: false + benunits: + benunit: + members: [person1, person2] + output: + is_uc_work_allowance_eligible: true + - name: Case 7, a looked-after child does not create a work allowance. period: 2025 input: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_no_education_route.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_no_education_route.yaml new file mode 100644 index 0000000000..3aadede91b --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_no_education_route.yaml @@ -0,0 +1,335 @@ +# UC Regs 2013 reg. 5(1)(a) and SPC Regs 2002 reg. 4A(1)(a): a person who has +# reached 16 is a qualifying young person "up to, but not including, the 1st +# September following" their 16th birthday, whatever their education. The +# model counts the route for a year that starts (6 April) before that date. +# Ages are ages on 6 October; with no date of birth, a whole age places the +# birthday on 6 April (months_since_last_birthday 6). + +- name: A 16-year-old outside education is a qualifying young person for UC and Pension Credit. + # 16th birthday 6 April 2026; the route runs to 31 August 2026. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, young_person] + output: + is_in_non_advanced_education: [false, false] + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_pension_credit: [false, true] + is_child_or_qualifying_young_person_for_pension_credit: [false, true] + +- name: A 16-year-old apprentice qualifies through reg. 5(1)(a), which has no contract-of-employment exclusion. + # Reg. 5(3) excludes training under a contract of employment from the + # education route (b) only. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: POST_SECONDARY + is_apprentice: true + benunits: + benunit: + members: [parent, young_person] + output: + is_in_non_advanced_education: [false, false] + is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_pension_credit: [false, true] + +- name: A 16-year-old outside education whose route has ended for the year is not a qualifying young person. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + is_before_first_september_following_16th_birthday: false + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + +- name: The education route still applies after the 1 September following the 16th birthday. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: UPPER_SECONDARY + is_before_first_september_following_16th_birthday: false + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_pension_credit: [false, true] + +- name: A 17-year-old with a 16th birthday on 15 September 2025 is in the route until 31 August 2026. + # Born 15 September 2009: aged 17 on 6 October 2026. The 1 September + # following the 16th birthday is 1 September 2026, after 6 April 2026. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + date_of_birth: 20090915 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, young_person] + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_pension_credit: [false, true] + +- name: A 16th birthday on 1 September runs the route to the 17th birthday. + # Born 1 September 2009: the 1 September following the 16th birthday (1 + # September 2025) is 1 September 2026, after 6 April 2026. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + date_of_birth: 20090901 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, young_person] + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, true] + +- name: A 16th birthday on 31 August ends the route that day. + # Born 31 August 2009: the route ran only on 31 August 2025, before the + # 2026-27 fiscal year starts. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + date_of_birth: 20090831 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, young_person] + output: + is_before_first_september_following_16th_birthday: [false, false] + is_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + +- name: A 16-year-old receiving Universal Credit in their own right is not a qualifying young person. + # Reg. 5(5) and reg. 4A(5) apply to both routes. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + receives_benefits_in_own_right: true + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + +- name: A looked-after 16-year-old is a qualifying young person but not the family's. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + is_looked_after_by_local_authority: true + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, true] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + +- name: Neither 15- nor 18-year-olds outside education are qualifying young persons. + # A 15-year-old is a child (Welfare Reform Act 2012 s. 40); an 18-year-old's + # 1 September following their 16th birthday has passed. + period: 2026 + input: + people: + parent: + age: 45 + is_parent: true + child: + age: 15 + current_education: NOT_IN_EDUCATION + adult_child: + age: 18 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, child, adult_child] + output: + is_before_first_september_following_16th_birthday: [false, true, false] + is_qualifying_young_person_for_universal_credit: [false, false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, true, false] + is_qualifying_young_person_for_pension_credit: [false, false, false] + +- name: A lone parent with a 16-year-old outside education gets the child element and a work allowance. + # 2026-27: born 6 April 2010, before 6 April 2017, so the higher first-child + # amount: GBP 351.88 a month x 12 = GBP 4,222.56. Responsibility for a + # qualifying young person gives the work allowance (reg. 22(1)(b)): no + # housing element, so GBP 710 a month x 12 = GBP 8,520. Maximum amount: the + # single 25-or-over standard allowance GBP 424.90 x 12 = GBP 5,098.80, plus + # the child element, = GBP 9,321.36. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + output: + uc_individual_child_element: [0, 4_222.56] + uc_child_element: 4_222.56 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true + is_uc_work_allowance_eligible: true + uc_work_allowance: 8_520 + uc_maximum_amount: 9_321.36 + is_uc_benefit_cap_single_claimant_rate: false + +- name: A pensioner with a 16-year-old outside education gets the Pension Credit child addition. + # 2026-27: born 6 April 2010, before 6 April 2017, so the first child + # addition (Sch. IIA para. 10): GBP 81.07 a week x 52 = GBP 4,215.64. The + # single standard minimum guarantee is GBP 238.00 x 52 = GBP 12,376, so the + # minimum guarantee is GBP 16,591.64. + period: 2026 + input: + people: + pensioner: + age: 70 + is_SP_age: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [pensioner, young_person] + households: + household: + members: [pensioner, young_person] + output: + child_minimum_guarantee_addition: 4_215.64 + standard_minimum_guarantee: 12_376 + minimum_guarantee: 16_591.64 + +- name: In 2025-26 the 16-year-old counts first under the two-child limit, and the youngest child loses the element. + # 2025-26: the 16-year-old (born 6 April 2009) is a qualifying young person + # through reg. 5(1)(a), so the eldest of three. The higher first-child + # amount: GBP 339 a month x 12 = GBP 4,068. The 7-year-old (born 6 April + # 2018): GBP 292.81 x 12 = GBP 3,513.72. The 5-year-old (born 6 April 2020, + # after 6 April 2017) is the third child, so no element (reg. 24A). Total + # GBP 7,581.72. + period: 2025 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 16 + current_education: NOT_IN_EDUCATION + child_7: + age: 7 + child_5: + age: 5 + benunits: + benunit: + members: [parent, young_person, child_7, child_5] + households: + household: + members: [parent, young_person, child_7, child_5] + output: + uc_child_index: [-1, 1, 2, 3] + uc_individual_child_element: [0, 4_068, 3_513.72, 0] + uc_is_child_limit_affected: [false, false, false, true] + uc_child_element: 7_581.72 + +- name: A date of birth given for one person leaves another's age-placed date unchanged. + # The 17-year-old's date of birth (15 September 2009) puts them in the + # route; the 16-year-old elsewhere has none, so age places their birthday + # on 6 April 2010 and they are in it too. + period: 2026 + input: + people: + parent_1: + age: 34 + is_parent: true + young_person_17: + age: 17 + date_of_birth: 20090915 + current_education: NOT_IN_EDUCATION + parent_2: + age: 40 + is_parent: true + young_person_16: + age: 16 + current_education: NOT_IN_EDUCATION + benunits: + benunit_1: + members: [parent_1, young_person_17] + benunit_2: + members: [parent_2, young_person_16] + households: + household_1: + members: [parent_1, young_person_17] + household_2: + members: [parent_2, young_person_16] + output: + is_before_first_september_following_16th_birthday: [false, true, false, true] + is_qualifying_young_person_for_universal_credit: [false, true, false, true] + is_qualifying_young_person_for_pension_credit: [false, true, false, true] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml new file mode 100644 index 0000000000..3fd69d83e7 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/qualifying_young_person_reported_own_awards.yaml @@ -0,0 +1,608 @@ +# UC reg. 5(5) and SPC reg. 4A(5) exclude own awards from both QYP routes. +# The reports identify the recipient whether receives_benefits_in_own_right +# is omitted or explicitly false. SPC reg. 4A(5) also lists income support, +# which UC reg. 5(5) does not: is_on_income_support reads it, from the +# calculated award for a claimant or partner and from their own report for +# anyone else. + +- name: A 16-year-old reporting income-related ESA does not attract child additions for a parent aged 40. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting income-based JSA does not attract child additions for a parent aged 40. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting Universal Credit does not attract child additions for a parent aged 40. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + universal_credit_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting contributory ESA does not attract child additions for a parent aged 40. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + esa_contrib_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting contribution-based JSA does not attract child additions for a parent aged 40. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + jsa_contrib_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting income-related ESA does not attract child additions for a parent aged 70. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting income-based JSA does not attract child additions for a parent aged 70. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting Universal Credit does not attract child additions for a parent aged 70. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + universal_credit_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting contributory ESA does not attract child additions for a parent aged 70. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + esa_contrib_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A 16-year-old reporting contribution-based JSA does not attract child additions for a parent aged 70. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + jsa_contrib_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + uc_individual_child_element: [0, 0] + uc_child_element: 0 + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + child_minimum_guarantee_addition: 0 + +- name: A reported award also excludes the education route. + period: 2026 + input: + age: [18, 18, 18, 18, 18] + current_education: [UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY, UPPER_SECONDARY] + universal_credit_reported: [5_000, 0, 0, 0, 0] + esa_income_reported: [0, 5_000, 0, 0, 0] + esa_contrib_reported: [0, 0, 5_000, 0, 0] + jsa_income_reported: [0, 0, 0, 5_000, 0] + jsa_contrib_reported: [0, 0, 0, 0, 5_000] + output: + is_qualifying_young_person_for_universal_credit: [false, false, false, false, false] + is_qualifying_young_person_for_pension_credit: [false, false, false, false, false] + +- name: An explicit false own-right flag does not erase a reported own ESA award. + period: 2026 + input: + age: 16 + current_education: NOT_IN_EDUCATION + receives_benefits_in_own_right: false + esa_income_reported: 5_000 + output: + is_qualifying_young_person_for_universal_credit: false + is_qualifying_young_person_for_pension_credit: false + +- name: A parent's report does not identify their child as an own-award recipient. + period: 2026 + input: + people: + parent: + age: 40 + is_parent: true + is_benunit_head: true + esa_income_reported: 5_000 + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_qualifying_young_person_for_pension_credit: [false, true] + is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true + +- name: Zero reports leave the no-education route available. + period: 2026 + input: + age: 16 + current_education: NOT_IN_EDUCATION + universal_credit_reported: 0 + esa_income_reported: 0 + esa_contrib_reported: 0 + jsa_income_reported: 0 + jsa_contrib_reported: 0 + output: + is_qualifying_young_person_for_universal_credit: true + is_qualifying_young_person_for_pension_credit: true + +- name: Contribution-based JSA excludes UC and PC QYPs without changing Child Benefit's distinct rule. + # Child Benefit (General) Regulations 2006 reg. 8 lists income-based JSA, + # but UC reg. 5(5) and SPC reg. 4A(5) also exclude contribution-based JSA. + period: 2026 + input: + age: 18 + current_education: UPPER_SECONDARY + jsa_contrib_reported: 5_000 + output: + is_qualifying_young_person_for_universal_credit: false + is_qualifying_young_person_for_pension_credit: false + is_qualifying_young_person_for_child_benefit: true + +- name: A 16-year-old reporting their own Income Support does not attract the Pension Credit child addition, but stays a UC young person. + # SPC reg. 4A(5) lists income support; UC reg. 5(5) does not. Income + # Support is in payment in 2025 (gov.dwp.income_support.active). + period: 2025 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + receives_benefits_in_own_right: false + income_support_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + rent: 0 + savings: 0 + output: + is_before_first_september_following_16th_birthday: [false, true] + is_on_income_support: [false, true] + is_qualifying_young_person_for_pension_credit: [false, false] + is_child_or_qualifying_young_person_for_pension_credit: [false, false] + child_minimum_guarantee_addition: 0 + is_qualifying_young_person_for_universal_credit: [false, true] + +- name: A member's own reported Income Support excludes the Pension Credit education route only. + period: 2025 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 18 + is_benunit_head: false + current_education: UPPER_SECONDARY + income_support_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + output: + is_on_income_support: [false, true] + is_qualifying_young_person_for_pension_credit: [false, false] + is_qualifying_young_person_for_universal_credit: [false, true] + +- name: A member's Income Support report leaves the Pension Credit route available once Income Support stops paying. + # Income Support stops paying in 2026 (gov.dwp.income_support.active), so a + # report then is no award in payment. + period: 2026 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + income_support_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + output: + is_on_income_support: [false, false] + is_qualifying_young_person_for_pension_credit: [false, true] + is_qualifying_young_person_for_universal_credit: [false, true] + +- name: A claimant's calculated Income Support excludes them from the Pension Credit status. + period: 2025 + input: + age: 17 + is_carer_for_benefits: true + current_education: UPPER_SECONDARY + income_support_reported: 1 + output: + income_support: 3_790.80 + is_on_income_support: true + is_qualifying_young_person_for_pension_credit: false + is_qualifying_young_person_for_universal_credit: true + +- name: A claimant's report without a calculated award leaves the Pension Credit status available. + # The claimant falls within no prescribed category, so the model calculates + # no Income Support for them. + period: 2025 + input: + age: 17 + current_education: UPPER_SECONDARY + income_support_reported: 1 + output: + income_support: 0 + is_on_income_support: false + is_qualifying_young_person_for_pension_credit: true + is_qualifying_young_person_for_universal_credit: true + +- name: The partner of an Income Support claimant keeps the Pension Credit status. + # A couple's award goes to the claimant: the partner who reports nothing is + # not receiving it in their own right. + period: 2025 + input: + people: + claimant: + age: 25 + is_parent: true + is_benunit_head: true + is_carer_for_benefits: true + income_support_reported: 1 + partner: + age: 17 + is_parent: true + is_benunit_head: false + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + is_on_income_support: [true, false] + is_qualifying_young_person_for_pension_credit: [false, true] + +- name: A report of one penny of a listed award other than Income Support excludes the young person. + period: 2025 + input: + age: [16, 16, 16, 16, 16] + current_education: [NOT_IN_EDUCATION, NOT_IN_EDUCATION, NOT_IN_EDUCATION, NOT_IN_EDUCATION, NOT_IN_EDUCATION] + universal_credit_reported: [0.01, 0, 0, 0, 0] + esa_income_reported: [0, 0.01, 0, 0, 0] + esa_contrib_reported: [0, 0, 0.01, 0, 0] + jsa_income_reported: [0, 0, 0, 0.01, 0] + jsa_contrib_reported: [0, 0, 0, 0, 0.01] + output: + is_qualifying_young_person_for_universal_credit: [false, false, false, false, false] + is_qualifying_young_person_for_pension_credit: [false, false, false, false, false] + +- name: A member's report of one penny of Income Support excludes them from the Pension Credit status alone. + period: 2025 + input: + people: + parent: + age: 70 + is_parent: true + is_benunit_head: true + young_person: + age: 16 + is_benunit_head: false + current_education: NOT_IN_EDUCATION + income_support_reported: 0.01 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + country: ENGLAND + output: + is_on_income_support: [false, true] + is_qualifying_young_person_for_pension_credit: [false, false] + is_qualifying_young_person_for_universal_credit: [false, true] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml index a9d6e1605e..ce709391d1 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/means_test_income_members.yaml @@ -47,7 +47,7 @@ output: universal_credit: 9_321.36 -- name: A 17-year-old outside education is not a UC young person, so their pay does not count +- name: A 17-year-old outside education and past the 1 September after their 16th birthday is not a UC young person, so their pay does not count period: 2026 absolute_error_margin: 1 input: diff --git a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py index f65edc3e71..076d8ffa2c 100644 --- a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py @@ -54,7 +54,8 @@ earnings exception the claimants (UC Regs 2013 reg 82(1)(a)), so the draws give the member State Pension age or earnings at times. 2. Each scheme's exemption equals its own reference written directly from - the regulations, with roles fixed by construction (differential test). + the regulations, with claimant roles fixed and young-person status + following each scheme's own exclusions (differential test). The union of the two equals the former shared reference. 3. A family exempt under a scheme has no cap and no reduction to that scheme's award; any other family has that scheme's statutory cap (UC reg @@ -123,6 +124,13 @@ "incapacity_benefit": "incapacity_benefit_reported", "sda": "sda_reported", } +OWN_RIGHT_REPORTS = ( + "universal_credit_reported", + "esa_contrib_reported", + "esa_income_reported", + "jsa_contrib_reported", + "jsa_income_reported", +) CIRCUMSTANCES = AMOUNTS + [ "esa_contrib", "esa_income", @@ -269,6 +277,11 @@ def own_esa(m): return m["esa_contrib_reported"] > 0 or m["esa_income_reported"] > 0 +def own_benefits(m): + # UC reg 5(5): a personal report identifies an award in the person's own right. + return any(m.get(report, 0) > 0 for report in OWN_RIGHT_REPORTS) + + def reference_specified_benefit(members, benunit_inputs, scheme): """UC Regs 2013 reg 83(1) or HB Regs 2006 reg 75F(1), read directly, with roles known by construction.""" @@ -279,7 +292,16 @@ def reference_specified_benefit(members, benunit_inputs, scheme): # school-leaver role here has no Child Benefit education or terminal-date # circumstance, so it is not one. young_roles = ("young_person", "sixteen") if scheme == "uc" else ("young_person",) - young_persons = [m for m in members if m["_role"] in young_roles] + # The UC predicate supplements its qualifying-young-person test with a + # separate age-16 approximation, which reads only the explicit own-right + # flag. Reported awards remove older UC young persons, but do not change + # that approximation or the Housing Benefit test. + young_persons = [ + m + for m in members + if m["_role"] in young_roles + and (scheme != "uc" or m["age"] == 16 or not own_benefits(m)) + ] children = [m for m in members if m["_role"] == "child"] # No capital, so the couple's income-related award is their reports, # unless the family claims Universal Credit: the claim ends it (SI @@ -380,16 +402,22 @@ def reference_union(members, benunit_inputs): ) -def reference_cap(members): +def reference_cap(members, scheme): # Each scheme's rate: a single claimant (UC reg 80A(2)(c)) or a claimant # who neither has a partner nor is a lone parent (HB regs 2(1), 75CA(2)), # with no child or young person. The family-rate helper counts any # 16-year-old for the year (UC reg 5(1)(a); for HB, Child Benefit's # school-leaver terminal dates, which annual ages cannot place); it does # not yet apply reg 5(5) to one receiving benefits in their own right. + # Older UC young persons cease to count when they report their own award; + # Housing Benefit continues to read the existing Child Benefit test. claimants = sum(m["_role"] == "claimant" for m in members) dependants = any( - m["_role"] in ("child", "young_person", "sixteen", "sixteen_own_right") + m["_role"] in ("child", "sixteen", "sixteen_own_right") + or ( + m["_role"] == "young_person" + and (scheme != "uc" or m["age"] == 16 or not own_benefits(m)) + ) for m in members ) return SINGLE_CAP if claimants == 1 and not dependants else FAMILY_CAP @@ -530,7 +558,7 @@ def test_exempt_families_have_no_cap_and_others_the_statutory_cap(drawn): if exempt[i]: assert np.isinf(cap[i]) and reduction[i] == 0, (scheme, units[i]) else: - assert cap[i] == reference_cap(members), (scheme, units[i]) + assert cap[i] == reference_cap(members, scheme), (scheme, units[i]) excess = max(total[part][i] - cap[i], 0) # UC reg 81: the excess less the childcare element (nil # here). HB reg 75D(2): no more than leaves reg 75's 50p a diff --git a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py index bc6ee58fbf..34431e6fab 100644 --- a/policyengine_uk/tests/test_child_and_adult_definitions_properties.py +++ b/policyengine_uk/tests/test_child_and_adult_definitions_properties.py @@ -44,7 +44,14 @@ claimant's child either way (the any-age gap). Before/after families share one Simulation to avoid repeated model construction, but occupy distinct households and benefit units. -5. Supplied claimant/partner role vectors, valid or not, are returned +5. UC reg 5(1)(a) and SPC reg 4A(1)(a): the year counts in the route when it + starts (6 April) before the 1 September following the 16th birthday, for + every date of birth checked against calendar arithmetic. Within ages 16 to + 19 and without own-right benefits, that route alone makes a UC or PC + qualifying young person whatever the education inputs; without it the + education route is unchanged. Own-right benefits exclude both routes. UC + and PC agree below 19, where only UC's terminal date differs. +6. Supplied claimant/partner role vectors, valid or not, are returned unchanged on any benefit unit. Widening an unflagged member's gap below the claimant, with the claimant, flags, other members and HBAI adulthood fixed, never makes a member the presumption excludes the partner. @@ -55,8 +62,10 @@ Scope: these are tests of the ENCODED statutory limbs, not full entitlement. Education status proxies full-time non-advanced study: hours, employment contracts, approved providers and interruptions are unobserved. CB/CTC leaver -extensions are omitted, as is UC/PC's unconditional age-16-to-next-September -route. UC's terminal date is an input; PC's September terminal date is absent. +extensions are omitted. UC/PC's route to the 1 September after the 16th +birthday, which has no education condition, counts for a year that starts +before that date (invariant 5). UC's terminal date is an input; PC's September +terminal date is absent. Own-benefit receipt collapses different programme-specific benefit lists. UC/CTC/PC blanket looked-after exclusions omit statutory exceptions. HBAI's fallback assumes dependence at 16--17 regardless of study, requires an @@ -70,11 +79,12 @@ the independent scalar references below. No network or survey data is used. """ +from datetime import date, timedelta from itertools import product from unittest.mock import patch import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, assume, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -190,8 +200,17 @@ def statutory_child(age): return age < 16 +def in_route_to_first_september_after_16th_birthday(p): + # UC reg 5(1)(a) and SPC reg 4A(1)(a) have no education condition. With no + # date of birth, a whole age on 6 October places the birthday on 6 April, + # so the year (from 6 April) starts before the 1 September following the + # 16th birthday exactly when that age is under 17. + return p.get("is_before_first_september_following_16th_birthday", p["age"] < 17) + + def qualifying_young_person(p, programme): - # Education/training routes only (see omissions in module docstring): + # CB and CTC: education/training routes only (see omissions in module + # docstring). UC and PC: also the route with no education condition. # CB: https://www.legislation.gov.uk/uksi/2006/223/regulation/3 # CB own-benefit restriction: https://www.legislation.gov.uk/uksi/2006/223/regulation/8 # UC: https://www.legislation.gov.uk/uksi/2013/376/regulation/5 @@ -199,6 +218,11 @@ def qualifying_young_person(p, programme): # PC: https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A if not 16 <= p["age"] < 20 or p["receives_benefits_in_own_right"]: return False + if programme in { + "universal_credit", + "pension_credit", + } and in_route_to_first_september_after_16th_birthday(p): + return True if not (non_advanced_education(p) or p["is_in_approved_training"]): return False if ( @@ -995,6 +1019,111 @@ def test_unflagged_adult_pairs_follow_the_20_year_gap(): assert expected == [c for f in families for c in claimants_or_partners(f)] +ROUTE = "is_before_first_september_following_16th_birthday" + + +def first_september_following_16th_birthday(born): + # Calendar arithmetic, independent of the model's month grid: a 16th + # birthday before 1 September is followed by 1 September of the same year, + # and one on or after it by the next year's. A 29 February birthday is + # before 1 September whichever day it is kept on in other years. + # https://www.legislation.gov.uk/uksi/2013/376/regulation/5 + # https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A + return date(born.year + 16 + ((born.month, born.day) >= (9, 1)), 9, 1) + + +def age_on_6_october(born, year): + return year - born.year - ((10, 6) < (born.month, born.day)) + + +def assert_route_matches_calendar(births, year): + families = [ + [person(age_on_6_october(born, year), date_of_birth=int(f"{born:%Y%m%d}"))] + for born in births + ] + # The fiscal year starts on 6 April. + expected = [ + date(year, 4, 6) < first_september_following_16th_birthday(born) + for born in births + ] + assert_values(simulate(families, year), ROUTE, expected, year) + + +def test_route_year_starts_before_first_september_for_every_birthday(): + # Every date of birth from 2008 to 2011: ages 14 to 18 on 6 October 2026, + # including the 31 August / 1 September and 6 April / 6 October edges. + start, end = date(2008, 1, 1), date(2012, 1, 1) + births = [start + timedelta(days=d) for d in range((end - start).days)] + assert_route_matches_calendar(births, YEAR) + + +@PROPERTY_SETTINGS +@given( + st.lists(st.dates(date(1990, 1, 1), date(2031, 12, 31)), min_size=1, max_size=30), + st.integers(2013, 2031), +) +def test_route_matches_calendar_arithmetic_in_any_year(births, year): + births = [born for born in births if age_on_6_october(born, year) >= 0] + assume(births) + assert_route_matches_calendar(births, year) + + +def test_route_alone_qualifies_and_leaves_the_education_route_unchanged(): + # Ages 14 to 21, every education value, own-right benefits, approved + # training, apprenticeship and looked-after status, with the route input + # off then on (innermost, so each subject's two rows are adjacent). + rows = list( + product( + range(14, 22), + EDUCATIONS, + product((False, True), repeat=4), + (False, True), + ) + ) + families = [ + [ + person(40, is_parent=True, is_benunit_head=True, **{ROUTE: False}), + person( + age, + current_education=education, + receives_benefits_in_own_right=own_benefits, + is_in_approved_training=training, + is_apprentice=apprentice, + is_looked_after_by_local_authority=looked_after, + **{ROUTE: route}, + ), + ] + for age, education, flags, route in rows + for own_benefits, training, apprentice, looked_after in [flags] + ] + # The oracle: the route, else the education route as before. + assert_definitions(families) + sim = simulate(families) + age = np.array([row[0] for row in rows[::2]]) + own_benefits = np.array([row[2][0] for row in rows[::2]]) + qyp = { + programme: sim.calculate(f"is_qualifying_young_person_for_{programme}", YEAR)[ + 1::2 + ].reshape(-1, 2) + for programme in ("universal_credit", "pension_credit") + } + for programme, status in qyp.items(): + # Switching the route on never removes the status. + assert (status[:, 1] >= status[:, 0]).all(), programme + # With the route on, ages 16 to 19 without own-right benefits qualify, + # whatever the education inputs, and nobody else does. + np.testing.assert_array_equal( + status[:, 1], (16 <= age) & (age < 20) & ~own_benefits, err_msg=programme + ) + # Own-right benefits exclude both routes. + assert not status[own_benefits].any(), programme + # Below 19 the two programmes' definitions are the same. + below_19 = age < 19 + np.testing.assert_array_equal( + qyp["universal_credit"][below_19], qyp["pension_credit"][below_19] + ) + + def test_wide_gap_couples_by_head_order_and_with_supplied_roles(): # An unflagged couple 25 years apart: with the elder as head (the claimant) # the younger is presumed their child; with the younger as head the elder diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py index f82151672d..9fa4db56d9 100644 --- a/policyengine_uk/tests/test_legacy_award_readers_properties.py +++ b/policyengine_uk/tests/test_legacy_award_readers_properties.py @@ -87,6 +87,7 @@ """ import numpy as np +import pytest from hypothesis import HealthCheck, event, example, given, settings from hypothesis import strategies as st @@ -238,13 +239,15 @@ def families(draw): def other_members(draw): """An adult who is neither claimant, partner nor a dependant. - They are under state pension age and not in education, so a 16 to 19 year - old is not a qualifying young person. They may work, and have no income - other than their pay and the awards they report. + They are under state pension age, not in education and past the 1 + September following their 16th birthday (UC Regs 2013 reg. 5(1)(a)), so a + 16 to 19 year old is not a qualifying young person. They may work, and have + no income other than their pay and the awards they report. """ return { "age": draw(st.integers(16, 60)), "current_education": "NOT_IN_EDUCATION", + "is_before_first_september_following_16th_birthday": False, **draw(work()), **draw(award_reports()), } @@ -539,6 +542,7 @@ def other_members_with_awards(draw): return { "age": draw(st.integers(16, 60)), "current_education": "NOT_IN_EDUCATION", + "is_before_first_september_following_16th_birthday": False, **awards, } @@ -653,6 +657,7 @@ def _family_readers_never_change(drawn, enter_is_mixed_age_couple=False): values[starts[i] : starts[i] + n], values[starts[k + i] : starts[k + i] + n], ), (variable, drawn[i]) + return sim @SETTINGS @@ -679,6 +684,44 @@ def test_other_members_awards_never_change_the_readers_for_young_singles(drawn): _family_readers_never_change(drawn) +@pytest.mark.parametrize("report", ["esa_income_reported", "jsa_income_reported"]) +def test_sixteen_year_old_own_award_leaves_young_single_readers_unchanged(report): + # A natural route-(a) case: no route or own-right Boolean is supplied. + # UC reg 5(5) and SPC reg 4A(5) still exclude the member's own award. + family = ( + [ + { + "age": 18, + "is_benunit_head": True, + "current_education": "NOT_IN_EDUCATION", + **NO_AWARDS, + } + ], + [], + {"country": "SCOTLAND", "savings": 0}, + ) + other = outside_adult(16, is_benunit_head=False, **{report: 5_000}) + sim = _family_readers_never_change([(family, other)]) + assert sim.calculate("is_before_first_september_following_16th_birthday", YEAR)[-1] + assert not sim.calculate("receives_benefits_in_own_right", YEAR).any() + for programme in ("universal_credit", "pension_credit"): + assert not sim.calculate(f"is_qualifying_young_person_for_{programme}", YEAR)[ + -1 + ] + assert not sim.calculate( + f"is_child_or_qualifying_young_person_for_{programme}", YEAR + )[-1] + for variable in ( + "uc_child_element", + "is_uc_work_allowance_eligible", + "uc_work_allowance", + ): + assert not sim.calculate(variable, YEAR).any(), variable + for variable in ("uc_earned_income", "uc_unearned_income"): + values = sim.calculate(variable, YEAR) + assert np.isclose(values[0], values[1], atol=0.01), variable + + @SETTINGS @given( st.lists( diff --git a/policyengine_uk/tests/test_means_test_income_properties.py b/policyengine_uk/tests/test_means_test_income_properties.py index c03ab5f698..b6a102cbc0 100644 --- a/policyengine_uk/tests/test_means_test_income_properties.py +++ b/policyengine_uk/tests/test_means_test_income_properties.py @@ -5,8 +5,10 @@ 1. For any benefit unit, giving income to a member who is neither the claimant, the partner, nor the programme's own child or young person (for example an 18- or 19-year-old presumed the claimant's child but not in - education, or a third adult) never changes a means test's income. The UC, - HB, IS, tax credit, CTR and Pension Credit income totals are covered. + education, a 16-year-old outside education and past the 1 September + following their 16th birthday, or a third adult) never changes a means + test's income. The UC, HB, IS, tax credit, CTR and Pension Credit income + totals are covered. 2. Giving income to the family's own child or young person never changes it either (HB Regs 2006 reg 25(3); IS Regs 1987 reg 23(2); Tax Credits (Definition and Calculation of Income) Regs 2002 reg 3(1); SSI 2021/249 reg @@ -54,9 +56,10 @@ def families(draw): # member who by construction is # outside every programme's family: a 16-19-year-old at least 16 years # younger than the head (presumed the head's child) who is not in - # non-advanced education, so no programme's young person; or a third adult - # no older than the partner (the partner, earlier in member order, wins any - # age tie). + # non-advanced education and is past the 1 September following their 16th + # birthday (UC Regs 2013 reg. 5(1)(a)), so no programme's young person; or + # a third adult no older than the partner (the partner, earlier in member + # order, wins any age tie). kind = draw(st.sampled_from(["presumed_child", "third_adult"])) if kind == "presumed_child": other_age = draw(st.integers(16, 19)) @@ -81,6 +84,7 @@ def families(draw): { "age": other_age, "current_education": education, + "is_before_first_september_following_16th_birthday": False, "employment_income": draw(st.floats(1, 50_000)), "private_pension_income": draw(money), "savings_interest_income": draw(money), diff --git a/policyengine_uk/tests/test_pension_credit_young_person_income_support.py b/policyengine_uk/tests/test_pension_credit_young_person_income_support.py new file mode 100644 index 0000000000..18e2464fd6 --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_young_person_income_support.py @@ -0,0 +1,278 @@ +"""Pension Credit's young person and their Income Support (SPC reg. 4A(5)). + +A 16-year-old outside education, in a 70-year-old's benefit unit, reports +their own Income Support in 2025, when it is in payment. They are not the +claimant's qualifying young person, so the claimant gets no child addition, +until a reform removes Income Support. UC reg. 5(5) does not list income +support, so they stay a UC qualifying young person throughout. + +A young carer who claims Income Support in their own benefit unit follows +the calculated award. While they receive it they are no qualifying young +person, so they count as another resident and end the severe disability +addition of a pensioner they live with (Sch. I para. 2(2)(f)). A reform that +ends the award restores that addition however it ends it. +""" + +from itertools import product + +import numpy as np +import pytest +from policyengine_core.model_api import YEAR as YEAR_PERIOD +from policyengine_core.reforms import Reform +from policyengine_core.variables import Variable + +from policyengine_uk import Simulation +from policyengine_uk.entities import BenUnit + +YEAR = 2025 + + +class abolish_income_support(Reform): + def apply(self): + self.neutralize_variable("income_support") + + +class income_support(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR_PERIOD + + def formula(benunit, period, parameters): + return benunit.filled_array(0) + + +class zero_income_support(Reform): + def apply(self): + self.update_variable(income_support) + + +def simulation(reform=None): + young_person = { + "age": 16, + "is_benunit_head": False, + "current_education": "NOT_IN_EDUCATION", + "income_support_reported": 3_000, + } + parent = {"age": 70, "is_parent": True, "is_benunit_head": True} + return Simulation( + situation={ + "people": { + name: {key: {YEAR: value} for key, value in inputs.items()} + for name, inputs in ( + ("parent", parent), + ("young_person", young_person), + ) + }, + "benunits": {"benunit": {"members": ["parent", "young_person"]}}, + "households": { + "household": { + "members": ["parent", "young_person"], + "country": {YEAR: "ENGLAND"}, + "rent": {YEAR: 0}, + "savings": {YEAR: 0}, + } + }, + }, + reform=reform, + ) + + +def inputs_for(year, values): + return {key: {year: value} for key, value in values.items()} + + +def young_carer_and_pensioner(reform=None, year=2024): + young = { + "age": 19, + "date_of_birth": 20051002, + "is_benunit_head": True, + "is_claimant_or_partner": True, + "is_carer_for_benefits": True, + "care_hours": 35, + "would_claim_carers_allowance": False, + "current_education": "UPPER_SECONDARY", + "age_started_or_accepted_current_education_or_training": 17, + "income_support_reported": 1, + } + pensioner = { + "age": 70, + "is_benunit_head": True, + "is_claimant_or_partner": True, + "would_claim_carers_allowance": False, + "attendance_allowance": 1_000, + } + return Simulation( + situation={ + "people": { + "young": inputs_for(year, young), + "pensioner": inputs_for(year, pensioner), + }, + "benunits": { + "young_unit": { + "members": ["young"], + **inputs_for( + year, {"would_claim_IS": True, "would_claim_uc": False} + ), + }, + "pensioner_unit": { + "members": ["pensioner"], + **inputs_for( + year, {"would_claim_pc": True, "would_claim_uc": False} + ), + }, + }, + "households": { + "home": { + "members": ["young", "pensioner"], + **inputs_for(year, {"country": "ENGLAND", "rent": 0, "savings": 0}), + } + }, + }, + reform=reform, + ) + + +def grid(year, reform=None): + """A young person of each kind, with a pensioner in the same household.""" + people, benunits, households = {}, {}, {} + cases = product( + (16, 17, 18, 19), + ("NOT_IN_EDUCATION", "UPPER_SECONDARY"), + (0, 0.01, 3_000), + (False, True), + (False, True), + ) + for index, (age, education, report, carer, shares_a_unit) in enumerate(cases): + young, elder = f"young_{index}", f"elder_{index}" + people[young] = inputs_for( + year, + { + "age": age, + "current_education": education, + "income_support_reported": report, + "is_carer_for_benefits": carer, + "is_benunit_head": not shares_a_unit, + }, + ) + people[elder] = inputs_for( + year, {"age": 70, "is_parent": True, "is_benunit_head": True} + ) + units = [[elder, young]] if shares_a_unit else [[elder], [young]] + for number, members in enumerate(units): + benunits[f"unit_{index}_{number}"] = {"members": members} + households[f"household_{index}"] = { + "members": [elder, young], + **inputs_for(year, {"country": "ENGLAND", "rent": 0, "savings": 0}), + } + return Simulation( + situation={ + "people": people, + "benunits": benunits, + "households": households, + }, + reform=reform, + ) + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +def test_own_income_support_ends_the_young_person_until_a_reform_removes_it(): + kept = simulation() + removed = simulation(abolish_income_support) + assert kept.calculate("is_on_income_support", YEAR).tolist() == [False, True] + assert not removed.calculate("is_on_income_support", YEAR).any() + assert kept.calculate( + "is_qualifying_young_person_for_pension_credit", YEAR + ).tolist() == [False, False] + assert removed.calculate( + "is_qualifying_young_person_for_pension_credit", YEAR + ).tolist() == [False, True] + assert kept.calculate("child_minimum_guarantee_addition", YEAR).tolist() == [0] + assert removed.calculate("child_minimum_guarantee_addition", YEAR)[0] > 0 + for sim in (kept, removed): + assert sim.calculate( + "is_qualifying_young_person_for_universal_credit", YEAR + ).tolist() == [False, True] + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +@pytest.mark.parametrize("ending", [zero_income_support, abolish_income_support]) +def test_a_reform_ending_a_young_carers_award_restores_a_pensioners_addition(ending): + year = 2024 + kept = young_carer_and_pensioner() + ended = young_carer_and_pensioner(ending) + assert kept.calculate("income_support", year)[0] > 0 + assert ended.calculate("income_support", year)[0] == 0 + assert kept.calculate( + "is_qualifying_young_person_for_pension_credit", year + ).tolist() == [False, False] + assert ended.calculate( + "is_qualifying_young_person_for_pension_credit", year + ).tolist() == [True, False] + addition = ended.calculate("severe_disability_minimum_guarantee_addition", year)[1] + assert kept.calculate("severe_disability_minimum_guarantee_addition", year)[1] == 0 + assert addition > 0 + assert ended.calculate("pension_credit", year)[1] == pytest.approx( + kept.calculate("pension_credit", year)[1] + addition + ) + + +@pytest.mark.parametrize( + "first, second", + [ + ("is_qualifying_young_person_for_pension_credit", "income_support"), + ("income_support", "is_qualifying_young_person_for_pension_credit"), + ("pension_credit", "is_on_income_support"), + ], +) +def test_pension_credit_status_and_income_support_calculate_in_either_order( + first, second +): + reference = young_carer_and_pensioner() + sim = young_carer_and_pensioner() + sim.calculate(first, 2024) + for variable in (first, second): + assert sim.calculate(variable, 2024).tolist() == ( + reference.calculate(variable, 2024).tolist() + ) + + +def test_labour_supply_responses_calculate_without_recursing_through_the_status(): + year = 2024 + sim = young_carer_and_pensioner( + { + "gov.simulation.labour_supply_responses.income_elasticity": { + str(year): -0.05 + }, + "gov.simulation.labour_supply_responses.substitution_elasticity": { + str(year): 0.15 + }, + } + ) + assert sim.calculate( + "is_qualifying_young_person_for_pension_credit", year + ).tolist() == [False, False] + assert sim.calculate("pension_credit", year)[1] > 0 + + +@pytest.mark.parametrize("year", [2025, 2026]) +def test_nobody_on_income_support_is_a_pension_credit_young_person(year): + sim = grid(year) + on_income_support = sim.calculate("is_on_income_support", year) + young_person = sim.calculate("is_qualifying_young_person_for_pension_credit", year) + assert not np.any(on_income_support & young_person) + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +def test_zeroing_or_neutralizing_income_support_gives_claimants_the_same_status(): + year = 2025 + zeroed = grid(year, zero_income_support) + neutralized = grid(year, abolish_income_support) + claimant = zeroed.calculate("is_claimant_or_partner", year) + for variable in ( + "is_on_income_support", + "is_qualifying_young_person_for_pension_credit", + ): + assert ( + zeroed.calculate(variable, year)[claimant].tolist() + == neutralized.calculate(variable, year)[claimant].tolist() + ) diff --git a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py index 532c00fac1..f7181fe776 100644 --- a/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py +++ b/policyengine_uk/variables/gov/dwp/is_responsible_for_child_or_young_person_for_uc_or_housing_benefit.py @@ -20,11 +20,12 @@ class is_responsible_for_child_or_young_person_for_uc_or_housing_benefit(Variabl cap rates, where a non-dependant does not matter, read each scheme's own responsibility test. - UC reg 5(1)(a) also makes every 16-year-old a qualifying young person until - the 1 September after their 16th birthday, whatever their education. Annual - ages cannot place that date, so a 16-year-old in the benefit unit who is not - a claimant or partner counts here; the UC child element does not yet model - reg 5(1)(a). + The UC test includes reg 5(1)(a), which makes a 16-year-old a qualifying + young person until the 1 September after their 16th birthday, whatever + their education. Housing Benefit's young person follows Child Benefit, + whose school-leaver rules (Child Benefit (General) Regulations 2006 regs 4, + 5 and 7) are not modelled, so a 16-year-old in the benefit unit who is not + a claimant or partner also counts here for the year. """ value_type = bool diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_qualifying_young_person_for_pension_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_qualifying_young_person_for_pension_credit.py index ee1d028e8d..125702ebd4 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_qualifying_young_person_for_pension_credit.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee/additional/is_qualifying_young_person_for_pension_credit.py @@ -5,22 +5,55 @@ class is_qualifying_young_person_for_pension_credit(Variable): value_type = bool entity = Person label = "Qualifying young person for Pension Credit" + documentation = ( + "A person aged 16 to 19 who is either before the 1 September following " + "their 16th birthday, whatever their education (reg. 4A(1)(a)), or in " + "non-advanced education or approved training that meets the entry " + "condition (reg. 4A(1)(b), (2) and (3)), and who is not receiving " + "Universal Credit, Employment and Support Allowance, Jobseeker's " + "Allowance or Income Support in their own right (reg. 4A(5)). The " + "own-right flag or their own reported award shows the first three; " + "is_on_income_support shows Income Support. The model does not apply " + "reg. 4A(1)(b)'s end at the 1 September after the 19th birthday." + ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A" def formula(person, period, parameters): + # Reg. 4A(1)(a): no education or training condition. + before_first_september_following_16th_birthday = person( + "is_before_first_september_following_16th_birthday", period + ) + # Reg. 4A(1)(b), (2) and (3). + in_education_or_training = person( + "meets_qualifying_young_person_education_or_training_condition_for_pension_credit", + period, + ) & person( + "meets_qualifying_young_person_entry_condition_for_pension_credit", + period, + ) + # Reg. 4A(5) applies to both routes. Raw person-level reports identify + # own awards without calculating family benefits, which depend on QYP + # status and could recurse. Unlike UC reg. 5(5), it lists income + # support, whose family follows the Child Benefit test and not this + # one, so is_on_income_support can read the calculated award. + receives_own_benefits = ( + person("receives_benefits_in_own_right", period) + | (person("universal_credit_reported", period) > 0) + | (person("esa_income_reported", period) > 0) + | (person("esa_contrib_reported", period) > 0) + | (person("jsa_income_reported", period) > 0) + | (person("jsa_contrib_reported", period) > 0) + | person("is_on_income_support", period) + ) return ( person( "meets_qualifying_young_person_age_condition_for_pension_credit", period, ) - & person( - "meets_qualifying_young_person_education_or_training_condition_for_pension_credit", - period, - ) - & person( - "meets_qualifying_young_person_entry_condition_for_pension_credit", - period, + & ( + before_first_september_following_16th_birthday + | in_education_or_training ) - & ~person("receives_benefits_in_own_right", period) + & ~receives_own_benefits ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py index 2b1608d539..4281403300 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/is_qualifying_young_person_for_universal_credit.py @@ -5,16 +5,25 @@ class is_qualifying_young_person_for_universal_credit(Variable): value_type = bool entity = Person label = "Qualifying young person for Universal Credit" + documentation = ( + "A person aged 16 to 19 who is either before the 1 September following " + "their 16th birthday, whatever their education (reg. 5(1)(a)), or in " + "non-advanced education or approved training that meets the entry and " + "terminal-date conditions (reg. 5(1)(b), (2) and (3)), and who is not " + "receiving Universal Credit, Employment and Support Allowance or " + "Jobseeker's Allowance (reg. 5(5))." + ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/5" def formula(person, period, parameters): - return ( + # Reg. 5(1)(a): no education or training condition. + before_first_september_following_16th_birthday = person( + "is_before_first_september_following_16th_birthday", period + ) + # Reg. 5(1)(b), (2) and (3). + in_education_or_training = ( person( - "meets_qualifying_young_person_age_condition_for_universal_credit", - period, - ) - & person( "meets_qualifying_young_person_education_or_training_condition_for_universal_credit", period, ) @@ -26,5 +35,26 @@ def formula(person, period, parameters): "meets_qualifying_young_person_terminal_date_condition_for_universal_credit", period, ) - & ~person("receives_benefits_in_own_right", period) + ) + # The exclusions apply to both routes (UC reg. 5(5); SPC reg. 4A(5)). + # Raw person-level reports identify own awards without calculating + # family benefits, which depend on QYP status and could recurse. + receives_own_benefits = ( + person("receives_benefits_in_own_right", period) + | (person("universal_credit_reported", period) > 0) + | (person("esa_income_reported", period) > 0) + | (person("esa_contrib_reported", period) > 0) + | (person("jsa_income_reported", period) > 0) + | (person("jsa_contrib_reported", period) > 0) + ) + return ( + person( + "meets_qualifying_young_person_age_condition_for_universal_credit", + period, + ) + & ( + before_first_september_following_16th_birthday + | in_education_or_training + ) + & ~receives_own_benefits ) diff --git a/policyengine_uk/variables/household/demographic/is_before_first_september_following_16th_birthday.py b/policyengine_uk/variables/household/demographic/is_before_first_september_following_16th_birthday.py new file mode 100644 index 0000000000..c1358f9e53 --- /dev/null +++ b/policyengine_uk/variables/household/demographic/is_before_first_september_following_16th_birthday.py @@ -0,0 +1,54 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.dates import birth_day + + +# UC Regs 2013 reg. 5(1)(a); SPC Regs 2002 reg. 4A(1)(a). +NO_EDUCATION_ROUTE_BIRTHDAY_AGE = 16 + + +class is_before_first_september_following_16th_birthday(Variable): + value_type = bool + entity = Person + label = ( + "Before the 1 September following the 16th birthday at the start of the year" + ) + documentation = ( + "Whether the fiscal year starts (6 April) before the 1 September " + "following the person's 16th birthday, so that some of the year falls " + "in the qualifying young person route that has no education condition " + "(UC Regs 2013 reg. 5(1)(a); SPC Regs 2002 reg. 4A(1)(a)). The date " + "comes from date_of_birth, or from age and months_since_last_birthday. " + "A 16th birthday on 31 August ends the route that day, and one on 1 " + "September runs it to the 17th birthday. The route ends on a fixed 1 " + "September that annual periods cannot split, so a person in it for " + "part of the year counts for the year, as the benefit cap and LHA " + "family rules count a 16-year-old member. This departs from the 6 " + "October reading of annual status that is_SP_age uses. Everyone aged " + "16 on 6 October qualifies, as does a 17-year-old born from 1 " + "September to 6 October. With birthdays spread evenly, a 16-year-old " + "outside education is a child or in the route for 46% of the year on " + "average, and a 17-year-old for 4%. To place the route for one " + "person, set their date_of_birth. Setting this variable for some " + "people gives everyone else in the situation false, so set it for " + "every person or not at all." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/4A", + ) + + def formula(person, period, parameters): + date_of_birth = birth_day(person, period) + # The statutory birthday falls in the birth year plus its age; the 1 September + # following it is in that year if the birthday is before 1 September, + # and otherwise in the next. + born_on_or_after_first_september = date_of_birth % 10_000 >= 901 + year_of_first_september = ( + date_of_birth // 10_000 + + NO_EDUCATION_ROUTE_BIRTHDAY_AGE + + born_on_or_after_first_september + ) + # The year starts on 6 April, before 1 September of the same calendar + # year and after 1 September of the one before. + return year_of_first_september >= period.start.year