diff --git a/changelog.d/uc-reg3-3-single-claim.fixed.md b/changelog.d/uc-reg3-3-single-claim.fixed.md new file mode 100644 index 0000000000..65ca8b36da --- /dev/null +++ b/changelog.d/uc-reg3-3-single-claim.fixed.md @@ -0,0 +1 @@ +Universal Credit for a member of a couple who claims as a single person (UC Regs 2013 reg. 3(3)): the single-claimant standard allowance at the claimant's own age, no LCWRA or carer element for the other member, the single-claimant benefit cap and the shared accommodation rate, while the couple's capital and income still count. The other member's own disability and other personal benefits no longer lift the benefit cap. In the childcare work condition the claimant must meet the paid work limb and the other member the partner limb, and only the claimant can be treated as in paid work under regulation 32(2). A partner under 18 outside regulation 8 is now identified as such a partner. The benefit cap rate and exceptions and the LHA shared accommodation test, which Housing Benefit shares, keep the couple rules for a family that stays on legacy benefits (reports one and would not claim Universal Credit). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml index 2038e23465..9f2078bd74 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml @@ -90,7 +90,7 @@ # Responsibility for a UC QYP blocks A. Existing bedroom formula = 1: B. LHA_category: B -- name: Couple aged 30 and 17 is not a single specified renter +- name: Renter aged 30 whose partner is 17 claims as a single person and is a specified renter period: 2025 input: people: @@ -109,10 +109,39 @@ output: is_couple: true is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + # The partner is not a non-dependant (UC Sch 4 para 9(2)(b)). lha_renter_has_non_dependant: false + # The partner is under 18 with no reg. 8 circumstance, so the renter + # claims as a single person (UC Regs 2013 reg. 3(3)(a)) and is "a member + # of a couple claiming as a single person" under 35 (Sch 4 para 28(2); + # ADM F3213): shared accommodation. + uc_member_of_couple_claims_as_single_person: true + is_lha_shared_accommodation_rate_specified_renter: true + LHA_category: A + +- name: Couple aged 30 and 17 where the partner is within reg. 8 is not a specified renter + period: 2025 + input: + people: + renter: + age: 30 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + # Reg. 8(1)(c): caring responsibilities, so the couple claim jointly. + care_hours: 35 + benunits: + couple: + members: [renter, partner] + households: + home: + members: [renter, partner] + output: + is_couple: true + uc_member_of_couple_claims_as_single_person: false is_lha_shared_accommodation_rate_specified_renter: false - # A partner establishes neither QYP responsibility nor a non-dependant; - # couple room = 1: B under the existing bedroom calculation. + # Joint renters have one bedroom (Sch 4 para 10(1)(a)): B. LHA_category: B - name: A 17-year-old in another family is a non-dependant of the household head only diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml new file mode 100644 index 0000000000..4176316e7c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml @@ -0,0 +1,874 @@ +# A member of a couple who claims Universal Credit as a single person +# because the other member cannot be a joint claimant (UC Regs 2013 reg. 3(3)). +# +# The model reads 2026 at the 2026-27 fiscal year. Amounts per assessment +# period from the reg. 36 table (as amended from 6 April 2026 by the +# Universal Credit Act 2025, S.I. 2026/113 and the Social Security Benefits +# Up-rating Order 2026, S.I. 2026/148) and the reg. 22 table: +# standard allowance: single under 25 338.58, single 25 or over 424.90, +# joint both under 25 528.34, joint either 25 or over 666.97; +# LCWRA element for a claimant other than a pre-2026, severe conditions or +# terminally ill claimant 217.26; carer element 209.34; +# work allowance: higher 710, lower 427; taper 55% (reg. 22(1)(b)). +# Benefit cap annual limits (reg. 80A(2)): single claimant outside London +# with no child 14,753; joint claimants or a single claimant responsible for +# a child, outside London, 22,020. +# Earnings stay below the personal allowance and the NI primary threshold +# (12,570), so no tax or NI is deducted. + +- name: Reg. 36(3) - a member of a couple claiming as a single person gets the single 25-or-over standard allowance + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_uc_single_or_joint_claimant: [true, false] + uc_member_of_couple_claims_as_single_person: true + uc_standard_allowance_claimant_type: SINGLE_OLD + # 424.90 x 12 + uc_standard_allowance: 5_098.80 + is_uc_eligible: true + +- name: Joint claimants of the same ages get the joint standard allowance (control) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_uc_single_or_joint_claimant: [true, true] + uc_member_of_couple_claims_as_single_person: false + uc_standard_allowance_claimant_type: COUPLE_OLD + # 666.97 x 12 + uc_standard_allowance: 8_003.64 + +- name: Reg. 36(3) - the single rate follows the claimant's own age, not the partner's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + uc_standard_allowance_claimant_type: SINGLE_YOUNG + # 338.58 x 12 (joint claimants with one aged 30 would get 666.97) + uc_standard_allowance: 4_062.96 + +- name: Reg. 3(3)(a) - a 19-year-old whose 17-year-old partner has no reg. 8 circumstance claims as a single person (ADM E2017 example) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + tom: + age: 19 + is_claimant_or_partner: true + jane: + age: 17 + is_claimant_or_partner: true + benunits: + benunit: + members: [tom, jane] + households: + household: + members: [tom, jane] + region: NORTH_EAST + output: + meets_uc_minimum_age_condition: [true, false] + uc_is_ineligible_partner: [false, true] + is_uc_single_or_joint_claimant: [true, false] + uc_standard_allowance_claimant_type: SINGLE_YOUNG + # 338.58 x 12 + uc_standard_allowance: 4_062.96 + +- name: Reg. 8(1)(c) - a 17-year-old partner with caring responsibilities can be a joint claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + tom: + age: 19 + is_claimant_or_partner: true + jane: + age: 17 + is_claimant_or_partner: true + care_hours: 35 + benunits: + benunit: + members: [tom, jane] + households: + household: + members: [tom, jane] + region: NORTH_EAST + output: + uc_is_ineligible_partner: [false, false] + uc_standard_allowance_claimant_type: COUPLE_YOUNG + # 528.34 x 12 + uc_standard_allowance: 6_340.08 + +- name: WRA 2012 s. 4(1)(b) - a claimant over State Pension age cannot claim as a single person + period: 2026 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + partner: + age: 40 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # The partner is under State Pension age but cannot be a joint claimant, + # so the reg. 3(2)(a) mixed-age route is closed, and there is no single + # claim either. + is_uc_eligible: false + uc_member_of_couple_claims_as_single_person: false + +- name: A working-age claimant can claim as a single person when the partner over State Pension age cannot be a joint claimant + period: 2026 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + partner: + age: 70 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_uc_eligible: true + +- name: Reg. 18(2) - the claimant's capital includes the other member's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + lifetime_isa_balance: 8_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + lifetime_isa_balance: 16_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # Surrender values under 60, 75% of the balance (ADM H1659): + # 6,000 + 12,000 = 18,000, above the 16,000 limit of reg. 18(1)(a). + uc_assessable_capital: 18_000 + is_uc_eligible: false + +- name: Reg. 22(3) - the other member's earnings are deducted as if the couple were joint claimants + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + employment_income: 6_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # No child and no limited capability for work: no work allowance + # (reg. 22(1)(b)(i)), so 55% of 6,000 is deducted. + is_uc_work_allowance_eligible: false + uc_earned_income: 6_000 + uc_income_reduction: 3_300 + # 5,098.80 - 3,300 + universal_credit: 1_798.80 + +- name: Reg. 22(3) - the other member's unearned income is deducted in full + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + private_pension_income: 1_200 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + uc_unearned_income: 1_200 + uc_income_reduction: 1_200 + # 5,098.80 - 1,200 + universal_credit: 3_898.80 + +- name: Reg. 22(3) - the other member's limited capability for work gives the work allowance joint claimants would have, but no LCWRA element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + uc_limited_capability_for_WRA: true + employment_income: 6_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # Joint claimants where "one or both have limited capability for work" + # have a work allowance (reg. 22(1)(b)(ii)): the higher one, 710 x 12 = + # 8,520, with no housing costs element (reg. 22(2)(a)). 6,000 is below it. + is_uc_work_allowance_eligible: true + uc_work_allowance: 8_520 + uc_earned_income: 0 + uc_income_reduction: 0 + # Reg. 27(1): the element is for "a claimant"; the partner is not one. + uc_LCWRA_element: 0 + # 424.90 x 12 + universal_credit: 5_098.80 + +- name: Reg. 27(1) - the claimant's own LCWRA gives the element on a single claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + uc_limited_capability_for_WRA: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # 217.26 x 12 + uc_LCWRA_element: 2_607.12 + +- name: Reg. 29(1) - the other member's caring gives no carer element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + care_hours: 35 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + uc_carer_element: 0 + +- name: Reg. 29(1) - the claimant's caring gives the carer element on a single claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + care_hours: 35 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # 209.34 x 12 + uc_carer_element: 2_512.08 + +- name: Reg. 32(1) - the claimant must be in paid work and the other member too + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + employment_income: 10_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + # Reg. 32(1)(b) applies "whether claiming jointly or as a single person"; + # the partner is neither in paid work nor unable to provide childcare + # for a reason in (i)-(iii). + uc_unable_to_provide_childcare: [false, false, false] + uc_childcare_work_condition: false + +- name: Reg. 32(1) - met when the claimant and the other member both work + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + employment_income: 10_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + employment_income: 5_000 + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + uc_childcare_work_condition: true + +- name: Reg. 32(1)(b)(iii) - the other member's exception applies on a single claim + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + employment_income: 10_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + uc_is_temporarily_absent_from_claimant_household: true + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + # Reg. 32(1)(b) applies "whether claiming jointly or as a single person", + # so the other member, who does not work, is unable to provide childcare + # while temporarily absent from the claimant's household (ADM F7012-F7013). + uc_unable_to_provide_childcare: [false, true, false] + uc_childcare_work_condition: true + +- name: Reg. 32(2) - the other member of a single claim on statutory maternity pay is not treated as in paid work + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + employment_income: 10_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + statutory_maternity_pay: 6_000 + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + # Reg. 32(2) treats "a claimant" as in paid work. The other member of a + # single claim is not a claimant (WRA 2012 s. 40), does not work and has + # no reg. 32(1)(b) exception, so limb (b) fails. + in_work: [true, false, false] + uc_childcare_treated_as_in_paid_work: [false, true, false] + uc_childcare_work_condition: false + +- name: Reg. 32(2) - a joint claimant on statutory maternity pay is treated as in paid work (control) + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + employment_income: 10_000 + partner: + age: 30 + is_claimant_or_partner: true + statutory_maternity_pay: 6_000 + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + # Each joint claimant is a claimant, so reg. 32(2)(b) treats the partner + # as in paid work and limb (b) is met. + uc_is_ineligible_partner: [false, false, false] + uc_childcare_work_condition: true + +- name: Reg. 32(1)(a) - on a single claim only the claimant can meet the paid work limb + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + uc_limited_capability_for_WRA: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + employment_income: 10_000 + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + # Limb (a) needs "the claimant" in paid work or with an offer of it. The + # claimant has limited capability for work but does not work; the other + # member's work does not meet limb (a) for them. + uc_unable_to_provide_childcare: [true, false, false] + uc_childcare_work_condition: false + +- name: Reg. 32(1) - joint claimants, one working and the other with limited capability for work (control) + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + uc_limited_capability_for_WRA: true + partner: + age: 30 + is_claimant_or_partner: true + employment_income: 10_000 + child: + age: 3 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + # The partner is a joint claimant in paid work (limb (a)), and the + # claimant who does not work has limited capability for work (limb (b)(i)). + uc_childcare_work_condition: true + +- name: Reg. 80A(2)(d)(ii) - a single claimant responsible for a child has the family cap + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + child: + age: 5 + benunits: + benunit: + members: [claimant, partner, child] + is_benefit_cap_exempt: false + households: + household: + members: [claimant, partner, child] + region: NORTH_EAST + output: + is_benefit_cap_single_claimant_rate: false + benefit_cap: 22_020 + +- name: Reg. 80A(2)(c) - a single claimant with no child has the single cap + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + is_benefit_cap_exempt: false + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_benefit_cap_single_claimant_rate: true + benefit_cap: 14_753 + +- name: Reg. 83(1)(g) - the other member's PIP does not lift the cap + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + pip_dl: 4_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_benefit_cap_exempt_health_disability: false + is_benefit_cap_exempt: false + +- name: Reg. 83(1)(g) - joint claimants, either receiving PIP, are not capped (control) + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + pip_dl: 4_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_benefit_cap_exempt: true + +- name: Reg. 83(1)(g) - the claimant's own PIP lifts the cap on a single claim + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + pip_dl: 4_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + is_benefit_cap_exempt: true + +- name: Reg. 82(1)(a) - the couple's combined earned income, the other member's included, meets the earnings exception + period: 2026 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + employment_income: 11_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # "Couple" in the cap includes a single claimant's partner (reg. 78(2)). + # 11,000 a year (916.67 a month) exceeds 16 hours at the national living + # wage of 12.71 (NMW Regs 2015 reg. 4 from 1 April 2026): + # 12.71 x 16 x 52 / 12 = 881.23 a month. The model uses a fixed + # 10,152 a year; 11,000 clears both. + is_benefit_cap_exempt_earnings: true + +- name: Sch 4 para 28(2) - a renter under 35 claiming as a single person is a specified renter, whatever the partner's age + period: 2026 + input: + people: + renter: + age: 30 + is_claimant_or_partner: true + partner: + age: 40 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [renter, partner] + households: + household: + members: [renter, partner] + tenure_type: RENT_PRIVATELY + region: NORTH_EAST + output: + # The partner is not a non-dependant (para 9(2)(b)). + lha_renter_has_non_dependant: false + is_lha_shared_accommodation_rate_specified_renter: true + LHA_category: A + +- name: Sch 4 para 28(2) - a renter aged 35 or over claiming as a single person is not a specified renter + period: 2026 + input: + people: + renter: + age: 40 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [renter, partner] + households: + household: + members: [renter, partner] + tenure_type: RENT_PRIVATELY + region: NORTH_EAST + output: + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B + +- name: Sch 4 para 29(5) - only the renter's own disability benefit excepts them from the shared rate + period: 2026 + input: + people: + renter: + age: 30 + is_claimant_or_partner: true + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + pip_dl: 4_000 + benunits: + benunit: + members: [renter, partner] + households: + household: + members: [renter, partner] + tenure_type: RENT_PRIVATELY + region: NORTH_EAST + output: + is_lha_shared_accommodation_rate_specified_renter: true + +- name: A flag on a single person marks no single claim by a member of a couple + period: 2026 + input: + people: + person: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + benunits: + benunit: + members: [person] + households: + household: + members: [person] + region: NORTH_EAST + output: + uc_member_of_couple_claims_as_single_person: false + is_uc_single_or_joint_claimant: false + is_uc_eligible: false + +- name: A family that reports Housing Benefit but claims UC gets the UC single-claim rules in the cap and LHA + period: 2026 + absolute_error_margin: 0.01 + input: + people: + renter: + age: 30 + is_claimant_or_partner: true + housing_benefit_reported: 3_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + pip_dl: 4_000 + benunits: + benunit: + members: [renter, partner] + would_claim_uc: true + households: + household: + members: [renter, partner] + tenure_type: RENT_PRIVATELY + region: NORTH_EAST + output: + claims_legacy_benefits: true + uc_member_of_couple_claims_as_single_person: true + # Calculated on UC, so reg. 80A(2)(c), reg. 83(1) and Sch 4 para 28(2) + # apply: the single cap, no exception for the other member's PIP, and + # shared accommodation. + is_benefit_cap_single_claimant_rate: true + is_benefit_cap_exempt: false + benefit_cap: 14_753 + is_lha_shared_accommodation_rate_specified_renter: true + LHA_category: A + # 424.90 x 12 + uc_standard_allowance: 5_098.80 + +- name: A family that stays on legacy benefits keeps the couple rules Housing Benefit shares + period: 2026 + absolute_error_margin: 0.01 + input: + people: + renter: + age: 30 + is_claimant_or_partner: true + housing_benefit_reported: 3_000 + partner: + age: 30 + is_claimant_or_partner: true + uc_is_ineligible_partner: true + pip_dl: 4_000 + benunits: + benunit: + members: [renter, partner] + would_claim_uc: false + households: + household: + members: [renter, partner] + tenure_type: RENT_PRIVATELY + region: NORTH_EAST + output: + claims_legacy_benefits: true + # Housing Benefit has no single claim by a member of a couple: the couple + # rate (HB reg. 75CA), the partner's PIP lifts the cap (HB reg. + # 75F(1)(ea), "the claimant or the claimant's partner"), and a couple is + # not a young individual (HB reg. 2(1)). + is_benefit_cap_single_claimant_rate: false + is_benefit_cap_exempt: true + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml index fcdafae7eb..19ed1f92a6 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap.yaml @@ -217,7 +217,7 @@ # Same 25,323 as before, now because of responsibility rather than two 18+ members. benefit_cap: 25_323 -- name: Couple aged 30 and 17 receives the family cap without counting the partner as a child. +- name: Couple aged 30 and 17 claims as a single person, so the single cap applies without counting the partner as a child. period: 2025 input: people: @@ -239,9 +239,43 @@ output: is_couple: true is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false + # The partner is under 18 with no UC Regs 2013 reg. 8 circumstance, so + # the claimant claims as a single person (reg. 3(3)(a); ADM E2017). + uc_is_ineligible_partner: [false, true] + uc_member_of_couple_claims_as_single_person: true + is_benefit_cap_single_claimant_rate: true + # Reg. 80A(2)(c): 14,753 for a single claimant outside Greater London + # who is not responsible for a child or qualifying young person (ADM + # E5007 note 3 applies it to a member of a couple claiming as single). + benefit_cap: 14_753 + +- name: Couple aged 30 and 17 where the partner is within reg. 8 claim jointly and receive the couple cap. + period: 2025 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + partner: + age: 17 + is_claimant_or_partner: true + current_education: POST_SECONDARY + # Reg. 8(1)(c): regular and substantial caring responsibilities, so + # the minimum age is 16 and the couple claim jointly. + care_hours: 35 + benunits: + family: + members: [claimant, partner] + is_benefit_cap_exempt: false + households: + home: + members: [claimant, partner] + region: SOUTH_EAST + output: + uc_is_ineligible_partner: [false, false] + uc_member_of_couple_claims_as_single_person: false is_benefit_cap_single_claimant_rate: false - # Couple composition selects 22,020. This is not a UC eligibility test: - # UC reg 3(3) permits some couples to claim as singles, not modelled here. + # Reg. 80A(2)(d)(i): 22,020 for joint claimants outside Greater London. benefit_cap: 22_020 - name: Explicitly identified 19-year-old non-dependant does not imply a partner. diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py new file mode 100644 index 0000000000..266cd3a93a --- /dev/null +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -0,0 +1,689 @@ +"""Property-based tests for a Universal Credit claim by a member of a couple as +a single person (UC Regs 2013 reg. 3(3)). + +Where the other member of a couple cannot be a joint claimant, the member +who can claims as a single person, and "regulations 18 (capital limit), 36 +(amount of elements) and 22 (deduction of income and work allowance) provide +for the calculation of the award" (reg. 3(3)). + +Each generated family is a couple, with up to two children, earnings, +pension income, limited capability for work and work-related activity, +caring, PIP, private or social rent, in or outside London. The same people +are calculated three ways: as a single claim with the second adult flagged +`uc_is_ineligible_partner`, as joint claimants (no flag), and as the +claimant alone with the children ("solo"). Invariants: + +1. Standard allowance (reg. 36(3)): the single claimant's amount at the + claimant's own age, whatever the partner's age. +2. Maximum amount (regs. 36(3), 27(1), 29(1); Sch 4 paras 9(2)(b), 10, 28 + and 29): the single claim's maximum amount equals the solo claimant's. + The partner adds no standard allowance, LCWRA or carer element and no + bedroom, is not a non-dependant, and neither their age nor their PIP + changes the shared accommodation test. +3. Deduction (reg. 22(3)): earned and unearned income, and so the reg. 22(1) + amount, equal the joint claimants'. That includes the work allowance, + which joint claimants have where either has limited capability for work. + No one is self-employed, so the minimum income floor (reg. 62, with + reg. 90(3)(b) for this case) is not engaged. +4. Capital (reg. 18(2)): the single claim's capital equals the joint + claimants', and the sum of each member's capital calculated alone, the + other member's Lifetime ISA included. +5. Benefit cap: in families whose children are all under 16, the + single-claimant rate applies exactly when there is no child (reg. 80A(2); + a 16 or 17-year-old is a qualifying young person only in qualifying + education, which the generator leaves unset). The other member's + disability benefits, LCWRA, caring, AFCS and contributory ESA lift no cap + (reg. 83(1)), so both cap exceptions equal the joint claimants' with those + removed. The model takes any contributory ESA as including the support + component (reg. 83(1)(a) needs the support component); that proxy is the + model's, not this change's. The second exception is also met where + Housing Benefit falls under the pension-age regulations, which the model + applies to a family with a member over the qualifying age for State + Pension Credit and no Universal Credit award. Whether there is an award + can differ between the single claim and the stripped calculation, either + way round. The single claim has a single claimant's amounts (reg. 36(3)) + and still counts the other member's income, such as contributory ESA + (reg. 22(3)), so its award can be the lower. It also keeps the work + allowance the other member's limited capability gives (reg. 22(3)), + which stripping removes, so its award can be the higher. So each + calculation's second exception is checked against its own inputs (the + route as that calculation gives it, or a claimant's AFCS or contributory + ESA), and the two are compared only for families the pension-age route + reaches in neither (see off_pension_age_route). +6. Regulation 3(3)(a): with the flag left to its formula, a member of a + couple under 18 is the ineligible partner exactly when none of the + generated regulation 8(1) circumstances applies to them (limited + capability for work and work-related activity, 35 hours of caring, or a + child under 16 in the family), or they are under 16; the other member + then claims as a single person. +7. A flag that marks no single claim (a single adult, or both members of a + couple) leaves every rule Housing Benefit shares (the cap rate, the cap + exceptions, the shared accommodation test) as it was without the flag. + Such a flag leaves no one able to claim Universal Credit, so for the + second cap exception each calculation is again checked against its own + inputs, and families on the pension-age route in either calculation are + left out of the comparison (see off_pension_age_route). + +Each property also runs on EXAMPLE_FAMILIES, built so that the cases the +properties are about occur, each in a family of its own: an other member +with LCWRA and PIP, with caring, with AFCS, with contributory ESA; a +claimant with LCWRA, with caring, with AFCS; a private renter under 35; a +claimant under 25 with an older partner; an older partner; children under +and over 16; ADM E2017's Tom and Jane. LCWRA and caring are never in the +same person, since reg. 29(4) then gives only the LCWRA element, which the +model does not yet apply. test_examples_reach_the_cases checks that each +case changes the result it should, against the joint claimants or the +claimant alone. + +Marriage Allowance is switched off throughout: a transfer moves tax between +the partners, which is not what these invariants are about. +""" + +import numpy as np +import copy + +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2021: the old LCWRA amount and work allowances. 2025 and 2026: the fiscal +# years either side of the Universal Credit Act 2025 changes to the LCWRA +# element and the standard allowance. +YEARS = [2021, 2025, 2026] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY"] +rarely = st.integers(0, 4).map(lambda n: n == 0) +employment = st.one_of(st.just(0.0), st.floats(0, 30_000)) +pension = st.one_of(st.just(0.0), st.floats(0, 5_000)) +pip = st.one_of(st.just(0.0), st.just(0.0), st.floats(1_000, 6_000)) +sometimes_paid = st.one_of(st.just(0.0), st.just(0.0), st.floats(500, 5_000)) +care = st.sampled_from([0, 0, 0, 35]) + +# Inputs that describe the other member's own disability, caring and +# benefits, with the value that removes each: what reg. 83(1) and regs. 27 +# and 29 ignore for a non-claimant. +PARTNER_OWN = { + "uc_limited_capability_for_WRA": False, + "care_hours": 0, + "pip_dl": 0.0, + "afcs": 0.0, + "esa_contrib": 0.0, +} + + +@st.composite +def adult(draw, ages): + return dict( + age=draw(ages), + employment_income=draw(employment), + private_pension_income=draw(pension), + uc_limited_capability_for_WRA=draw(rarely), + care_hours=draw(care), + pip_dl=draw(pip), + afcs=draw(sometimes_paid), + esa_contrib=draw(sometimes_paid), + ) + + +@st.composite +def couples(draw): + return dict( + claimant=draw(adult(st.integers(18, 64))), + partner=draw(adult(st.integers(18, 80))), + children=[draw(st.integers(0, 17)) for _ in range(draw(st.integers(0, 2)))], + rent=draw(st.one_of(st.just(0.0), st.floats(1_000, 15_000))), + tenure=draw(st.sampled_from(TENURES)), + region=draw(st.sampled_from(REGIONS)), + ) + + +populations = st.lists(couples(), min_size=1, max_size=6) + + +def adult_inputs(age, **inputs): + values = dict( + age=age, + employment_income=0.0, + private_pension_income=0.0, + uc_limited_capability_for_WRA=False, + care_hours=0, + pip_dl=0.0, + afcs=0.0, + esa_contrib=0.0, + ) + values.update(inputs) + return values + + +def example_families(): + """Families in which every case the properties cover occurs. + + The order is EXAMPLE_INDEX's. + """ + return [ + # A claimant under 25, a private renter, with an older other member + # who has LCWRA, PIP and a private pension. + dict( + claimant=adult_inputs(22, employment_income=6_000.0), + partner=adult_inputs( + 40, + uc_limited_capability_for_WRA=True, + pip_dl=4_000.0, + private_pension_income=1_200.0, + ), + children=[], + rent=9_000.0, + tenure="RENT_PRIVATELY", + region="NORTH_EAST", + ), + # A claimant with LCWRA, a child under 16, social rent. + dict( + claimant=adult_inputs(30, uc_limited_capability_for_WRA=True), + partner=adult_inputs(30, employment_income=12_000.0), + children=[4], + rent=6_000.0, + tenure="RENT_FROM_COUNCIL", + region="LONDON", + ), + # An other member over State Pension age; children under and over 16. + dict( + claimant=adult_inputs(45, employment_income=8_000.0), + partner=adult_inputs(70, pip_dl=3_000.0), + children=[10, 16], + rent=12_000.0, + tenure="RENT_PRIVATELY", + region="WALES", + ), + # ADM E2017's example: Tom, 19, and Jane, 17, with no regulation 8 + # circumstance. + dict( + claimant=adult_inputs(19), + partner=adult_inputs(17), + children=[], + rent=4_000.0, + tenure="RENT_FROM_COUNCIL", + region="SCOTLAND", + ), + # An other member who cares 35 hours a week, without LCWRA. + dict( + claimant=adult_inputs(33), + partner=adult_inputs(35, care_hours=35), + children=[], + rent=5_000.0, + tenure="RENT_FROM_COUNCIL", + region="NORTH_EAST", + ), + # A claimant who cares 35 hours a week, without LCWRA. + dict( + claimant=adult_inputs(28, care_hours=35), + partner=adult_inputs(28), + children=[], + rent=5_000.0, + tenure="RENT_FROM_COUNCIL", + region="NORTH_EAST", + ), + # An other member with an AFCS payment and nothing else. + dict( + claimant=adult_inputs(40), + partner=adult_inputs(40, afcs=1_000.0), + children=[], + rent=0.0, + tenure="RENT_FROM_COUNCIL", + region="NORTH_EAST", + ), + # An other member with contributory ESA and nothing else. + dict( + claimant=adult_inputs(40), + partner=adult_inputs(40, esa_contrib=2_000.0), + children=[], + rent=0.0, + tenure="RENT_FROM_COUNCIL", + region="NORTH_EAST", + ), + # A claimant with an AFCS payment and nothing else. + dict( + claimant=adult_inputs(40, afcs=1_000.0), + partner=adult_inputs(40), + children=[], + rent=0.0, + tenure="RENT_FROM_COUNCIL", + region="NORTH_EAST", + ), + ] + + +EXAMPLE_INDEX = dict( + young_renter=0, + lcwra_claimant=1, + older_partner=2, + tom_and_jane=3, + carer_partner=4, + carer_claimant=5, + afcs_partner=6, + esa_partner=7, + afcs_claimant=8, +) +EXAMPLE_FAMILIES = example_families() + + +def situation(families, year, mode): + """One simulation holding every family. + + ``mode`` is "single" (the partner flagged), "joint" (no flag), "solo" + (no partner), "stripped" (joint, with the partner's own disability, + caring and benefits removed), "both" (both members flagged), + "single_adult_flagged" (the claimant alone, flagged) or "derived" (the + flag left to its formula). + """ + people, benunits, households = {}, {}, {} + for i, family in enumerate(families): + names = [] + adults = [("claimant", family["claimant"])] + if mode not in ("solo", "single_adult_flagged"): + adults.append(("partner", family["partner"])) + for role, inputs in adults: + name = f"{role}{i}" + person = {k: {year: v} for k, v in inputs.items()} + person["is_claimant_or_partner"] = {year: True} + person["would_claim_marriage_allowance"] = {year: False} + if role == "partner": + if mode in ("single", "both"): + person["uc_is_ineligible_partner"] = {year: True} + elif mode == "stripped": + for variable, removed in PARTNER_OWN.items(): + person[variable] = {year: removed} + if role == "claimant" and mode in ("both", "single_adult_flagged"): + person["uc_is_ineligible_partner"] = {year: True} + if mode in ("joint", "solo", "stripped"): + person["uc_is_ineligible_partner"] = {year: False} + people[name] = person + names.append(name) + for k, age in enumerate(family["children"]): + name = f"child{i}_{k}" + people[name] = { + "age": {year: age}, + "is_claimant_or_partner": {year: False}, + } + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = { + "members": names, + "rent": {year: family["rent"]}, + "tenure_type": {year: family["tenure"]}, + "region": {year: family["region"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +BENUNIT_VARIABLES = [ + "uc_member_of_couple_claims_as_single_person", + "uc_standard_allowance", + "uc_maximum_amount", + "uc_earned_income", + "uc_unearned_income", + "uc_income_reduction", + "uc_assessable_capital", + "uc_LCWRA_element", + "uc_carer_element", + "uc_housing_costs_element", + "is_uc_eligible", + "is_uc_work_allowance_eligible", + "is_benefit_cap_single_claimant_rate", + "is_benefit_cap_exempt_health_disability", + "is_benefit_cap_exempt_other", + "is_lha_shared_accommodation_rate_specified_renter", + "housing_benefit_pension_age_regulations_apply", +] +PENSION_AGE_ROUTE = "housing_benefit_pension_age_regulations_apply" + + +def off_pension_age_route(*calculations): + """The families that no calculation puts on the pension-age route. + + is_benefit_cap_exempt_other also lifts the cap where Housing Benefit falls + under the pension-age regulations (HB (SPC) Regs 2006 reg. 5; the cap's + age exception, SI 2014/1230 reg. 60C). The model takes that route for a + family with a claimant or partner over the qualifying age for State + Pension Credit and no Universal Credit award. Whether there is an award + can differ between the calculations, either way round: a single claim has + a single claimant's amounts (reg. 36(3)) but the joint claimants' income + and work allowance (reg. 22(3)), and a flag on both members leaves no one + who can claim. So comparisons of that exception across calculations + exclude the families the route reaches in any of them. Within each + calculation the route lifts the cap wherever it applies. + """ + for values in calculations: + assert np.all(values["is_benefit_cap_exempt_other"][values[PENSION_AGE_ROUTE]]) + return ~np.logical_or.reduce([values[PENSION_AGE_ROUTE] for values in calculations]) + + +def exempt_other_from_inputs(families, values, members): + """is_benefit_cap_exempt_other as the inputs give it, with no mask. + + The pension-age route as the same calculation gives it (taken from the + model; test_benefit_cap_and_ctr_pension_age_properties.py and + benefit_cap_pension_age.yaml test the route itself), or AFCS or + contributory ESA of one of ``members`` (reg. 83(1)(a) and (e); the model + takes any contributory ESA as including the support component). + ``members`` are the people whose own benefits count: the claimant alone + on a single claim, in a stripped calculation (the other member's + benefits removed) or with no partner, and both members of a couple + neither of whom is excluded. + """ + own = np.array( + [ + any( + family[member]["afcs"] > 0 or family[member]["esa_contrib"] > 0 + for member in members + ) + for family in families + ] + ) + return values[PENSION_AGE_ROUTE] | own + + +def calculate(families, year, mode, extra=()): + sim = Simulation(situation=situation(families, year, mode)) + values = {v: np.asarray(sim.calculate(v, year)) for v in BENUNIT_VARIABLES} + values["claimant_type"] = np.asarray( + sim.calculate("uc_standard_allowance_claimant_type", year) + ).astype(str) + for v in extra: + values[v] = np.asarray(sim.calculate(v, year)) + values["parameters"] = sim.tax_benefit_system.parameters(f"{year}-01-01") + return values + + +def single_amount(families, year, parameters): + p = parameters.gov.dwp.universal_credit.standard_allowance + threshold = p.claimant_type.age_threshold + return np.array( + [ + 12 + * ( + p.amount.SINGLE_OLD + if family["claimant"]["age"] >= threshold + else p.amount.SINGLE_YOUNG + ) + for family in families + ] + ) + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +@example(families=example_families(), year=2026) +def test_single_standard_allowance_at_the_claimants_own_age(families, year): + single = calculate(families, year, "single") + assert np.all(single["uc_member_of_couple_claims_as_single_person"]), families + assert np.all(np.char.startswith(single["claimant_type"], "SINGLE")), families + np.testing.assert_allclose( + single["uc_standard_allowance"], + single_amount(families, year, single["parameters"]), + atol=0.01, + err_msg=str(families), + ) + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +@example(families=example_families(), year=2026) +@example(families=example_families(), year=2021) +def test_maximum_amount_is_the_solo_claimants(families, year): + single = calculate(families, year, "single") + solo = calculate(families, year, "solo") + # Capital is nil, so both are eligible exactly when the claimant is. + np.testing.assert_array_equal(single["is_uc_eligible"], solo["is_uc_eligible"]) + for variable in [ + "uc_maximum_amount", + "uc_standard_allowance", + "uc_LCWRA_element", + "uc_carer_element", + "uc_housing_costs_element", + "is_lha_shared_accommodation_rate_specified_renter", + ]: + np.testing.assert_allclose( + single[variable], + solo[variable], + atol=0.01, + err_msg=f"{variable}: {families}", + ) + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +@example(families=example_families(), year=2026) +def test_deduction_is_the_joint_claimants(families, year): + single = calculate(families, year, "single") + joint = calculate(families, year, "joint") + for variable in [ + "uc_earned_income", + "uc_unearned_income", + "is_uc_work_allowance_eligible", + ]: + np.testing.assert_allclose( + single[variable], + joint[variable], + atol=0.01, + err_msg=f"{variable}: {families}", + ) + rate = single["parameters"].gov.dwp.universal_credit.means_test.reduction_rate + reg_22_amount = rate * joint["uc_earned_income"] + joint["uc_unearned_income"] + eligible = single["is_uc_eligible"].astype(bool) + np.testing.assert_allclose( + single["uc_income_reduction"][eligible], + np.minimum(single["uc_maximum_amount"], reg_22_amount)[eligible], + atol=0.01, + err_msg=str(families), + ) + + +@PROPERTY_SETTINGS +@given( + families=populations, + year=st.sampled_from([2021, 2026]), + balances=st.lists(st.floats(0, 30_000), min_size=18, max_size=18), +) +@example( + families=example_families(), + year=2026, + balances=[8_000.0, 16_000.0, 0.0, 30_000.0, 2_000.0, 0.0] + [5_000.0] * 12, +) +def test_capital_includes_the_other_members(families, year, balances): + families = copy.deepcopy(families) + for i, family in enumerate(families): + family["claimant"]["lifetime_isa_balance"] = balances[2 * i] + family["partner"]["lifetime_isa_balance"] = balances[2 * i + 1] + single = calculate(families, year, "single") + joint = calculate(families, year, "joint") + claimant_alone = calculate(families, year, "solo") + partner_alone = calculate( + [dict(family, claimant=family["partner"]) for family in families], + year, + "solo", + ) + np.testing.assert_allclose( + single["uc_assessable_capital"], + joint["uc_assessable_capital"], + atol=0.01, + err_msg=str(families), + ) + # Differential: the other member's capital is the same amount it is when + # they are calculated alone, so leaving it out in both the single and + # joint calculations would fail here. + np.testing.assert_allclose( + single["uc_assessable_capital"], + claimant_alone["uc_assessable_capital"] + + partner_alone["uc_assessable_capital"], + atol=0.01, + err_msg=str(families), + ) + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +@example(families=example_families(), year=2026) +def test_benefit_cap_rate_and_exceptions(families, year): + single = calculate(families, year, "single") + stripped = calculate(families, year, "stripped") + # Reg. 80A(2): the single-claimant limit unless responsible for a child + # or qualifying young person. A 16 or 17-year-old is a qualifying young + # person only in qualifying education, which the generator leaves + # unset, so the oracle is read only for families without one. + no_children = np.array([not family["children"] for family in families]) + under_16_only = np.array( + [all(age < 16 for age in family["children"]) for family in families] + ) + np.testing.assert_array_equal( + single["is_benefit_cap_single_claimant_rate"][under_16_only], + no_children[under_16_only], + ) + np.testing.assert_array_equal( + single["is_benefit_cap_exempt_health_disability"], + stripped["is_benefit_cap_exempt_health_disability"], + err_msg=f"is_benefit_cap_exempt_health_disability: {families}", + ) + for values in [single, stripped]: + np.testing.assert_array_equal( + values["is_benefit_cap_exempt_other"], + exempt_other_from_inputs(families, values, ["claimant"]), + err_msg=f"is_benefit_cap_exempt_other: {families}", + ) + off_route = off_pension_age_route(single, stripped) + np.testing.assert_array_equal( + single["is_benefit_cap_exempt_other"][off_route], + stripped["is_benefit_cap_exempt_other"][off_route], + err_msg=f"is_benefit_cap_exempt_other: {families}", + ) + + +@PROPERTY_SETTINGS +@given( + families=populations, + year=st.sampled_from(YEARS), + partner_ages=st.lists(st.integers(15, 20), min_size=6, max_size=6), +) +@example( + families=example_families(), + year=2026, + partner_ages=[17, 17, 16, 17, 17, 16, 17, 17, 16], +) +def test_a_partner_under_18_outside_regulation_8_cannot_claim_jointly( + families, year, partner_ages +): + families = copy.deepcopy(families) + for family, age in zip(families, partner_ages): + family["partner"]["age"] = age + sim = Simulation(situation=situation(families, year, "derived")) + claims_as_single = np.asarray( + sim.calculate("uc_member_of_couple_claims_as_single_person", year) + ) + names = list(situation(families, year, "derived")["people"]) + ineligible = dict( + zip(names, np.asarray(sim.calculate("uc_is_ineligible_partner", year))) + ) + for i, family in enumerate(families): + partner = family["partner"] + # Reg. 8(1)(a), (c) and (d), as generated: limited capability for work + # (the LCWRA flag), 35 hours of caring, responsibility for a child + # (under 16, WRA 2012 s. 40). Reg. 3(3)(a): under 18 and outside + # reg. 8; the model also takes anyone under 16. + regulation_8 = ( + partner["uc_limited_capability_for_WRA"] + or partner["care_hours"] >= 35 + or any(age < 16 for age in family["children"]) + ) + expected = partner["age"] < 16 or (partner["age"] < 18 and not regulation_8) + assert ineligible[f"partner{i}"] == expected, family + # The claimant is 18 to 64, so never excluded, and can claim. + assert not ineligible[f"claimant{i}"], family + assert claims_as_single[i] == expected, family + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +@example(families=example_families(), year=2026) +def test_a_flag_marking_no_single_claim_leaves_shared_rules_alone(families, year): + shared = [ + "is_benefit_cap_single_claimant_rate", + "is_benefit_cap_exempt_health_disability", + "is_benefit_cap_exempt_other", + "is_lha_shared_accommodation_rate_specified_renter", + ] + both = calculate(families, year, "both") + joint = calculate(families, year, "joint") + alone_flagged = calculate(families, year, "single_adult_flagged") + solo = calculate(families, year, "solo") + assert not np.any(both["uc_member_of_couple_claims_as_single_person"]), families + assert not np.any(both["is_uc_eligible"]), families + assert not np.any(alone_flagged["is_uc_eligible"]), families + couple = ["claimant", "partner"] + for values, members in [ + (both, couple), + (joint, couple), + (alone_flagged, ["claimant"]), + (solo, ["claimant"]), + ]: + np.testing.assert_array_equal( + values["is_benefit_cap_exempt_other"], + exempt_other_from_inputs(families, values, members), + err_msg=f"is_benefit_cap_exempt_other: {families}", + ) + for flagged, unflagged in [(both, joint), (alone_flagged, solo)]: + off_route = off_pension_age_route(flagged, unflagged) + for variable in shared: + compared = ( + off_route + if variable == "is_benefit_cap_exempt_other" + else np.ones_like(off_route) + ) + np.testing.assert_array_equal( + flagged[variable][compared], + unflagged[variable][compared], + err_msg=f"{variable}: {families}", + ) + + +def test_examples_reach_the_cases(): + """The example families exercise each rule: the single claim changes it.""" + year = 2026 + single = calculate(EXAMPLE_FAMILIES, year, "single") + joint = calculate(EXAMPLE_FAMILIES, year, "joint") + i = EXAMPLE_INDEX + # Reg. 36(3): the claimant under 25 with a partner of 40. + assert single["claimant_type"][i["young_renter"]] == "SINGLE_YOUNG" + assert joint["claimant_type"][i["young_renter"]] == "COUPLE_OLD" + # Reg. 27(1): the other member's LCWRA gives nothing; the claimant's does. + family = i["young_renter"] + assert single["uc_LCWRA_element"][family] == 0 < joint["uc_LCWRA_element"][family] + assert single["uc_LCWRA_element"][i["lcwra_claimant"]] > 0 + # Reg. 29(1): the same for caring (no LCWRA in either carer, reg. 29(4)). + family = i["carer_partner"] + assert single["uc_carer_element"][family] == 0 < joint["uc_carer_element"][family] + assert single["uc_carer_element"][i["carer_claimant"]] > 0 + # Reg. 83(1): the other member's LCWRA and PIP, AFCS or ESA lift the + # joint claimants' cap only; the claimant's AFCS lifts it. + family = i["young_renter"] + assert not single["is_benefit_cap_exempt_health_disability"][family] + assert joint["is_benefit_cap_exempt_health_disability"][family] + for family in [i["afcs_partner"], i["esa_partner"]]: + assert not single["is_benefit_cap_exempt_other"][family] + assert joint["is_benefit_cap_exempt_other"][family] + assert not single["is_benefit_cap_exempt_health_disability"][family] + assert joint["is_benefit_cap_exempt_health_disability"][family] + assert single["is_benefit_cap_exempt_other"][i["afcs_claimant"]] + # Reg. 80A(2) and Sch 4 para 28(2): single rate and shared accommodation. + family = i["young_renter"] + assert single["is_benefit_cap_single_claimant_rate"][family] + assert not joint["is_benefit_cap_single_claimant_rate"][family] + assert single["is_lha_shared_accommodation_rate_specified_renter"][family] + assert not joint["is_lha_shared_accommodation_rate_specified_renter"][family] + # Reg. 22(3): the other member's LCW gives the work allowance either way. + assert single["is_uc_work_allowance_eligible"][family] diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index 3e6b956e69..4df69d4cbc 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -18,3 +18,52 @@ def claimants(person, period): return person("is_uc_assessed_claimant", period) & ~person( "uc_is_ineligible_partner", period ) + + +def other_member_of_single_claim(person, period): + """The partner of a member of a couple who claims as a single person. + + Under reg. 3(3) the member who can claim does so as a single person. The + other member is still in the couple, so their capital and income count + (regs. 18(2) and 22(3)), but they are not a claimant for the rules that + read "a claimant". A flag on a person with no claimant beside them (a + single person, or a couple neither of whom can claim) marks no such claim. + """ + return person("uc_is_ineligible_partner", period) & person.benunit( + "uc_member_of_couple_claims_as_single_person", period + ) + + +def stays_on_legacy_benefits(benunit, period): + """Whether the family stays on legacy benefits rather than claiming UC. + + A family that reports a legacy benefit (`claims_legacy_benefits`) and + would not claim Universal Credit keeps its legacy awards; this is the + route `housing_benefit_eligible` gives a continuing working-age Housing + Benefit award. A family that would claim Universal Credit is calculated + on Universal Credit whatever it reports. + """ + return benunit("claims_legacy_benefits", period) & ~benunit( + "would_claim_uc", period + ) + + +def single_claim_in_rules_shared_with_legacy_benefits(benunit, period): + """Whether a reg. 3(3) single claim sets the rules the model shares. + + The benefit cap rate and exceptions and the LHA shared accommodation + test serve Housing Benefit as well as Universal Credit. Housing Benefit + and the other legacy benefits have no single claim by a member of a + couple, so a family that stays on legacy benefits keeps the couple rules; + a family claiming Universal Credit gets the Universal Credit rule. + """ + return benunit( + "uc_member_of_couple_claims_as_single_person", period + ) & ~stays_on_legacy_benefits(benunit, period) + + +def other_member_of_single_claim_in_shared_rules(person, period): + """`other_member_of_single_claim`, for the rules shared with legacy benefits.""" + return other_member_of_single_claim(person, period) & ~stays_on_legacy_benefits( + person.benunit, period + ) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py index 72c0d1a847..69e1116338 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim_in_shared_rules, +) +from policyengine_uk.utils.benefit_unit import add_for_members class is_benefit_cap_exempt_health_disability(Variable): @@ -11,9 +15,18 @@ class is_benefit_cap_exempt_health_disability(Variable): def formula(benunit, period, parameters): person = benunit.members + # The exceptions read "a claimant" (UC Regs 2013 reg. 83(1)). A + # partner who cannot be a joint claimant, so that the other member + # claims Universal Credit as a single person (reg. 3(3)), is not a + # claimant: their own disability benefits and limited capability do + # not lift the cap. + not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) + # UC-specific exemptions # Limited capability for work and work-related activity - has_lcwra = benunit.any(person("uc_limited_capability_for_WRA", period)) + has_lcwra = benunit.any( + person("uc_limited_capability_for_WRA", period) & ~not_a_claimant + ) # Carer element in UC indicates caring for someone with disability gets_uc_carer_element = benunit("uc_carer_element", period) > 0 @@ -49,14 +62,18 @@ def formula(benunit, period, parameters): "working_tax_credit", # If getting WTC, likely working enough ] - qualifying_personal_benefits = add(benunit, period, QUAL_PERSONAL_BENEFITS) + qualifying_personal_benefits = add_for_members( + benunit, period, QUAL_PERSONAL_BENEFITS, ~not_a_claimant + ) qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) # Check for Armed Forces Compensation Scheme payments - afcs = benunit("afcs", period) > 0 + afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 # ESA contribution-based with support component - esa_support_component = benunit("esa_contrib", period) > 0 + esa_support_component = ( + add_for_members(benunit, period, ["esa_contrib"], ~not_a_claimant) > 0 + ) return ( has_lcwra diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py index df02432607..b6a3608a2b 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_other.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim_in_shared_rules, +) +from policyengine_uk.utils.benefit_unit import add_for_members class is_benefit_cap_exempt_other(Variable): @@ -16,6 +20,7 @@ class is_benefit_cap_exempt_other(Variable): ) def formula(benunit, period, parameters): + person = benunit.members # The benefit cap applies to a Universal Credit award whatever the # claimants' ages (UC Regs 2013 regs 79, 82 and 83), including a # mixed-age couple's joint award (reg 3(2)(a)). The one age-based @@ -29,10 +34,17 @@ def formula(benunit, period, parameters): "housing_benefit_pension_age_regulations_apply", period ) + # The AFCS and ESA exceptions read "a claimant" (UC Regs 2013 + # reg. 83(1)(a) and (e)); a partner who cannot be a joint claimant + # (reg. 3(3)) is not one. + not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) + # Check for Armed Forces Compensation Scheme payments - afcs = benunit("afcs", period) > 0 + afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 # ESA contribution-based with support component - esa_support_component = benunit("esa_contrib", period) > 0 + esa_support_component = ( + add_for_members(benunit, period, ["esa_contrib"], ~not_a_claimant) > 0 + ) return pension_age_housing_benefit | afcs | esa_support_component diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py index 5527820b67..793001b342 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + single_claim_in_rules_shared_with_legacy_benefits, +) class is_benefit_cap_single_claimant_rate(Variable): @@ -6,24 +9,34 @@ class is_benefit_cap_single_claimant_rate(Variable): entity = BenUnit label = "Benefit cap single claimant rate applies" documentation = ( - "Whether the claimant has no partner and is not responsible for a " + "Whether the claimant is a single claimant, not responsible for a " "child or young person. The model uses one benefit cap for Universal " "Credit and Housing Benefit, so the family rate applies if either " - "scheme's child or young-person test is met. Couple status is a proxy " - "for joint-claim status and does not capture Universal Credit couples " - "who may claim as singles." + "scheme's child or young-person test is met. Couple status stands for " + "joint-claim status, except that a member of a couple who claims " + "Universal Credit as a single person (regulation 3(3)) is a single " + "claimant: regulation 80A sets the limits by single claimant and " + "joint claimants (ADM E5007 note 3). The welfare benefits capped are " + "still the couple's (regulations 78(2) and 79(1)). Housing Benefit " + "has no such single claim, so a family that stays on legacy " + "benefits (reports one and would not claim Universal Credit) keeps " + "the couple rate." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2013/376/regulation/80A", "https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/78", "https://www.legislation.gov.uk/uksi/2006/213/regulation/75CA", "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", ) def formula(benunit, period, parameters): - return ~benunit("is_couple", period) & ~benunit( + single_claimant = ~benunit( + "is_couple", period + ) | single_claim_in_rules_shared_with_legacy_benefits(benunit, period) + return single_claimant & ~benunit( "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", period, ) diff --git a/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py index 2e43025fa3..92578088c2 100644 --- a/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py +++ b/policyengine_uk/variables/gov/dwp/is_lha_shared_accommodation_rate_specified_renter.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim_in_shared_rules, + single_claim_in_rules_shared_with_legacy_benefits, +) class is_lha_shared_accommodation_rate_specified_renter(Variable): @@ -6,14 +10,22 @@ class is_lha_shared_accommodation_rate_specified_renter(Variable): entity = BenUnit label = "LHA renter restricted to the shared accommodation rate" documentation = ( - "The modelled specified-renter conditions: no partner, below the shared " + "The modelled specified-renter conditions: no partner, or a member of " + "a couple who claims Universal Credit as a single person (UC " + "regulation 3(3); Schedule 4 paragraph 28(2)), below the shared " "accommodation age threshold, not responsible for a child or young " "person under the UC or Housing Benefit rules, no non-dependant under " "the household composition proxy, and not excepted by a disability " - "benefit (UC Schedule 4 paragraph 29(5)). Other paragraph 29 " - "exceptions and couples claiming as single people are not modelled. " - "The same category serves Housing Benefit, whose young-individual " - "definition (HB regulation 2(1)) has no disability exception." + "benefit (UC Schedule 4 paragraph 29(5)). The age and the exception " + "are the renter's own: the other member of a couple claiming as a " + "single person is neither a renter nor a non-dependant (paragraph " + "9(2)(b)). Other paragraph 29 exceptions are not modelled. The same " + "category serves Housing Benefit, whose young-individual definition " + "(HB regulation 2(1)) has neither the disability exception nor a " + "single claim by a member of a couple. The model applies the " + "disability exception to Housing Benefit too, and the single-claim " + "rule unless the family stays on legacy benefits (reports one and " + "would not claim Universal Credit)." ) definition_period = YEAR reference = ( @@ -28,21 +40,29 @@ class is_lha_shared_accommodation_rate_specified_renter(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.LHA person = benunit.members + # UC Sch 4 para 28(2): "the renter is a single person (or a member of + # a couple claiming as a single person) who (a) is under 35 years old; + # and (b) is not an excepted person". Where a member of a couple + # claims as a single person (reg. 3(3)), the renter is that member: + # the other member's age and benefits do not count. + claims_as_single_person = single_claim_in_rules_shared_with_legacy_benefits( + benunit, period + ) + other_member = other_member_of_single_claim_in_shared_rules(person, period) + renter = person("is_claimant_or_partner", period) & ~other_member + renter_age = benunit.max(where(renter, person("age", period), -np.inf)) # UC Sch 4 para 29(5): a renter under 35 receiving # attendance allowance, DLA care at the middle or highest rate, or the # PIP daily living component is excepted. Other para 29 exceptions # (care leavers, hostel residents, MAPPA, domestic abuse, modern # slavery, foster parents) are not observed. excepted_disabled_renter = benunit.any( - person("is_claimant_or_partner", period) + renter & (add(person, period, p.shared_accommodation_exception_benefits) > 0) ) return ( - ~benunit("is_couple", period) - & ( - benunit("eldest_claimant_or_partner_age", period) - < p.shared_accommodation_age_threshold - ) + (~benunit("is_couple", period) | claims_as_single_person) + & (renter_age < p.shared_accommodation_age_threshold) & ~benunit( "is_responsible_for_child_or_young_person_for_uc_or_housing_benefit", period, diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element.py index 917d46bcbd..1e5e36cb48 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim, +) class uc_carer_element(Variable): @@ -8,7 +11,27 @@ class uc_carer_element(Variable): definition_period = YEAR unit = GBP defined_for = "benunit_has_carer" + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 29(1)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/29", + ), + dict( + title="Universal Credit Regulations 2013 reg. 36(3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/36", + ), + ] def formula(benunit, period, parameters): p = parameters(period).gov.dwp.universal_credit.elements.carer - return p.amount * MONTHS_IN_YEAR + person = benunit.members + # The element is for "a claimant" with regular and substantial caring + # responsibilities (reg. 29(1)). A partner who cannot be a joint + # claimant, so that the other member claims as a single person + # (reg. 3(3)), is not a claimant: their caring gives no element. + not_a_claimant = other_member_of_single_claim(person, period) + carer = person("is_carer_for_benefits", period) + only_carer_is_not_a_claimant = benunit.any( + carer & not_a_claimant + ) & ~benunit.any(carer & ~not_a_claimant) + return ~only_carer_is_not_a_claimant * p.amount * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py index d6e16123e6..0d25100750 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py @@ -18,7 +18,12 @@ class uc_childcare_work_condition(Variable): "or being treated as in paid work under regulation 32(2) " "(uc_childcare_treated_as_in_paid_work). The exceptions use the " "approximations in uc_unable_to_provide_childcare. Dependants are not " - "tested." + "tested. Where a member of a couple claims as a single person " + "(regulation 3(3)), the claimant must meet limb (a) and the other " + 'member, who is not a claimant, limb (b), which applies "whether ' + 'claiming jointly or as a single person" (ADM F7012). Regulation ' + "32(2) treats only a claimant as in paid work, so it does not apply " + "to that other member." ) definition_period = YEAR reference = [ @@ -35,11 +40,16 @@ class uc_childcare_work_condition(Variable): def formula(benunit, period, parameters): person = benunit.members # The claimant and partner (at most two members), never a dependant - # or a further member flagged as a claimant. - claimant = person("is_uc_assessed_claimant", period) - in_paid_work = person("in_work", period) | person( - "uc_childcare_treated_as_in_paid_work", period - ) + # or a further member flagged as a claimant. This includes the other + # member of a reg. 3(3) single claim, tested under reg. 32(1)(b) + # "whether claiming jointly or as a single person". + claimant_or_partner = person("is_uc_assessed_claimant", period) + # WRA 2012 s. 40 claimants: a single claimant or each joint claimant. + claimant = person("is_uc_single_or_joint_claimant", period) + # Reg. 32(2) treats "a claimant" as in paid work, so not the other + # member of a single claim. + treated = claimant & person("uc_childcare_treated_as_in_paid_work", period) + in_paid_work = person("in_work", period) | treated # Reg. 32(1)(a): in paid work or has an offer of paid work due to # start before the end of the next assessment period. meets_claimant_limb = in_paid_work | person( @@ -54,8 +64,9 @@ def formula(benunit, period, parameters): ) # Joint claimants are each "the claimant" (WRA 2012 s. 40), so the # condition holds if any claimant meets limb (a) while every other - # claimant meets limb (b). A single claimant has no other member. - fails_other_member_limb = claimant & ~meets_other_member_limb + # member of the couple meets limb (b). A single claimant has no other + # member; a single claimant in a couple has their partner. + fails_other_member_limb = claimant_or_partner & ~meets_other_member_limb others_failing = ( benunit.project(benunit.sum(fails_other_member_limb)) - fails_other_member_limb diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_LCWRA_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_LCWRA_element.py index 6460d3aaae..f35e6103f1 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_LCWRA_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_LCWRA_element.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim, +) class uc_LCWRA_element(Variable): @@ -7,9 +10,25 @@ class uc_LCWRA_element(Variable): label = "Universal Credit limited capability for work-related-activity element" definition_period = YEAR unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 27(1)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/27", + ), + dict( + title="Universal Credit Regulations 2013 reg. 36(3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/36", + ), + ] def formula(benunit, period, parameters): p = parameters(period).gov.dwp.universal_credit.elements.disabled - limited_capability = benunit.members("uc_limited_capability_for_WRA", period) - person_amounts = limited_capability * p.amount + person = benunit.members + limited_capability = person("uc_limited_capability_for_WRA", period) + # The element is for "a claimant" with limited capability for work and + # work-related activity (reg. 27(1)). A partner who cannot be a joint + # claimant, so that the other member claims as a single person + # (reg. 3(3)), is not a claimant. + not_a_claimant = other_member_of_single_claim(person, period) + person_amounts = (limited_capability & ~not_a_claimant) * p.amount return benunit.sum(person_amounts) * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py index 2033c754fb..b1a536b9b8 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_claimant.py @@ -8,7 +8,10 @@ class is_uc_claimant(Variable): a couple jointly; children and qualifying young persons are people a claimant is responsible for. This is the benefit unit's claimant or partner (`is_claimant_or_partner`). It does not establish eligibility: - the UC age and other basic conditions are applied separately. + the UC age and other basic conditions are applied separately. A partner + who cannot be a joint claimant (`uc_is_ineligible_partner`, reg. 3(3)) is + still flagged here: their capital and income count. The claimant in the + sense of WRA 2012 s. 40 is `is_uc_single_or_joint_claimant`. """ value_type = bool 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 dab00df84c..74f807d8b8 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 @@ -9,8 +9,11 @@ class is_uc_eligible(Variable): "Whether a claimant or partner meets the modelled minimum-age and " "pension-age conditions and the benefit unit meets the capital limit. " "One qualifying claimant suffices, preserving mixed-age couples under " - "regulation 3(2)(a). Dependants cannot satisfy the claimant age test. " - "A family on the Pension Credit route " + "regulation 3(2)(a). Dependants cannot satisfy the claimant age test, " + "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 " "(meets_pension_credit_age_conditions) is not eligible." ) definition_period = YEAR @@ -24,7 +27,7 @@ class is_uc_eligible(Variable): def formula(benunit, period, parameters): capital = benunit("uc_assessable_capital", period) limit = parameters(period).gov.dwp.universal_credit.means_test.capital.limit - claimant = benunit.members("is_uc_claimant", period) + claimant = benunit.members("is_uc_single_or_joint_claimant", period) meets_minimum_age = benunit.members("meets_uc_minimum_age_condition", period) # WRA 2012 s.4(1)(b): "has not reached the qualifying age for state # pension credit"; s.4(4) imports SPCA 2002 s.1(6). diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_single_or_joint_claimant.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_single_or_joint_claimant.py new file mode 100644 index 0000000000..d87f1785e3 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_single_or_joint_claimant.py @@ -0,0 +1,35 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import claimants + + +class is_uc_single_or_joint_claimant(Variable): + value_type = bool + entity = Person + label = "Universal Credit claimant (a single claimant or each joint claimant)" + documentation = ( + "A claimant in the sense of the Welfare Reform Act 2012 s. 40: a " + "single claimant or each of joint claimants. This is the claimant or " + "partner (`is_uc_claimant`), the two eldest if more are flagged, less " + "a partner who cannot be a joint claimant (`uc_is_ineligible_partner`): " + "where the other member of a couple is under 18 and outside " + "regulation 8, not in Great Britain, a prisoner, excluded by " + "regulation 19 or subject to immigration control, the member who can " + "claim does so as a single person (Universal Credit Regulations 2013 " + "reg. 3(3)). Such a partner is still a member of the couple: their " + "capital and income count (regs. 18(2) and 22(3)), but rules that " + "turn on a claimant's own circumstances do not read theirs." + ) + definition_period = YEAR + reference = [ + dict( + title="Welfare Reform Act 2012 s. 40", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/40", + ), + dict( + title="Universal Credit Regulations 2013 reg. 3(3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ), + ] + + def formula(person, period, parameters): + return claimants(person, period) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance_claimant_type.py b/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance_claimant_type.py index 4e6387156a..9172ea31ca 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance_claimant_type.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/standard_allowance/uc_standard_allowance_claimant_type.py @@ -19,8 +19,12 @@ class uc_standard_allowance_claimant_type(Variable): def formula(benunit, period, parameters): person = benunit.members - claimant = person("is_uc_claimant", period) - is_single = add(benunit, period, ["is_uc_claimant"]) <= 1 + # A member of a couple who claims as a single person because the + # other member cannot be a joint claimant gets "the amounts ... shown + # in the table for a single claimant" (reg. 36(3)), at the rate for + # their own age: the other member is not a claimant. + claimant = person("is_uc_single_or_joint_claimant", period) + is_single = benunit.sum(claimant) <= 1 p = parameters(period).gov.dwp.universal_credit.standard_allowance.claimant_type eldest_claimant_age = benunit.max( where(claimant, person("age", period.this_year), 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_member_of_couple_claims_as_single_person.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_member_of_couple_claims_as_single_person.py new file mode 100644 index 0000000000..cfc964c0b9 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_member_of_couple_claims_as_single_person.py @@ -0,0 +1,44 @@ +from policyengine_uk.model_api import * + + +class uc_member_of_couple_claims_as_single_person(Variable): + value_type = bool + entity = BenUnit + label = "A member of the couple claims Universal Credit as a single person" + documentation = ( + "Whether the claimant is a member of a couple but claims as a single " + "person because the other member cannot be a joint claimant " + "(Universal Credit Regulations 2013 reg. 3(3)): one member of the " + "couple is `uc_is_ineligible_partner` and the other is a claimant who " + "meets the modelled age conditions of section 4(1)(a) and (b). The " + "award then has a single claimant's amounts (reg. 36(3)), the " + "couple's capital (reg. 18(2)) and the deduction joint claimants " + "would have (reg. 22(3)). False where no member can claim: both " + "flagged, a single adult flagged, or a claimant who has reached the " + "qualifying age for State Pension Credit." + ) + definition_period = YEAR + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 3(3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ), + dict( + title="Welfare Reform Act 2012 s. 4", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/4", + ), + ] + + def formula(benunit, period, parameters): + person = benunit.members + ineligible_partner = person("is_uc_assessed_claimant", period) & person( + "uc_is_ineligible_partner", period + ) + # Reg. 3(3) lets the other member claim; WRA 2012 s. 4(1)(a) and (b) + # (with reg. 8) decide whether they can, as in is_uc_eligible. + can_claim = ( + person("is_uc_single_or_joint_claimant", period) + & person("meets_uc_minimum_age_condition", period) + & ~person("has_attained_state_pension_credit_qualifying_age", period) + ) + return benunit.any(ineligible_partner) & benunit.any(can_claim) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py index 09deb21191..2fc47f6ad5 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py @@ -8,15 +8,41 @@ class uc_is_ineligible_partner(Variable): documentation = ( "Whether this person is the member of a couple whose partner claims " "Universal Credit as a single person under the Universal Credit " - "Regulations 2013 reg. 3(3): this person is under 18 and cannot claim " - "at that age, is not in Great Britain, is a prisoner, is excluded by " - "reg. 19, or is subject to immigration control. They are in no work-related group and the " - "minimum income floor never applies to them, but their earned income " - "counts as their partner's, and they add the pay for 35 hours at " - "the national living wage to the couple threshold (reg. 90(3)(b)). " - "An input. The model does not yet give such a couple the single " - "claimant's standard allowance of reg. 36(3)." + "Regulations 2013 reg. 3(3): this person (a) is under 18 and not a " + "person to whom the regulation 8 minimum age of 16 applies, (b) is " + "not in Great Britain, (c) is a prisoner, (d) is excluded by " + "regulation 19, or (e) is subject to immigration control. The model " + "derives (a) from `meets_uc_minimum_age_condition`, treating a member " + "under 16 as within (a) whatever their circumstances; (b) to (e) are " + "not observed, so set this to true for such a partner. They are not " + "a claimant (`is_uc_single_or_joint_claimant`): the award has a " + "single claimant's amounts (reg. 36(3)), no element for their " + "disability or caring, and they are in no work-related group, so the " + "minimum income floor never applies to them. Their capital and " + "income count as the claimant's (regs. 18(2) and 22(3)), and they add " + "the pay for 35 hours at the national living wage to the couple " + "threshold (reg. 90(3)(b))." ) - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/3" + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 3(3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ), + dict( + title="Universal Credit Regulations 2013 reg. 8", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/8", + ), + ] definition_period = YEAR - default_value = False + + def formula(person, period, parameters): + # Reg. 3(3)(a): the other member "does not meet the basic condition + # in section 4(1)(a) (at least 18 years old) and is not a person in + # respect of whom the minimum age specified in regulation 8 applies". + # From 16, failing the age condition means reg. 8 does not apply. A + # member under 16 fails it even where reg. 8 applies; reg. 3(3)(a) + # then strictly leaves the other member no single claim, which the + # model does not follow. Where both members fail, neither can claim. + couple = person("is_uc_assessed_claimant", period) + in_couple = person.benunit.sum(couple) == 2 + return couple & in_couple & ~person("meets_uc_minimum_age_condition", period)