From 5be05b546a51e44a908b4189fb127a23ae37d421 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 11:51:16 -0400 Subject: [PATCH 01/10] Model the rest of a UC single claim by a member of a couple (reg 3(3)) A member of a couple whose partner cannot be a joint claimant claims as a single person (UC Regs 2013 reg 3(3)); regs 18, 22 and 36 then set the award. - is_uc_single_or_joint_claimant: the WRA 2012 s.40 claimant (claimant or partner, less the partner who cannot be a joint claimant). - uc_member_of_couple_claims_as_single_person: the benefit unit flag. - uc_is_ineligible_partner derives reg 3(3)(a) (under 18, outside reg 8) from meets_uc_minimum_age_condition; (b)-(e) stay inputs. - reg 36(3): single standard allowance at the claimant's own age. - s.4 / reg 3(3): only a claimant can meet the age conditions. - regs 27(1), 29(1): no LCWRA or carer element for the other member. - reg 80A(2): single-claimant cap limits (ADM E5007 note 3). - reg 83(1): the other member's benefits do not lift the cap. - Sch 4 para 28(2): shared accommodation rate on the renter's own age and exceptions (ADM F3213). - reg 32(1): the claimant must be in paid work; the other member is tested as a partner (no change in result). - regs 18(2), 22(3), 82(1)(a): unchanged; the partner's capital, income, LCW for the work allowance and earnings already count. Tests pin them. Co-Authored-By: Claude Opus 5.5 --- changelog.d/uc-reg3-3-single-claim.fixed.md | 1 + .../family/LHA_claimant_composition.yaml | 35 +- .../claims_as_single_person.yaml | 653 ++++++++++++++++++ .../policy/baseline/gov/dwp/benefit_cap.yaml | 40 +- .../utils/uc_work_related_requirements.py | 14 + ...is_benefit_cap_exempt_health_disability.py | 25 +- .../gov/dwp/is_benefit_cap_exempt_other.py | 15 +- .../is_benefit_cap_single_claimant_rate.py | 18 +- ...red_accommodation_rate_specified_renter.py | 39 +- .../carer_element/uc_carer_element.py | 25 +- .../uc_childcare_work_condition.py | 13 +- .../limited_work_ability/uc_LCWRA_element.py | 23 +- .../dwp/universal_credit/is_uc_eligible.py | 8 +- .../is_uc_single_or_joint_claimant.py | 35 + .../uc_standard_allowance_claimant_type.py | 8 +- ...ember_of_couple_claims_as_single_person.py | 33 + .../uc_is_ineligible_partner.py | 47 +- 17 files changed, 984 insertions(+), 48 deletions(-) create mode 100644 changelog.d/uc-reg3-3-single-claim.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/is_uc_single_or_joint_claimant.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/uc_member_of_couple_claims_as_single_person.py 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..11f6285580 --- /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)): single-claimant standard allowance at the claimant's own age, no LCWRA or carer element or benefit cap exception for the other member, the single-claimant benefit cap and the shared accommodation rate, while the couple's capital and income still count. A partner under 18 outside regulation 8 is now identified as such a partner. 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 b1435b45d5..75c99e4bb4 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 claimant in another benefit unit blocks the shared rate 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..e89d575bb3 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml @@ -0,0 +1,653 @@ +# 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. + is_uc_eligible: 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 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + 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 (as modelled) excepted. + 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 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + 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. + 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 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/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index 15370b2918..175ee17968 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -25,3 +25,17 @@ def claimants(person, period): couple but not a claimant: no work-related group or floor applies to them. """ return couple_members(person, 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 + ) 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 69d9205c9a..ffe41f746a 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, +) +from policyengine_uk.utils.benefit_unit import add_for_members class is_benefit_cap_exempt_health_disability(Variable): @@ -14,9 +18,18 @@ def formula(benunit, period, parameters): over_pension_age = person("is_SP_age", period) has_pensioner = benunit.any(over_pension_age) + # 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(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 0fb1547506..96de0bd116 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, +) +from policyengine_uk.utils.benefit_unit import add_for_members class is_benefit_cap_exempt_other(Variable): @@ -51,10 +55,17 @@ def formula(benunit, period, parameters): qualifying_personal_benefits = add(benunit, period, QUAL_PERSONAL_BENEFITS) qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) + # 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(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 has_pensioner | 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..fd9d94e9b4 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 @@ -6,24 +6,32 @@ 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, but shares this rate in the model." ) 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) | benunit( + "uc_member_of_couple_claims_as_single_person", 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..95425d34a3 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,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim, +) class is_lha_shared_accommodation_rate_specified_renter(Variable): @@ -6,14 +9,20 @@ 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 both UC " + "rules to Housing Benefit too." ) definition_period = YEAR reference = ( @@ -28,21 +37,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 = benunit( + "uc_member_of_couple_claims_as_single_person", period + ) + other_member = other_member_of_single_claim(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 661786c65f..a5673bb228 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 @@ -9,13 +9,20 @@ class uc_childcare_work_condition(Variable): "Tests work for the claimant and partner, excluding dependants. " "The in_work proxy means positive hours or earnings; offers of work, " "the partner exceptions in regulation 32(1)(b), and the treated-as-working " - "rules in regulation 32(2) are not modelled." + "rules in regulation 32(2) are not modelled. Where a member of a " + "couple claims as a single person (regulation 3(3)), the claimant must " + "be in work and the other member is tested as a partner is, " + 'regulation 32(1)(b) applying "whether claiming jointly or as a ' + 'single person".' ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/32" def formula(benunit, period, parameters): person = benunit.members - claimant = person("is_uc_claimant", period) + claimant = person("is_uc_single_or_joint_claimant", period) + claimant_or_partner = person("is_uc_claimant", period) in_work = person("in_work", period) - return benunit.any(claimant & in_work) & benunit.all(in_work | ~claimant) + return benunit.any(claimant & in_work) & benunit.all( + in_work | ~claimant_or_partner + ) 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_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_eligible.py index f8c205416b..953888ba71 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,7 +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." + "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))." ) definition_period = YEAR reference = ( @@ -21,7 +25,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) pension_age = benunit.members("is_SP_age", period) has_qualifying_claimant = benunit.any( 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..93bb5d0086 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_member_of_couple_claims_as_single_person.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import couple_members + + +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. " + "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 neither member of the couple " + "can claim, so there is no claim." + ) + definition_period = YEAR + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 3(3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/3", + ), + ] + + def formula(benunit, period, parameters): + person = benunit.members + ineligible_partner = couple_members(person, period) & person( + "uc_is_ineligible_partner", period + ) + claimant = person("is_uc_single_or_joint_claimant", period) + return benunit.any(ineligible_partner) & benunit.any(claimant) 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..33278fa680 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 @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import couple_members class uc_is_ineligible_partner(Variable): @@ -8,15 +9,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 = couple_members(person, period) + in_couple = person.benunit.sum(couple) == 2 + return couple & in_couple & ~person("meets_uc_minimum_age_condition", period) From 649503f7bf73fe85cab7151efbc585c52e9574f3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 12:06:16 -0400 Subject: [PATCH 02/10] Add property tests for a UC single claim by a member of a couple Seven invariants over generated couples, each calculated as a reg 3(3) single claim, as joint claimants and as the claimant alone: single standard allowance at the claimant's age (reg 36(3)); maximum amount equal to the solo claimant's (regs 27, 29, 36(3), Sch 4 paras 9, 10, 28, 29); deduction equal to the joint claimants' (reg 22(3)); capital equal to the joint claimants' (reg 18(2)); single cap rate exactly when childless and cap exceptions ignoring the other member (regs 80A, 83); reg 3(3)(a) derivation; and a flag marking no single claim leaving the rules Housing Benefit shares unchanged. Mutation-checked: reverting the LCWRA, standard allowance, cap rate or guard change each fails its property. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_single_claim_properties.py | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 policyengine_uk/tests/test_uc_single_claim_properties.py 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..28d90d4566 --- /dev/null +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -0,0 +1,363 @@ +"""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', the other member's Lifetime ISA included. +5. Benefit cap: the single-claimant rate applies exactly when no child is in + the family (reg. 80A(2)); the other member's disability benefits, LCWRA, + caring, AFCS and contributory ESA lift no cap (reg. 83(1)), so the health + and disability exception equals the joint claimants' with those removed. +6. Regulation 3(3)(a): with the flag left to its formula, a member of a + couple under 18 with no regulation 8 circumstance is the ineligible + partner, and 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. + +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 +from hypothesis import HealthCheck, 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)) +care = st.sampled_from([0, 0, 0, 35]) + +# Inputs that describe the other member's own disability, caring and +# benefits: what reg. 83(1) and regs. 27 and 29 ignore for a non-claimant. +PARTNER_OWN = [ + "uc_limited_capability_for_WRA", + "care_hours", + "pip_dl", +] + + +@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), + ) + + +@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 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": + person["uc_limited_capability_for_WRA"] = {year: False} + person["care_hours"] = {year: 0} + person["pip_dl"] = {year: 0.0} + 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", +] + + +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)) +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)) +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)) +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=12, max_size=12), +) +def test_capital_includes_the_other_members(families, year, balances): + 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") + solo = calculate(families, year, "solo") + np.testing.assert_allclose( + single["uc_assessable_capital"], + joint["uc_assessable_capital"], + atol=0.01, + err_msg=str(families), + ) + assert np.all( + single["uc_assessable_capital"] >= solo["uc_assessable_capital"] - 0.01 + ), families + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +def test_benefit_cap_rate_and_exceptions(families, year): + single = calculate(families, year, "single") + stripped = calculate(families, year, "stripped") + no_children = np.array([not family["children"] for family in families]) + np.testing.assert_array_equal( + single["is_benefit_cap_single_claimant_rate"], no_children + ) + for variable in [ + "is_benefit_cap_exempt_health_disability", + "is_benefit_cap_exempt_other", + ]: + np.testing.assert_array_equal( + single[variable], stripped[variable], err_msg=f"{variable}: {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), +) +def test_a_partner_under_18_outside_regulation_8_cannot_claim_jointly( + families, year, partner_ages +): + for family, age in zip(families, partner_ages): + family["partner"]["age"] = age + sim = Simulation(situation=situation(families, year, "derived")) + ineligible = np.asarray(sim.calculate("uc_is_ineligible_partner", year)) + meets_age = np.asarray(sim.calculate("meets_uc_minimum_age_condition", year)) + couple_member = np.asarray(sim.calculate("is_claimant_or_partner", year)) + age = np.asarray(sim.calculate("age", year)) + np.testing.assert_array_equal( + ineligible, couple_member & ~meets_age, err_msg=str(families) + ) + # Section 4(1)(a): no one aged 18 or over is excluded by reg. 3(3)(a). + assert not np.any(ineligible & (age >= 18)), families + claims_as_single = np.asarray( + sim.calculate("uc_member_of_couple_claims_as_single_person", year) + ) + claimants_failing = np.asarray( + sim.map_result((couple_member & ~meets_age).astype(float), "person", "benunit") + ) + # The claimant is 18 or over, so a single claim exists exactly when the + # partner fails the minimum age. + np.testing.assert_array_equal(claims_as_single, claimants_failing == 1) + + +@PROPERTY_SETTINGS +@given(families=populations, year=st.sampled_from(YEARS)) +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 + for variable in shared: + np.testing.assert_array_equal( + both[variable], joint[variable], err_msg=f"{variable}: {families}" + ) + np.testing.assert_array_equal( + alone_flagged[variable], solo[variable], err_msg=f"{variable}: {families}" + ) From 0dfcc26a7d2522fbdd936dd9adf1e8a99a073f30 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 12:26:17 -0400 Subject: [PATCH 03/10] Declare the claimant and partner in the UC work-related group property fixtures Main's claimant-or-partner inference (#1896) presumes a member under 20 who is at least 16 years younger than the claimant to be the claimant's child. The work-related group properties generate couples such as 52 and 18 without relationship inputs, so after main was merged in the 18-year-old was read as a child, the reg 3(3) flag landed on someone outside the couple, and test_partner_who_cannot_be_a_joint_claimant failed (it fails the same way on 6db0ab118, before this branch's own changes). Set is_claimant_or_partner from each generated role, as #1949 did for its fixtures in a57caf1dd. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_work_related_groups_properties.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_uc_work_related_groups_properties.py b/policyengine_uk/tests/test_uc_work_related_groups_properties.py index 69986bbb76..cd1225e5b5 100644 --- a/policyengine_uk/tests/test_uc_work_related_groups_properties.py +++ b/policyengine_uk/tests/test_uc_work_related_groups_properties.py @@ -151,6 +151,10 @@ def situation(units, year, bump=None, overrides=None): for j, adult in enumerate(unit["adults"]): name = f"p{i}_{j}" person = {k: {year: v} for k, v in adult.items()} + # The generated adults are the claimant and partner; say so, so the + # claimant-or-partner presumption (a member under 20 and much + # younger is the head's child) does not apply. Children get False. + person["is_claimant_or_partner"] = {year: True} if bump is not None and bump[:2] == (i, j): variable, amount = bump[2], bump[3] person[variable] = {year: adult[variable] + amount} @@ -159,7 +163,7 @@ def situation(units, year, bump=None, overrides=None): names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunits[f"b{i}"] = {"members": names} households[f"h{i}"] = { From 17b9b8a16dd7c37254775950728a42c2904e773d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 12:34:35 -0400 Subject: [PATCH 04/10] Address review r1: legacy families keep the shared couple rules; sharper properties From the independent review of 649503f7b (no P1s; three P2s and P3s): - The benefit cap rate and exceptions and the LHA shared accommodation test serve Housing Benefit too, which has no single claim by a member of a couple. A family claiming legacy benefits (claims_legacy_benefits) now keeps the couple rules there; the UC award itself is unchanged. Helpers single_claim_in_rules_shared_with_legacy_benefits and other_member_of_single_claim_in_shared_rules; changelog says so. - uc_member_of_couple_claims_as_single_person requires a claimant who meets the s.4(1)(a) and (b) age conditions, so a claimant over State Pension age with a flagged partner is no single claim (docstring corrected). - Property tests: generate AFCS and contributory ESA (the docstring claimed them); explicit examples, including ADM E2017's Tom and Jane, on every property, and test_examples_reach_the_cases showing each rule changes; property 6 uses the reg 3(3)(a)/reg 8 text as its oracle; the cap-rate oracle reads only families whose children are under 16. - YAML: a legacy-claiming family case; a child in the childcare cases; the reg 82 comment states the model's fixed 10,152 threshold too; the pension-age case asserts no single claim. - is_uc_claimant's docstring points to is_uc_single_or_joint_claimant. Co-Authored-By: Claude Opus 5.5 --- changelog.d/uc-reg3-3-single-claim.fixed.md | 2 +- .../claims_as_single_person.yaml | 52 ++++- .../tests/test_uc_single_claim_properties.py | 197 +++++++++++++++--- .../utils/uc_work_related_requirements.py | 20 ++ ...is_benefit_cap_exempt_health_disability.py | 4 +- .../gov/dwp/is_benefit_cap_exempt_other.py | 4 +- .../is_benefit_cap_single_claimant_rate.py | 12 +- ...red_accommodation_rate_specified_renter.py | 15 +- .../dwp/universal_credit/is_uc_claimant.py | 5 +- ...ember_of_couple_claims_as_single_person.py | 24 ++- 10 files changed, 280 insertions(+), 55 deletions(-) diff --git a/changelog.d/uc-reg3-3-single-claim.fixed.md b/changelog.d/uc-reg3-3-single-claim.fixed.md index 11f6285580..32405fc7ff 100644 --- a/changelog.d/uc-reg3-3-single-claim.fixed.md +++ b/changelog.d/uc-reg3-3-single-claim.fixed.md @@ -1 +1 @@ -Universal Credit for a member of a couple who claims as a single person (UC Regs 2013 reg. 3(3)): single-claimant standard allowance at the claimant's own age, no LCWRA or carer element or benefit cap exception for the other member, the single-claimant benefit cap and the shared accommodation rate, while the couple's capital and income still count. A partner under 18 outside regulation 8 is now identified as such a partner. +Universal Credit for a member of a couple who claims as a single person (UC Regs 2013 reg. 3(3)): single-claimant standard allowance at the claimant's own age, no LCWRA or carer element or benefit cap exception for the other member, the single-claimant benefit cap and the shared accommodation rate, while the couple's capital and income still count. 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, follow the Universal Credit rule for such a couple unless the family claims legacy benefits. 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 index e89d575bb3..8a18d6a46a 100644 --- 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 @@ -163,8 +163,10 @@ 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. + # 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 @@ -386,12 +388,14 @@ age: 30 is_claimant_or_partner: true uc_is_ineligible_partner: true + child: + age: 3 benunits: benunit: - members: [claimant, partner] + members: [claimant, partner, child] households: household: - members: [claimant, partner] + members: [claimant, partner, child] region: NORTH_EAST output: # Reg. 32(1)(b) applies "whether claiming jointly or as a single person"; @@ -411,12 +415,14 @@ is_claimant_or_partner: true uc_is_ineligible_partner: true employment_income: 5_000 + child: + age: 3 benunits: benunit: - members: [claimant, partner] + members: [claimant, partner, child] households: household: - members: [claimant, partner] + members: [claimant, partner, child] region: NORTH_EAST output: uc_childcare_work_condition: true @@ -558,7 +564,8 @@ # "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. + # 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 @@ -651,3 +658,36 @@ uc_member_of_couple_claims_as_single_person: false is_uc_single_or_joint_claimant: false is_uc_eligible: false + +- name: A family claiming legacy benefits keeps the couple rules Housing Benefit shares, while the UC award is a single claim + 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 + benunits: + benunit: + members: [renter, partner] + is_benefit_cap_exempt: false + 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 + # Housing Benefit has no single claim by a member of a couple. + is_benefit_cap_single_claimant_rate: false + benefit_cap: 22_020 + is_lha_shared_accommodation_rate_specified_renter: false + LHA_category: B + # 424.90 x 12 + uc_standard_allowance: 5_098.80 diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 28d90d4566..6d6328543a 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -32,18 +32,29 @@ caring, AFCS and contributory ESA lift no cap (reg. 83(1)), so the health and disability exception equals the joint claimants' with those removed. 6. Regulation 3(3)(a): with the flag left to its formula, a member of a - couple under 18 with no regulation 8 circumstance is the ineligible - partner, and the other member then claims as a single person. + 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. +Each property also runs on EXAMPLE_FAMILIES, built so that the cases the +properties are about occur: an other member with LCWRA, caring, PIP, AFCS +and contributory ESA; a private renter under 35; a claimant under 25 with an +older partner; children under and over 16. test_examples_reach_the_cases +checks that each of those cases changes the result it should. + 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 -from hypothesis import HealthCheck, given, settings +import copy + +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -64,15 +75,19 @@ 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: what reg. 83(1) and regs. 27 and 29 ignore for a non-claimant. -PARTNER_OWN = [ - "uc_limited_capability_for_WRA", - "care_hours", - "pip_dl", -] +# 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 @@ -84,6 +99,8 @@ def adult(draw, ages): uc_limited_capability_for_WRA=draw(rarely), care_hours=draw(care), pip_dl=draw(pip), + afcs=draw(sometimes_paid), + esa_contrib=draw(sometimes_paid), ) @@ -102,6 +119,78 @@ def couples(draw): 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.""" + return [ + # A claimant under 25, a private renter, with an older other member + # who has LCWRA, caring, PIP, AFCS and contributory ESA. + dict( + claimant=adult_inputs(22, employment_income=6_000.0), + partner=adult_inputs( + 40, + uc_limited_capability_for_WRA=True, + care_hours=35, + pip_dl=4_000.0, + afcs=1_000.0, + esa_contrib=2_000.0, + private_pension_income=1_200.0, + ), + children=[], + rent=9_000.0, + tenure="RENT_PRIVATELY", + region="NORTH_EAST", + ), + # A claimant with LCWRA and caring, a child under 16, social rent. + dict( + claimant=adult_inputs( + 30, uc_limited_capability_for_WRA=True, care_hours=35 + ), + 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", + ), + ] + + +EXAMPLE_FAMILIES = example_families() + + def situation(families, year, mode): """One simulation holding every family. @@ -126,9 +215,8 @@ def situation(families, year, mode): if mode in ("single", "both"): person["uc_is_ineligible_partner"] = {year: True} elif mode == "stripped": - person["uc_limited_capability_for_WRA"] = {year: False} - person["care_hours"] = {year: 0} - person["pip_dl"] = {year: 0.0} + 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"): @@ -202,6 +290,7 @@ def single_amount(families, year, parameters): @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 @@ -216,6 +305,8 @@ def test_single_standard_allowance_at_the_claimants_own_age(families, year): @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") @@ -239,6 +330,7 @@ def test_maximum_amount_is_the_solo_claimants(families, year): @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") @@ -270,7 +362,13 @@ def test_deduction_is_the_joint_claimants(families, year): year=st.sampled_from([2021, 2026]), balances=st.lists(st.floats(0, 30_000), min_size=12, max_size=12), ) +@example( + families=example_families(), + year=2026, + balances=[8_000.0, 16_000.0, 0.0, 30_000.0, 2_000.0, 0.0] + [0.0] * 6, +) 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] @@ -290,12 +388,21 @@ def test_capital_includes_the_other_members(families, year, balances): @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"], no_children + single["is_benefit_cap_single_claimant_rate"][under_16_only], + no_children[under_16_only], ) for variable in [ "is_benefit_cap_exempt_health_disability", @@ -312,34 +419,42 @@ def test_benefit_cap_rate_and_exceptions(families, year): 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, 0, 0]) 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")) - ineligible = np.asarray(sim.calculate("uc_is_ineligible_partner", year)) - meets_age = np.asarray(sim.calculate("meets_uc_minimum_age_condition", year)) - couple_member = np.asarray(sim.calculate("is_claimant_or_partner", year)) - age = np.asarray(sim.calculate("age", year)) - np.testing.assert_array_equal( - ineligible, couple_member & ~meets_age, err_msg=str(families) - ) - # Section 4(1)(a): no one aged 18 or over is excluded by reg. 3(3)(a). - assert not np.any(ineligible & (age >= 18)), families claims_as_single = np.asarray( sim.calculate("uc_member_of_couple_claims_as_single_person", year) ) - claimants_failing = np.asarray( - sim.map_result((couple_member & ~meets_age).astype(float), "person", "benunit") + names = list(situation(families, year, "derived")["people"]) + ineligible = dict( + zip(names, np.asarray(sim.calculate("uc_is_ineligible_partner", year))) ) - # The claimant is 18 or over, so a single claim exists exactly when the - # partner fails the minimum age. - np.testing.assert_array_equal(claims_as_single, claimants_failing == 1) + 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", @@ -361,3 +476,31 @@ def test_a_flag_marking_no_single_claim_leaves_shared_rules_alone(families, year np.testing.assert_array_equal( alone_flagged[variable], solo[variable], 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") + first, second = 0, 1 + # Reg. 36(3): the claimant under 25 with a partner of 40. + assert single["claimant_type"][first] == "SINGLE_YOUNG" + assert joint["claimant_type"][first] == "COUPLE_OLD" + # Regs. 27(1), 29(1): the other member's LCWRA and caring give nothing; + # the claimant's do. + assert single["uc_LCWRA_element"][first] == 0 < joint["uc_LCWRA_element"][first] + assert single["uc_carer_element"][first] == 0 < joint["uc_carer_element"][first] + assert single["uc_LCWRA_element"][second] > 0 + assert single["uc_carer_element"][second] > 0 + # Reg. 83(1): the other member's benefits lift the joint claimants' cap + # only. + assert not single["is_benefit_cap_exempt_health_disability"][first] + assert joint["is_benefit_cap_exempt_health_disability"][first] + # Reg. 80A(2) and Sch 4 para 28(2): single rate and shared accommodation. + assert single["is_benefit_cap_single_claimant_rate"][first] + assert not joint["is_benefit_cap_single_claimant_rate"][first] + assert single["is_lha_shared_accommodation_rate_specified_renter"][first] + assert not joint["is_lha_shared_accommodation_rate_specified_renter"][first] + # Reg. 22(3): the other member's LCW gives the work allowance either way. + assert single["is_uc_work_allowance_eligible"][first] diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index 175ee17968..c44804de19 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -39,3 +39,23 @@ def other_member_of_single_claim(person, period): return person("uc_is_ineligible_partner", period) & person.benunit( "uc_member_of_couple_claims_as_single_person", 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 claiming legacy benefits keeps the couple rules. + """ + return benunit("uc_member_of_couple_claims_as_single_person", period) & ~benunit( + "claims_legacy_benefits", 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) & ~person.benunit( + "claims_legacy_benefits", 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 ffe41f746a..4637e8812c 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,6 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim, + other_member_of_single_claim_in_shared_rules, ) from policyengine_uk.utils.benefit_unit import add_for_members @@ -23,7 +23,7 @@ def formula(benunit, period, parameters): # 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(person, period) + not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) # UC-specific exemptions # Limited capability for work and work-related activity 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 96de0bd116..b4524710f9 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,6 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim, + other_member_of_single_claim_in_shared_rules, ) from policyengine_uk.utils.benefit_unit import add_for_members @@ -58,7 +58,7 @@ def formula(benunit, period, parameters): # 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(person, period) + not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) # Check for Armed Forces Compensation Scheme payments afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 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 fd9d94e9b4..4e57669bfd 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): @@ -15,7 +18,8 @@ class is_benefit_cap_single_claimant_rate(Variable): "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, but shares this rate in the model." + "has no such single claim, so a family claiming legacy benefits " + "(`claims_legacy_benefits`) keeps the couple rate." ) definition_period = YEAR reference = ( @@ -28,9 +32,9 @@ class is_benefit_cap_single_claimant_rate(Variable): ) def formula(benunit, period, parameters): - single_claimant = ~benunit("is_couple", period) | benunit( - "uc_member_of_couple_claims_as_single_person", period - ) + 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 95425d34a3..68db1b2923 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,6 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.uc_work_related_requirements import ( - other_member_of_single_claim, + other_member_of_single_claim_in_shared_rules, + single_claim_in_rules_shared_with_legacy_benefits, ) @@ -21,8 +22,10 @@ class is_lha_shared_accommodation_rate_specified_renter(Variable): "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 both UC " - "rules to Housing Benefit too." + "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 claims legacy benefits " + "(`claims_legacy_benefits`)." ) definition_period = YEAR reference = ( @@ -42,10 +45,10 @@ def formula(benunit, period, parameters): # 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 = benunit( - "uc_member_of_couple_claims_as_single_person", period + claims_as_single_person = single_claim_in_rules_shared_with_legacy_benefits( + benunit, period ) - other_member = other_member_of_single_claim(person, 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 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/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 index 93bb5d0086..6361b98992 100644 --- 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 @@ -10,11 +10,13 @@ class uc_member_of_couple_claims_as_single_person(Variable): "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. " - "The award then has a single claimant's amounts (reg. 36(3)), 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 neither member of the couple " - "can claim, so there is no claim." + "would have (reg. 22(3)). False where no member can claim: both " + "flagged, a single adult flagged, or a claimant over State Pension " + "age." ) definition_period = YEAR reference = [ @@ -22,6 +24,10 @@ class uc_member_of_couple_claims_as_single_person(Variable): 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): @@ -29,5 +35,11 @@ def formula(benunit, period, parameters): ineligible_partner = couple_members(person, period) & person( "uc_is_ineligible_partner", period ) - claimant = person("is_uc_single_or_joint_claimant", period) - return benunit.any(ineligible_partner) & benunit.any(claimant) + # 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("is_SP_age", period) + ) + return benunit.any(ineligible_partner) & benunit.any(can_claim) From fa1e7448b92e6e78b045e32b35fd0af938a3e926 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 23:02:55 -0400 Subject: [PATCH 05/10] Address review r2: legacy gate only for families not claiming UC; isolate example cases From the independent review of 17b9b8a16 (no P1s; two P2s, two P3s): - P2: the shared-rules gate read only claims_legacy_benefits, so a family that reports Housing Benefit but is calculated on Universal Credit (would_claim_uc) lost the UC rules in the LHA category, the UC housing costs element and the cap. The gate is now stays_on_legacy_benefits = claims_legacy_benefits & ~would_claim_uc, the route housing_benefit_eligible gives a continuing working-age HB award. YAML pairs: reports HB but claims UC (single cap, no exception for the partner's PIP, shared rate) and stays on legacy (couple rate, partner's PIP lifts the cap per HB reg 75F(1)(ea), no shared rate). - P2: the examples gave LCWRA and caring to the same person and asserted a carer element, against reg 29(4). LCWRA and caring are now in separate example families. - P3: AFCS and contributory ESA examples isolated, with positive joint and claimant controls on both cap exceptions; the capital property is now a differential against each member calculated alone; docstring notes the model's ESA-amount proxy for the support component and the under-16 scope of the cap-rate oracle. - P3: the changelog no longer says every cap exception is gone for the other member (an older partner still lifts it, pre-existing; #2076). Co-Authored-By: Claude Opus 5.5 --- changelog.d/uc-reg3-3-single-claim.fixed.md | 2 +- .../claims_as_single_person.yaml | 50 ++++- .../tests/test_uc_single_claim_properties.py | 177 +++++++++++++----- .../utils/uc_work_related_requirements.py | 27 ++- .../is_benefit_cap_single_claimant_rate.py | 5 +- ...red_accommodation_rate_specified_renter.py | 4 +- 6 files changed, 205 insertions(+), 60 deletions(-) diff --git a/changelog.d/uc-reg3-3-single-claim.fixed.md b/changelog.d/uc-reg3-3-single-claim.fixed.md index 32405fc7ff..0044178862 100644 --- a/changelog.d/uc-reg3-3-single-claim.fixed.md +++ b/changelog.d/uc-reg3-3-single-claim.fixed.md @@ -1 +1 @@ -Universal Credit for a member of a couple who claims as a single person (UC Regs 2013 reg. 3(3)): single-claimant standard allowance at the claimant's own age, no LCWRA or carer element or benefit cap exception for the other member, the single-claimant benefit cap and the shared accommodation rate, while the couple's capital and income still count. 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, follow the Universal Credit rule for such a couple unless the family claims legacy benefits. +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 (an older partner still does through the existing pension-age exception). 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/universal_credit/claims_as_single_person.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml index 8a18d6a46a..af5612469c 100644 --- 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 @@ -659,7 +659,7 @@ is_uc_single_or_joint_claimant: false is_uc_eligible: false -- name: A family claiming legacy benefits keeps the couple rules Housing Benefit shares, while the UC award is a single claim +- 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: @@ -672,10 +672,11 @@ age: 30 is_claimant_or_partner: true uc_is_ineligible_partner: true + pip_dl: 4_000 benunits: benunit: members: [renter, partner] - is_benefit_cap_exempt: false + would_claim_uc: true households: household: members: [renter, partner] @@ -684,10 +685,47 @@ output: claims_legacy_benefits: true uc_member_of_couple_claims_as_single_person: true - # Housing Benefit has no single claim by a member of a couple. + # 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 - benefit_cap: 22_020 + is_benefit_cap_exempt: true is_lha_shared_accommodation_rate_specified_renter: false LHA_category: B - # 424.90 x 12 - uc_standard_allowance: 5_098.80 diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 6d6328543a..0488913f2c 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -26,11 +26,17 @@ 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', the other member's Lifetime ISA included. -5. Benefit cap: the single-claimant rate applies exactly when no child is in - the family (reg. 80A(2)); the other member's disability benefits, LCWRA, - caring, AFCS and contributory ESA lift no cap (reg. 83(1)), so the health - and disability exception equals the joint claimants' with those removed. + 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. 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 @@ -42,10 +48,15 @@ exceptions, the shared accommodation test) as it was without the flag. Each property also runs on EXAMPLE_FAMILIES, built so that the cases the -properties are about occur: an other member with LCWRA, caring, PIP, AFCS -and contributory ESA; a private renter under 35; a claimant under 25 with an -older partner; children under and over 16. test_examples_reach_the_cases -checks that each of those cases changes the result it should. +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. @@ -135,19 +146,19 @@ def adult_inputs(age, **inputs): def example_families(): - """Families in which every case the properties cover occurs.""" + """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, caring, PIP, AFCS and contributory ESA. + # 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, - care_hours=35, pip_dl=4_000.0, - afcs=1_000.0, - esa_contrib=2_000.0, private_pension_income=1_200.0, ), children=[], @@ -155,11 +166,9 @@ def example_families(): tenure="RENT_PRIVATELY", region="NORTH_EAST", ), - # A claimant with LCWRA and caring, a child under 16, social rent. + # A claimant with LCWRA, a child under 16, social rent. dict( - claimant=adult_inputs( - 30, uc_limited_capability_for_WRA=True, care_hours=35 - ), + 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, @@ -185,9 +194,65 @@ def example_families(): 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() @@ -360,12 +425,12 @@ def test_deduction_is_the_joint_claimants(families, year): @given( families=populations, year=st.sampled_from([2021, 2026]), - balances=st.lists(st.floats(0, 30_000), min_size=12, max_size=12), + 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] + [0.0] * 6, + 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) @@ -374,16 +439,28 @@ def test_capital_includes_the_other_members(families, year, balances): family["partner"]["lifetime_isa_balance"] = balances[2 * i + 1] single = calculate(families, year, "single") joint = calculate(families, year, "joint") - solo = calculate(families, year, "solo") + 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), ) - assert np.all( - single["uc_assessable_capital"] >= solo["uc_assessable_capital"] - 0.01 - ), 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 @@ -419,7 +496,11 @@ def test_benefit_cap_rate_and_exceptions(families, year): 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, 0, 0]) +@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 ): @@ -483,24 +564,34 @@ def test_examples_reach_the_cases(): year = 2026 single = calculate(EXAMPLE_FAMILIES, year, "single") joint = calculate(EXAMPLE_FAMILIES, year, "joint") - first, second = 0, 1 + i = EXAMPLE_INDEX # Reg. 36(3): the claimant under 25 with a partner of 40. - assert single["claimant_type"][first] == "SINGLE_YOUNG" - assert joint["claimant_type"][first] == "COUPLE_OLD" - # Regs. 27(1), 29(1): the other member's LCWRA and caring give nothing; - # the claimant's do. - assert single["uc_LCWRA_element"][first] == 0 < joint["uc_LCWRA_element"][first] - assert single["uc_carer_element"][first] == 0 < joint["uc_carer_element"][first] - assert single["uc_LCWRA_element"][second] > 0 - assert single["uc_carer_element"][second] > 0 - # Reg. 83(1): the other member's benefits lift the joint claimants' cap - # only. - assert not single["is_benefit_cap_exempt_health_disability"][first] - assert joint["is_benefit_cap_exempt_health_disability"][first] + 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. - assert single["is_benefit_cap_single_claimant_rate"][first] - assert not joint["is_benefit_cap_single_claimant_rate"][first] - assert single["is_lha_shared_accommodation_rate_specified_renter"][first] - assert not joint["is_lha_shared_accommodation_rate_specified_renter"][first] + 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"][first] + 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 c44804de19..3b03a84bc6 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -41,21 +41,36 @@ def other_member_of_single_claim(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 claiming legacy benefits keeps the couple rules. + 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) & ~benunit( - "claims_legacy_benefits", period - ) + 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) & ~person.benunit( - "claims_legacy_benefits", period + 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_single_claimant_rate.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_single_claimant_rate.py index 4e57669bfd..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 @@ -18,8 +18,9 @@ class is_benefit_cap_single_claimant_rate(Variable): "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 claiming legacy benefits " - "(`claims_legacy_benefits`) keeps the couple rate." + "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 = ( 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 68db1b2923..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 @@ -24,8 +24,8 @@ class is_lha_shared_accommodation_rate_specified_renter(Variable): "(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 claims legacy benefits " - "(`claims_legacy_benefits`)." + "rule unless the family stays on legacy benefits (reports one and " + "would not claim Universal Credit)." ) definition_period = YEAR reference = ( From 1363635ff2331b60b0989bef50b45c7dd390a659 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 13:33:12 -0400 Subject: [PATCH 06/10] Pin reg 32 on a single claim; test the SPC qualifying age as is_uc_eligible does - claims_as_single_person.yaml: five cases for the childcare work condition after #2088: the other member's reg 32(1)(b) exception applies on a single claim; their statutory maternity pay does not treat them as in paid work (reg 32(2) reads "a claimant"), against a joint-claim control; only the claimant can meet limb (a), against a joint-claim control. - uc_member_of_couple_claims_as_single_person: the claimant's pension-age test reads has_attained_state_pension_credit_qualifying_age, as main's is_uc_eligible now does for WRA 2012 s.4(1)(b). The two differ only for men born before 6 December 1953. - Changelog: main's cap now has only the reg 60C pension-age exception, so an older partner no longer lifts a UC single claimant's cap; the note that it did is dropped, and the reg 32 change is described. Co-Authored-By: Claude Opus 5.5 --- changelog.d/uc-reg3-3-single-claim.fixed.md | 2 +- .../claims_as_single_person.yaml | 145 +++++++++++++++++- ...ember_of_couple_claims_as_single_person.py | 6 +- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/changelog.d/uc-reg3-3-single-claim.fixed.md b/changelog.d/uc-reg3-3-single-claim.fixed.md index 0044178862..65ca8b36da 100644 --- a/changelog.d/uc-reg3-3-single-claim.fixed.md +++ b/changelog.d/uc-reg3-3-single-claim.fixed.md @@ -1 +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 (an older partner still does through the existing pension-age exception). 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). +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/universal_credit/claims_as_single_person.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claims_as_single_person.yaml index af5612469c..4176316e7c 100644 --- 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 @@ -399,7 +399,9 @@ 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 (as modelled) excepted. + # 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 @@ -427,6 +429,147 @@ 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: 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 index b31adb8250..cfc964c0b9 100644 --- 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 @@ -14,8 +14,8 @@ class uc_member_of_couple_claims_as_single_person(Variable): "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 over State Pension " - "age." + "flagged, a single adult flagged, or a claimant who has reached the " + "qualifying age for State Pension Credit." ) definition_period = YEAR reference = [ @@ -39,6 +39,6 @@ def formula(benunit, period, parameters): can_claim = ( person("is_uc_single_or_joint_claimant", period) & person("meets_uc_minimum_age_condition", period) - & ~person("is_SP_age", period) + & ~person("has_attained_state_pension_credit_qualifying_age", period) ) return benunit.any(ineligible_partner) & benunit.any(can_claim) From 0f16b40d1a06bce0057f0b5a792e37b272fa179f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 6 Oct 2026 17:25:25 -0400 Subject: [PATCH 07/10] Hold the pension-age route fixed in the single-claim cap properties MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI on b997b0079 (run 37523832169) failed properties 5 and 7 of test_uc_single_claim_properties.py. Main's is_benefit_cap_exempt_other now lifts the cap where housing_benefit_pension_age_regulations_apply holds: a claimant or partner over the State Pension Credit qualifying age and no Universal Credit award. The reg. 3(3) flag changes whether there is an award, as intended: - property 5 (generated, 2021): claimant 19, flagged partner 70. The single claim's maximum amount (reg. 36(3)) of £14,223.33 is wholly deducted, so there is no award and the route applies; the stripped joint calculation has £4,103.58, so it does not. - property 7 (explicit example): with both members flagged no one can claim, so there is no award and the 70-year-old partner puts the family on the route; joint claimants have £25,151.68. These are intended differences, not reg. 83 ones. The oracles now compare is_benefit_cap_exempt_other only for families the route reaches in neither calculation (off_pension_age_route), and assert that within each calculation the route lifts the cap. The other shared rules, and the health and disability exception, are still compared for every family. No model change. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_single_claim_properties.py | 69 +++++++++++++++---- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 0488913f2c..9ac9a23cea 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -36,7 +36,13 @@ (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. + 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. A single claim's lower + amounts can leave no award where joint claimants have one, so that + exception is compared only for families the pension-age route reaches in + neither calculation (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 @@ -46,6 +52,8 @@ 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 the + pension-age route is again held fixed for the second cap exception. 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 @@ -322,7 +330,28 @@ def situation(families, year, mode): "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 + differs between the calculations: a single claim has a single claimant's + amounts (reg. 36(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 calculate(families, year, mode, extra=()): @@ -481,13 +510,17 @@ def test_benefit_cap_rate_and_exceptions(families, year): single["is_benefit_cap_single_claimant_rate"][under_16_only], no_children[under_16_only], ) - for variable in [ - "is_benefit_cap_exempt_health_disability", - "is_benefit_cap_exempt_other", - ]: - np.testing.assert_array_equal( - single[variable], stripped[variable], err_msg=f"{variable}: {families}" - ) + 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}", + ) + 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 @@ -550,13 +583,19 @@ def test_a_flag_marking_no_single_claim_leaves_shared_rules_alone(families, year 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 - for variable in shared: - np.testing.assert_array_equal( - both[variable], joint[variable], err_msg=f"{variable}: {families}" - ) - np.testing.assert_array_equal( - alone_flagged[variable], solo[variable], err_msg=f"{variable}: {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(): From 7cb62d76c78047009717edf4e196384bf38ff4f8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 14:23:48 -0400 Subject: [PATCH 08/10] Check the second cap exception against its inputs in every calculation Answers the review of 0f16b40d1 (subfleet/review-proptest-fix.md): - Docstrings: the pension-age route is not "held fixed"; families on it in either calculation are left out of the cross-calculation comparison. And whether there is an award can differ either way round: the single claim has a single claimant's amounts (reg. 36(3)) but keeps the work allowance the other member's limited capability gives (reg. 22(3)), which the stripped calculation loses. - The review's optional stronger oracle: in each calculation, is_benefit_cap_exempt_other must equal the pension-age route or AFCS or contributory ESA of the members whose own benefits count (the claimant alone on a single claim, in the stripped calculation and with no partner; both members otherwise). This checks the masked families too, so a wrong AFCS or ESA term for the other member cannot hide behind the route. Test-only; no model change. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_single_claim_properties.py | 63 ++++++++++++++++--- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 9ac9a23cea..908e4adfc0 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -39,10 +39,14 @@ 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. A single claim's lower - amounts can leave no award where joint claimants have one, so that - exception is compared only for families the pension-age route reaches in - neither calculation (see off_pension_age_route). + 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)) + but keeps the work allowance the other member's limited capability gives + (reg. 22(3)), which stripping it removes. So each calculation's second + exception is checked against its own inputs (the route, 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 @@ -52,8 +56,10 @@ 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 the - pension-age route is again held fixed for the second cap exception. + 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 @@ -343,8 +349,9 @@ def off_pension_age_route(*calculations): 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 - differs between the calculations: a single claim has a single claimant's - amounts (reg. 36(3)), and a flag on both members leaves no one who can + can differ between the calculations, either way round: a single claim has + a single claimant's amounts (reg. 36(3)) but the joint claimants' 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. @@ -354,6 +361,28 @@ def off_pension_age_route(*calculations): 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, 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} @@ -515,6 +544,12 @@ def test_benefit_cap_rate_and_exceptions(families, year): 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], @@ -583,6 +618,18 @@ def test_a_flag_marking_no_single_claim_leaves_shared_rules_alone(families, year 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: From 6d584bd3a933d6716540c1497e3068ca71ce0805 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 15:03:57 -0400 Subject: [PATCH 09/10] Name the other member's income among the reasons a single claim's award differs Answers the P3s of the review of 929a9d4bb (subfleet/review-after-merge-2.md): - Stripping the other member also removes their contributory ESA from unearned income, which a single claim keeps (reg. 22(3)), so that is a second reason the single claim's award can be the lower. The module and off_pension_age_route docstrings now name it. - exempt_other_from_inputs takes the pension-age route from the model; its docstring now says so and names the tests of the route itself. Docstrings only. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_single_claim_properties.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 908e4adfc0..e45ee67d0e 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -41,12 +41,15 @@ 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)) - but keeps the work allowance the other member's limited capability gives - (reg. 22(3)), which stripping it removes. So each calculation's second - exception is checked against its own inputs (the route, 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). + 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 @@ -350,9 +353,9 @@ def off_pension_age_route(*calculations): 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' 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 + 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. """ @@ -364,9 +367,11 @@ def off_pension_age_route(*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, 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 + 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. From caa114a33e1d10c1b0e3deff3eb7bdc333f327af Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 7 Oct 2026 15:07:17 -0400 Subject: [PATCH 10/10] Rewrap two docstring paragraphs to the line length Whitespace only (review-docstrings.md P3): the words are unchanged. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_single_claim_properties.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index e45ee67d0e..266cd3a93a 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -355,9 +355,9 @@ def off_pension_age_route(*calculations): 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. + 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]]) @@ -371,10 +371,11 @@ def exempt_other_from_inputs(families, values, members): 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. + 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( [