diff --git a/changelog.d/uc-claimant-only-elements.fixed.md b/changelog.d/uc-claimant-only-elements.fixed.md new file mode 100644 index 0000000000..a5be4573bf --- /dev/null +++ b/changelog.d/uc-claimant-only-elements.fixed.md @@ -0,0 +1 @@ +- Give the Universal Credit LCWRA element, carer element and work allowance on the claimants' own circumstances (UC Regs 2013 regs 27(1), 29(1), 22(1)(b)), not a dependant's: a disabled child no longer adds the LCWRA element, and a child or qualifying young person who cares no longer adds the carer element. The work allowance still counts the other member of a single claim by a member of a couple (reg 22(3)). Joint claimants who both have limited capability get one LCWRA element (reg 27(4)). Each claimant who is a carer gets a carer element, but carers of the same severely disabled person get one between them (reg 29(2)-(3)). A carer with the LCWRA element does not also get the carer element (reg 29(4)). `receives_uc_carer_element` follows the claimant the carer element is included for, and the disability basic income reform's LCWRA route needs a claimant with limited capability on a UC award. diff --git a/policyengine_uk/tests/policy/baseline/contrib/disability_basic_income/disability_basic_income.yaml b/policyengine_uk/tests/policy/baseline/contrib/disability_basic_income/disability_basic_income.yaml index 4592e250b5..fe96d0e365 100644 --- a/policyengine_uk/tests/policy/baseline/contrib/disability_basic_income/disability_basic_income.yaml +++ b/policyengine_uk/tests/policy/baseline/contrib/disability_basic_income/disability_basic_income.yaml @@ -70,3 +70,21 @@ is_disabled_for_benefits: true output: disability_basic_income: 5_200 + +- name: Disability basic income excludes a child's LCWRA flag, the LCWRA element being a claimant's + period: 2025 + absolute_error_margin: 0.01 + input: + gov.contrib.disability_basic_income.amount: 100 + people: + parent: + age: 40 + is_parent: true + child: + age: 10 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [parent, child] + output: + disability_basic_income: [0, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/carer_element/uc_carer_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/carer_element/uc_carer_element.yaml index 9b341354d6..66cbdbbef1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/carer_element/uc_carer_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/carer_element/uc_carer_element.yaml @@ -1,15 +1,461 @@ -- name: No carers, no element - period: 2022 +# Universal Credit Regulations 2013 reg. 29(1): "An award of universal credit +# is to include an amount ("the carer element") specified in the table in +# regulation 36 where a claimant has regular and substantial caring +# responsibilities for a severely disabled person". Reg. 30(1)(a) and (2): a +# person has those responsibilities if they satisfy the conditions for +# entitlement to a carer's allowance, "whether or not the person has made a +# claim for a carer's allowance" (35 hours a week: SSCBA 1992 s. 70). +# Reg. 36 table, carer element: £201.68 a month in 2025-26 (£2,420.16 a year) +# and £209.34 in 2026-27 (£2,512.08). + +- name: No carer, no carer element + period: 2026 + absolute_error_margin: 0 + input: + age: 40 + care_hours: 0 + output: + uc_carer_element: 0 + +- name: A single claimant caring 35 hours a week has the carer element, 2025-26 + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + care_hours: 35 + output: + is_uc_assessed_claimant: true + uc_carer_element: 2_420.16 # 201.68 * 12 + +- name: A single claimant caring 35 hours a week has the carer element, 2026-27 + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + care_hours: 35 + output: + uc_carer_element: 2_512.08 # 209.34 * 12 + +- name: A claimant caring 34 hours a week has no carer element + period: 2026 absolute_error_margin: 0 input: - benunit_has_carer: false + age: 40 + care_hours: 34 + output: + uc_carer_element: 0 + +- name: The carer element does not need a Carer's Allowance claim (reg. 30(2)) + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + care_hours: 35 + would_claim_carers_allowance: false + output: + carers_allowance: 0 + uc_carer_element: 2_512.08 + +- name: A qualifying young person who is a carer gives the claimant no carer element + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + care_hours: 40 + benunits: + benunit: + members: [parent, young_person] + output: + is_uc_assessed_claimant: [true, false] + is_carer_for_benefits: [false, true] + uc_carer_element: 0 + +- name: A qualifying young person receiving Carer's Allowance gives the claimant no carer element + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + carers_allowance_reported: 4_000 + benunits: + benunit: + members: [parent, young_person] + output: + is_uc_assessed_claimant: [true, false] + is_carer_for_benefits: [false, true] + uc_carer_element: 0 + +- name: The claimant's own caring still counts when a qualifying young person also cares + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + care_hours: 35 + young_person: + age: 17 + current_education: UPPER_SECONDARY + care_hours: 40 + benunits: + benunit: + members: [parent, young_person] + output: + uc_carer_element: 2_512.08 + +- name: Joint claimants, one of them a carer, have one carer element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 40 + care_hours: 35 + partner: + age: 38 + benunits: + benunit: + members: [carer, partner] + output: + is_uc_assessed_claimant: [true, true] + uc_carer_element: 2_512.08 + +- name: A third member flagged as a claimant is not a claimant, and their caring gives no carer element + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_uc_claimant: true + partner: + age: 48 + is_uc_claimant: true + adult_child: + age: 22 + is_uc_claimant: true + care_hours: 40 + benunits: + benunit: + members: [claimant, partner, adult_child] + output: + is_uc_assessed_claimant: [true, true, false] + uc_carer_element: 0 + +# Reg. 29(2): "In the case of joint claimants, an award is to include the +# carer element for both joint claimants if they both qualify for it, but only +# if they are not caring for the same severely disabled person." Reg. 29(3): +# where two persons care for the same severely disabled person, the award +# "may only include the carer element in respect of one them". +# partners_care_for_same_severely_disabled_person records which; caring hours +# alone cannot show who is cared for, so it defaults to the same person. + +- name: Joint claimants who are both carers for different people have two carer elements + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + partner: + age: 38 + care_hours: 35 + benunits: + benunit: + members: [claimant, partner] + partners_care_for_same_severely_disabled_person: false + output: + uc_carer_element_included_for_claimant: [true, true] + uc_carer_element: 5_024.16 # 209.34 * 12 * 2 + +- name: Joint claimants who care for the same person have one carer element (reg. 29(3)) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + partner: + age: 38 + care_hours: 35 + benunits: + benunit: + members: [claimant, partner] + partners_care_for_same_severely_disabled_person: true + output: + # The model takes the elder where the couple would elect. + uc_carer_element_included_for_claimant: [true, false] + uc_carer_element: 2_512.08 # 209.34 * 12, once + +- name: Two carers by hours alone are taken to care for the same person + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + partner: + age: 38 + care_hours: 35 + benunits: + benunit: + members: [claimant, partner] + output: + partners_care_for_same_severely_disabled_person: true + uc_carer_element: 2_512.08 + +# Reg. 29(4): "Where an amount would, apart from this paragraph, be included +# in an award in relation to a claimant by virtue of paragraphs (1) to (3), +# and the claimant has limited capability for work and work-related activity +# (and, in the case of joint claimants, the LCWRA element has not been +# included in respect of the other claimant), only the LCWRA element may be +# included in respect of the claimant." Reg. 27(4)(a): joint claimants who +# both have limited capability have one LCWRA element. + +- name: A single claimant who is a carer with LCWRA has the LCWRA element only + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + output: + uc_carer_element: 0 + uc_LCWRA_element: 2_607.12 + +- name: A carer with LCWRA whose partner has neither has the LCWRA element only + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 38 + benunits: + benunit: + members: [claimant, partner] output: uc_carer_element: 0 + uc_LCWRA_element: 2_607.12 + +- name: A carer whose partner has LCWRA - the award has both elements + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + partner: + age: 38 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + output: + uc_carer_element: 2_512.08 + uc_LCWRA_element: 2_607.12 + +- name: A carer with LCWRA whose partner has LCWRA keeps the carer element, the LCWRA element being the partner's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 38 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + output: + uc_carer_element: 2_512.08 + uc_LCWRA_element: 2_607.12 + +- name: A carer with LCWRA whose partner is a carer - LCWRA element for one, carer element for the other + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 38 + care_hours: 35 + benunits: + benunit: + members: [claimant, partner] + output: + uc_carer_element: 2_512.08 + uc_LCWRA_element: 2_607.12 -- name: Carer gets element +- name: Joint claimants who are both carers with LCWRA have one LCWRA element and one carer element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 38 + care_hours: 35 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + output: + uc_carer_element: 2_512.08 + uc_LCWRA_element: 2_607.12 + +- name: A qualifying young person's limited capability does not displace the claimant's carer element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + care_hours: 35 + young_person: + age: 17 + current_education: UPPER_SECONDARY + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [parent, young_person] + output: + uc_carer_element: 2_512.08 + uc_LCWRA_element: 0 + +# receives_uc_carer_element attributes the award's carer element to the +# claimant who is the carer, never to a dependant who also cares. + +- name: The claimant carer receives the carer element; a qualifying young person who also cares does not + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + care_hours: 35 + young_person: + age: 17 + current_education: UPPER_SECONDARY + care_hours: 40 + benunits: + benunit: + members: [parent, young_person] + output: + is_carer_for_benefits: [true, true] + uc_carer_element: 2_512.08 # 209.34 * 12, once + receives_uc_carer_element: [true, false] + +# The LCWRA element is withheld during the reg. 28 relevant period; DWP's +# Advice for Decision Making F6034 note: "an amount for the carer element can +# be included in that claimant's UC award" meanwhile. A nil LCWRA element +# entered for the award stands for that. + +- name: A carer with LCWRA whose award has no LCWRA element keeps the carer element period: 2025 - absolute_error_margin: 100 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant] + uc_LCWRA_element: 0 + output: + uc_carer_element: 2_420.16 # 201.68 * 12 + +# receives_uc_carer_element follows the claimant the element is included for. + +- name: A carer with LCWRA and a partner who is a carer - the element is the partner's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 38 + care_hours: 35 + benunits: + benunit: + members: [claimant, partner] + output: + uc_carer_element_included_for_claimant: [false, true] + receives_uc_carer_element: [false, true] + uc_carer_element: 2_512.08 + +- name: Joint carers who both have LCWRA - the LCWRA element is the elder's, the carer element the younger's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 38 + care_hours: 35 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + output: + uc_carer_element_included_for_claimant: [false, true] + receives_uc_carer_element: [false, true] + uc_carer_element: 2_512.08 + +- name: A carer with LCWRA whose partner has LCWRA but does not care - the LCWRA element is the partner's + period: 2026 + absolute_error_margin: 0.01 input: - benunit_has_carer: true + people: + claimant: + age: 38 + care_hours: 35 + uc_limited_capability_for_WRA: true + partner: + age: 40 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] output: - uc_carer_element: 2417 + uc_carer_element_included_for_claimant: [true, false] + receives_uc_carer_element: [true, false] + uc_carer_element: 2_512.08 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/limited_work_ability/uc_LCWRA_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/limited_work_ability/uc_LCWRA_element.yaml index 4c3e4ab923..7ac6b40476 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/limited_work_ability/uc_LCWRA_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/limited_work_ability/uc_LCWRA_element.yaml @@ -1,3 +1,15 @@ +# Universal Credit Regulations 2013 reg. 27(1): "An award of universal credit +# is to include an amount in respect of the fact that a claimant has limited +# capability for work and work-related activity ("the LCWRA element")." +# Reg. 36 table, LCWRA element: £423.27 a month in 2025-26 (£5,079.24 a year); +# in 2026-27, £217.26 for a claimant "other than a pre-2026 claimant, a severe +# conditions criteria claimant or a claimant who is terminally ill" (£2,607.12 +# a year). These tests run the formula, which uses that 2026-27 amount; a +# simulation then sets protected amounts for existing claimants. +# A disabled child or qualifying young person has the "additional amount for +# disabled child or qualifying young person" instead (reg. 24(2)): lower rate +# £164.79 a month in 2026-27 (£1,977.48 a year). + - name: Ineligible period: 2022 absolute_error_margin: 0 @@ -64,3 +76,169 @@ members: person output: uc_LCWRA_element: 2_607.12 + +- name: A single claimant with LCWRA has the LCWRA element, 2025-26 + period: 2025 + absolute_error_margin: 0.01 + input: + age: 40 + uc_limited_capability_for_WRA: true + output: + is_uc_assessed_claimant: true + uc_LCWRA_element: 5_079.24 # 423.27 * 12 + +- name: A disabled child gives the claimant no LCWRA element, only the disabled child addition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 10 + is_disabled_for_benefits: true + benunits: + benunit: + members: [parent, child] + output: + is_uc_assessed_claimant: [true, false] + uc_limited_capability_for_WRA: [false, true] + uc_LCWRA_element: 0 + uc_individual_disabled_child_element: [0, 1_977.48] # 164.79 * 12 + uc_disability_elements: 1_977.48 + +- name: A child flagged with limited capability gives the claimant no LCWRA element + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 10 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [parent, child] + output: + uc_LCWRA_element: 0 + uc_disability_elements: 0 + +- name: A disabled qualifying young person gives the claimant no LCWRA element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + is_disabled_for_benefits: true + benunits: + benunit: + members: [parent, young_person] + output: + is_uc_assessed_claimant: [true, false] + uc_LCWRA_element: 0 + uc_disability_elements: 1_977.48 + +- name: A disabled parent of a disabled child has the LCWRA element and the disabled child addition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + is_disabled_for_benefits: true + child: + age: 10 + is_disabled_for_benefits: true + benunits: + benunit: + members: [parent, child] + output: + uc_LCWRA_element: 2_607.12 + uc_disability_elements: 4_584.60 # 2,607.12 + 1,977.48 + +- name: Joint claimants, one of them with LCWRA, have the LCWRA element + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + uc_limited_capability_for_WRA: true + partner: + age: 38 + benunits: + benunit: + members: [claimant, partner] + output: + is_uc_assessed_claimant: [true, true] + uc_LCWRA_element: 2_607.12 + +- name: A third member flagged as a claimant is not a claimant, and their limited capability gives no LCWRA element + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_uc_claimant: true + partner: + age: 48 + is_uc_claimant: true + adult_child: + age: 22 + is_uc_claimant: true + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner, adult_child] + output: + is_uc_assessed_claimant: [true, true, false] + uc_LCWRA_element: 0 + +# Reg. 27(4)(a): "In the case of joint claimants, where each of them has +# limited capability for work and work-related activity, the award is only to +# include one LCWRA element". + +- name: Joint claimants who both have LCWRA have one LCWRA element, 2025-26 + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + uc_limited_capability_for_WRA: true + partner: + age: 38 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + output: + is_uc_assessed_claimant: [true, true] + uc_LCWRA_element: 5_079.24 # 423.27 * 12, once + +- name: Joint claimants who both have LCWRA have one LCWRA element, 2026-27 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + uc_limited_capability_for_WRA: true + partner: + age: 38 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [claimant, partner] + output: + uc_LCWRA_element: 2_607.12 # 217.26 * 12, once diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml index 4710360580..4c062a41fe 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/is_uc_work_allowance_eligible.yaml @@ -116,3 +116,92 @@ output: is_child_or_qualifying_young_person_for_universal_credit: [false, false] is_uc_work_allowance_eligible: false + +# Reg. 22(1)(b)(i): no work allowance "where a single claimant does not have, +# or neither of joint claimants has, responsibility for a child or qualifying +# young person or limited capability for work". + +- name: Case 8, a single claimant with limited capability has a work allowance. + period: 2026 + input: + age: 40 + uc_limited_capability_for_WRA: true + output: + is_uc_work_allowance_eligible: true + +- name: Case 9, joint claimants where one has limited capability have a work allowance. + period: 2026 + input: + people: + person1: + age: 40 + uc_limited_capability_for_WRA: false + person2: + age: 38 + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [person1, person2] + output: + is_uc_work_allowance_eligible: true + +- name: Case 10, a nonqualifying 17-year-old's limited capability does not create a work allowance. + period: 2026 + input: + people: + person1: + age: 40 + is_uc_claimant: true + uc_limited_capability_for_WRA: false + person2: + age: 17 + is_uc_claimant: false + is_in_non_advanced_education: false + is_in_approved_training: false + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [person1, person2] + output: + is_uc_assessed_claimant: [true, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_uc_work_allowance_eligible: false + uc_work_allowance: 0 + +- name: Case 11, a third flagged claimant's limited capability does not create a work allowance. + period: 2026 + input: + people: + person1: + age: 50 + is_uc_claimant: true + person2: + age: 48 + is_uc_claimant: true + person3: + age: 22 + is_uc_claimant: true + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [person1, person2, person3] + output: + is_uc_assessed_claimant: [true, true, false] + is_uc_work_allowance_eligible: false + +- name: Case 12, a looked-after child's limited capability does not create a work allowance. + period: 2026 + input: + people: + person1: + age: 40 + is_parent: true + person2: + age: 5 + is_looked_after_by_local_authority: true + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [person1, person2] + output: + is_uc_work_allowance_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/uc_work_allowance.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/uc_work_allowance.yaml index 6d1c8f1f22..73c3a77058 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/uc_work_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_allowance/uc_work_allowance.yaml @@ -6,3 +6,26 @@ uc_limited_capability_for_WRA: true output: uc_work_allowance: 4_924 + +# Reg. 22(2) table, 2026-27: higher work allowance £710.00 a month (no housing +# costs element), lower work allowance £427.00 (with one). + +- name: Higher work allowance for a claimant with limited capability and no housing costs element, 2026-27 + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + uc_housing_costs_element: 0 + uc_limited_capability_for_WRA: true + output: + uc_work_allowance: 8_520 # 710 * 12 + +- name: Lower work allowance for a claimant with limited capability and a housing costs element, 2026-27 + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + uc_housing_costs_element: 1 + uc_limited_capability_for_WRA: true + output: + uc_work_allowance: 5_124 # 427 * 12 diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml index 39860f1648..c9aadf70c4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml @@ -538,3 +538,36 @@ output: carers_allowance: [0, 0, 0] extended_childcare_entitlement_work_condition: [true, false, false] + +# Reg 29(4): where a working parent who cares also has LCWRA and the other +# parent cares too, the award's LCWRA element is the working parent's and its +# carer element the other parent's. Before December 2022 the partner exclusion +# (SI 2016/1257 reg 9(3)) applied to a working partner receiving the carer +# element; the working parent does not receive it, so both meet the condition. +- name: Carer with LCWRA working, partner the carer on the UC award - the carer element is the partner's (2021) + period: 2021 + input: + people: + parent1: + age: 40 + is_parent: true + is_carer_for_benefits: true + uc_limited_capability_for_WRA: true + employment_income: 20_000 + adjusted_net_income: 20_000 + minimum_wage: 10 + parent2: + age: 38 + is_parent: true + is_carer_for_benefits: true + minimum_wage: 10 + child: + age: 3 + benunits: + benunit: + members: [parent1, parent2, child] + universal_credit: 3_000 + uc_carer_element: 2_400 + output: + receives_uc_carer_element: [false, true, false] + extended_childcare_entitlement_work_condition: [true, true, false] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_specified_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_specified_benefits.yaml index 082ac4d353..9327b3d862 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_specified_benefits.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_specified_benefits.yaml @@ -1575,7 +1575,7 @@ is_housing_benefit_benefit_cap_exempt_specified_benefit: false benefit_cap_reduction: 0 -- name: A carer element the family's entered carer flag gives lifts the cap, though no member is flagged +- name: A carer element entered for the award lifts the cap, though no member is flagged as a carer period: 2025 input: people: @@ -1584,20 +1584,20 @@ benunits: family: members: [claimant] - benunit_has_carer: true + uc_carer_element: 2_420.16 universal_credit_pre_benefit_cap: 18_000 households: home: members: [claimant] region: NORTH_EAST output: - # The award includes the carer element (reg 83(1)(j)), and no member who - # is not a claimant has caring responsibilities to give it instead. + # The award includes the carer element (reg 83(1)(j)): an element + # entered for the award is included when it is positive. is_uc_benefit_cap_exempt_specified_benefit: true is_housing_benefit_benefit_cap_exempt_specified_benefit: false benefit_cap_reduction: 0 -- name: A nil carer element is not included in the award, though the claimant cares +- name: A nil carer element entered for the award is not included, though the claimant cares period: 2025 input: people: @@ -1607,15 +1607,15 @@ benunits: family: members: [claimant] - benunit_has_carer: false + uc_carer_element: 0 universal_credit_pre_benefit_cap: 18_000 households: home: members: [claimant] region: NORTH_EAST output: - # Reg 83(1)(j) asks whether the award includes the element; the - # family's entered flag gives none. 18,000 - 14,753. + # Reg 83(1)(j) asks whether the award includes the element; the entered + # award includes none. 18,000 - 14,753. uc_carer_element: 0 is_uc_benefit_cap_exempt_specified_benefit: false is_housing_benefit_benefit_cap_exempt_specified_benefit: false diff --git a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py index 9648177f44..6c6745dff7 100644 --- a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py @@ -629,10 +629,20 @@ def test_an_element_a_reform_gives_is_decided_by_its_value(element): assert positive.calculate(element, YEAR)[0] == 6_000 assert positive.calculate(UC_SPECIFIED, YEAR)[0] # A reform's amount that equals the model's own (here, what the model's - # formula gives for the other member's circumstance) is still the - # reform's. + # formula gives for a claimant's circumstance; it gives nothing for the + # other member's, regs 27(1), 29(1)) is still the reform's. + assert ( + Simulation( + situation=situation([([claimant, other], {"working_tax_credit": 0})]) + ).calculate(element, YEAR)[0] + == 0 + ) + claimant_with_circumstance = blank(role="claimant", age=40) + claimant_with_circumstance[circumstance] = True model_amount = Simulation( - situation=situation([([claimant, other], {"working_tax_credit": 0})]) + situation=situation( + [([claimant_with_circumstance, other], {"working_tax_credit": 0})] + ) ).calculate(element, YEAR)[0] assert model_amount > 0 same = Simulation( @@ -662,18 +672,27 @@ def test_an_element_a_reform_gives_is_decided_by_its_value(element): def test_the_rebalancing_adds_no_entry_and_keeps_one(): """From 2026 the Universal Credit rebalancing rescales uc_LCWRA_element (scenarios/uc_reform.py). An element it rescales from the model's own - formula is not an entry, so the other member's limited capability still - does not lift the cap; an element entered for the award stays entered.""" + formula is not an entry: the claimant's limited capability gives one that + lifts the cap, and the other member's gives none (reg 27(1)); an element + entered for the award stays entered.""" year = 2026 claimant, other = blank(role="claimant", age=40), blank(role="other", age=30) + for holder in ["claimant", "other"]: + claimant_flag, other_flag = dict(claimant), dict(other) + (claimant_flag if holder == "claimant" else other_flag)[ + "uc_limited_capability_for_WRA" + ] = True + calculated = Simulation( + situation=situation( + [([claimant_flag, other_flag], {"working_tax_credit": 0})], year + ) + ) + positive = calculated.calculate("uc_LCWRA_element", year)[0] > 0 + assert positive == (holder == "claimant") + benunit = calculated.populations["benunit"] + assert supplied_input(benunit, "uc_LCWRA_element", period(year)) is None + assert bool(calculated.calculate(UC_SPECIFIED, year)[0]) == positive other["uc_limited_capability_for_WRA"] = True - calculated = Simulation( - situation=situation([([claimant, other], {"working_tax_credit": 0})], year) - ) - assert calculated.calculate("uc_LCWRA_element", year)[0] > 0 - benunit = calculated.populations["benunit"] - assert supplied_input(benunit, "uc_LCWRA_element", period(year)) is None - assert not calculated.calculate(UC_SPECIFIED, year)[0] entered = Simulation( situation=situation( @@ -691,9 +710,15 @@ def test_the_rebalancing_adds_no_entry_and_keeps_one(): # stored for the period, so an entry equal to it is still an entry. +# The model's own amount stored below for a family whose only member with +# limited capability is not a claimant: twelve months at the 2026-27 rate. +MODEL_OWNED_ELEMENT = 217.26 * 12 + + def element_situation(year, element=None): - """A claimant and a non-claimant adult with limited capability: the - model's formula gives the element, which does not lift the cap.""" + """A claimant and a non-claimant adult with limited capability. The + model's formula gives no element for the other member's limited + capability (reg 27(1)); ``model_owned`` stores one as the model's own.""" claimant, other = blank(role="claimant", age=40), blank(role="other", age=30) other["uc_limited_capability_for_WRA"] = True inputs = {"working_tax_credit": 0} @@ -702,6 +727,19 @@ def element_situation(year, element=None): return situation([([claimant, other], inputs)], year) +def model_owned(year): + """A simulation of ``element_situation`` for ``year`` holding a positive + element stored as the model's own (set_model_value, as the rebalancing + stores the amounts it rescales) although only the non-claimant has + limited capability. Such an element is not included in the award, so it + does not lift the cap; an entry of the same amount is, and does.""" + simulation = Simulation(situation=element_situation(year)) + set_model_value( + simulation, "uc_LCWRA_element", year, np.array([MODEL_OWNED_ELEMENT]) + ) + return simulation + + def is_entered(simulation, year): benunit = simulation.populations["benunit"] return supplied_input(benunit, "uc_LCWRA_element", period(year)) is not None @@ -753,9 +791,9 @@ def test_an_entry_equal_to_the_model_value_is_an_entry(enter): even one equal to the amount the rebalancing stored as the model's in the same simulation, however it is entered.""" year = 2026 - without_entry = Simulation(situation=element_situation(year)) + without_entry = model_owned(year) assert not without_entry.calculate(UC_SPECIFIED, year)[0] - simulation = Simulation(situation=element_situation(year)) + simulation = model_owned(year) model_amount = float(simulation.calculate("uc_LCWRA_element", year)[0]) assert model_amount > 0 assert not is_entered(simulation, year) @@ -776,7 +814,7 @@ def test_an_entry_equal_to_the_model_value_is_an_entry(enter): def test_clones_and_branches_keep_their_own_record_of_model_values(): year = 2026 - simulation = Simulation(situation=element_situation(year)) + simulation = model_owned(year) model_amount = float(simulation.calculate("uc_LCWRA_element", year)[0]) clone = simulation.clone() @@ -802,7 +840,10 @@ def test_clones_and_branches_keep_their_own_record_of_model_values(): @pytest.fixture(scope="module") def rebalanced(): year = 2026 - simulation = Simulation(situation=element_situation(year)) + # The situation gives the non-claimant limited capability for 2026 only, + # so the model's own element is stored for that year; later years keep + # the formula's nil. + simulation = model_owned(year) return simulation, float(simulation.calculate("uc_LCWRA_element", year)[0]) @@ -963,7 +1004,10 @@ def test_internal_lcwra_wrapper_keeps_the_model_claimant_filter( applied_before_data_load=True, ), ) - assert sim.calculate("uc_LCWRA_element", year)[0] > 0 + # The model's formula gives the element only for a claimant (reg 27(1)). + assert (sim.calculate("uc_LCWRA_element", year)[0] > 0) == ( + holder_role == "claimant" + ) assert not is_entered(sim, year) assert bool(sim.calculate(UC_SPECIFIED, year)[0]) == (holder_role == "claimant") add_universal_credit_reform(sim) diff --git a/policyengine_uk/tests/test_uc_claimant_elements_properties.py b/policyengine_uk/tests/test_uc_claimant_elements_properties.py new file mode 100644 index 0000000000..d695d46772 --- /dev/null +++ b/policyengine_uk/tests/test_uc_claimant_elements_properties.py @@ -0,0 +1,445 @@ +"""Property-based tests: Universal Credit elements turn on the claimants. + +UC Regs 2013 reg. 27(1) includes the LCWRA element "in respect of the fact +that a claimant has limited capability for work and work-related activity"; +reg. 29(1) includes the carer element "where a claimant has regular and +substantial caring responsibilities for a severely disabled person"; and reg. +22(1)(b)(i) gives no work allowance "where a single claimant does not have, +or neither of joint claimants has, responsibility for a child or qualifying +young person or limited capability for work". A child's or qualifying young +person's disability or caring is not the claimant's. (The benefit cap +exceptions, which read the same circumstances, are tested with the cap.) + +Invariants, for any generated population of single claimants and couples +with up to four dependants aged 0 to 19, with or without rent, in England, +Wales and Scotland: + +1. A dependant's disability or caring never gives the claimants an LCWRA + element, a carer element or a work allowance. Flagging every dependant as + disabled, as having limited capability, as caring 35 hours or more or as + receiving Carer's Allowance leaves `uc_LCWRA_element`, `uc_carer_element`, + `is_uc_work_allowance_eligible` and `uc_work_allowance` exactly as they + are when no dependant is. +2. Differential against DWP's published table. The Advice for Decision + Making, chapter F6, paragraph F6037 tabulates which of the carer and LCWRA + amounts an award includes for each combination a single claimant or joint + claimants can have, for carers of different people. The model includes + the same number of each element for all sixteen combinations, whatever the + dependants' circumstances, both in the generated populations and in a + deterministic run of every combination. Where joint claimants care for + the same severely disabled person (reg. 29(3)) it includes at most one + carer element: the table's number, capped at one. The carer element + amount is written here from the reg. 36 table (£201.68 a month in + 2025-26, £209.34 in 2026-27), as is the 2025-26 LCWRA amount (£423.27), + not read from the model. +3. Bounds: an award has at most one LCWRA element (reg. 27(4)), at most one + carer element for each claimant (reg. 29(2)), and at most as many of the + two elements together as it has claimants (reg. 29(4)). Giving both + claimants limited capability gives exactly the LCWRA amount that giving it + to one does, in 2026 too, where the rebalancing modifier sets the amount + per award. +4. Monotonicity: a claimant coming to have limited capability never lowers + the two elements' total (the LCWRA element is the larger), and never takes + a work allowance away. +5. Order does not matter: entering the people of every family in reverse + leaves the elements and the work allowance unchanged. +Marriage Allowance is switched off throughout, as in the other Universal +Credit property tests. +""" + +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=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +YEARS = [2025, 2026] +TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +EDUCATION = ["UPPER_SECONDARY", "TERTIARY", "NOT_IN_EDUCATION"] +# Reg. 36 table, a month: carer element and (2025-26) LCWRA element. +CARER_ELEMENT = {2025: 201.68, 2026: 209.34} +LCWRA_ELEMENT_2025 = 423.27 +ELEMENT_VARIABLES = [ + "uc_LCWRA_element", + "uc_carer_element", + "is_uc_work_allowance_eligible", + "uc_work_allowance", +] +# What a dependant's disability or caring looks like in the inputs. +DEPENDANT_CIRCUMSTANCES = { + "is_disabled_for_benefits": st.booleans(), + "uc_limited_capability_for_WRA": st.booleans(), + "care_hours": st.sampled_from([0.0, 20.0, 35.0, 60.0]), + "carers_allowance_reported": st.sampled_from([0.0, 4_000.0]), +} + +# DWP Advice for Decision Making, chapter F6, paragraph F6037: the additional +# amounts each claimant qualifies for ("C" carer, "L" LCWRA) and the amounts +# the award then includes, as (LCWRA elements, carer elements). The table +# lists twelve combinations; the other four follow by swapping the claimants +# or are the case where neither qualifies for either. +F6037 = { + ("C", ""): (0, 1), + ("L", ""): (1, 0), + ("CL", ""): (1, 0), + ("C", "C"): (0, 2), + ("L", "L"): (1, 0), + ("", "C"): (0, 1), + ("C", "L"): (1, 1), + ("", "L"): (1, 0), + ("CL", "C"): (1, 1), + ("CL", "L"): (1, 1), + ("C", "CL"): (1, 1), + ("CL", "CL"): (1, 1), +} + + +def published_elements(first, second=""): + """(LCWRA elements, carer elements) from the F6037 table.""" + if not first and not second: + return (0, 0) + if (first, second) in F6037: + return F6037[(first, second)] + return F6037[(second, first)] + + +def code(claimant): + return ("C" if claimant["care_hours"] >= 35 else "") + ( + "L" if claimant["uc_limited_capability_for_WRA"] else "" + ) + + +@st.composite +def dependants(draw, minimum=0, maximum=4): + members = [] + for _ in range(draw(st.integers(minimum, maximum))): + age = draw(st.integers(0, 19)) + member = dict(age=age) + for variable, strategy in DEPENDANT_CIRCUMSTANCES.items(): + member[variable] = draw(strategy) + if age >= 16: + # At 18 and 19 only a qualifying young person is a dependant + # (reg. 5(1)); anyone else that age is an adult whom the + # calculator treats as a claimant. + member["current_education"] = ( + draw(st.sampled_from(EDUCATION)) if age < 18 else "UPPER_SECONDARY" + ) + member["age_started_or_accepted_current_education_or_training"] = draw( + st.integers(16, min(age, 18)) + ) + if age == 19: + member[ + "is_before_universal_credit_qualifying_young_person_terminal_date" + ] = True + members.append(member) + return members + + +@st.composite +def families(draw): + claimants = [ + dict( + age=draw(st.integers(25, 64)), + care_hours=draw(st.sampled_from([0.0, 20.0, 35.0, 60.0])), + uc_limited_capability_for_WRA=draw(st.booleans()), + would_claim_carers_allowance=draw(st.booleans()), + ) + for _ in range(draw(st.integers(1, 2))) + ] + return dict( + claimants=claimants, + dependants=draw(dependants()), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + region=draw(st.sampled_from(REGIONS)), + ) + + +populations = st.lists(families(), min_size=1, max_size=6) + + +def situation( + units, + year, + clear=(), + reverse=False, + claimant_overrides=None, + first_claimant_overrides=None, + same_person=False, +): + """One simulation holding every family. + + ``clear`` names dependant inputs to reset to their defaults. With + ``reverse`` the people of each family are entered, and listed in their + benefit unit and household, in reverse order. ``claimant_overrides`` + replaces inputs of every claimant, ``first_claimant_overrides`` those of + the first. ``same_person`` says whether the claimants care for the same + severely disabled person. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + members = [] + for j, claimant in enumerate(unit["claimants"]): + values = {**claimant, **(claimant_overrides or {})} + if j == 0: + values.update(first_claimant_overrides or {}) + person = {k: {year: v} for k, v in values.items()} + person["is_parent"] = {year: bool(unit["dependants"])} + members.append((f"p{i}_{j}", person)) + for k, dependant in enumerate(unit["dependants"]): + person = { + key: {year: v} for key, v in dependant.items() if key not in clear + } + members.append((f"d{i}_{k}", person)) + if reverse: + members = members[::-1] + names = [] + for name, person in members: + person["would_claim_marriage_allowance"] = {year: False} + people[name] = person + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "partners_care_for_same_severely_disabled_person": {year: same_person}, + } + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "region": {year: unit["region"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def benunit_values(sim, year, variables): + return {v: np.asarray(sim.calculate(v, year)) for v in variables} + + +def assert_same(a, b, units): + for v in a: + np.testing.assert_allclose( + a[v].astype(float), b[v].astype(float), atol=0.01, err_msg=f"{v}: {units}" + ) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_dependants_disability_or_caring_never_gives_claimants_an_element(units, year): + with_circumstances = Simulation(situation=situation(units, year)) + without = Simulation( + situation=situation(units, year, clear=tuple(DEPENDANT_CIRCUMSTANCES)) + ) + assert_same( + benunit_values(with_circumstances, year, ELEMENT_VARIABLES), + benunit_values(without, year, ELEMENT_VARIABLES), + units, + ) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_elements_match_the_published_table_of_combinations(units, year): + sim = Simulation(situation=situation(units, year, same_person=False)) + lcwra = np.asarray(sim.calculate("uc_LCWRA_element", year)) + carer = np.asarray(sim.calculate("uc_carer_element", year)) + work_allowance = np.asarray(sim.calculate("is_uc_work_allowance_eligible", year)) + responsible = np.asarray( + sim.map_result( + np.asarray( + sim.calculate( + "is_child_or_qualifying_young_person_for_universal_credit", year + ) + ).astype(float), + "person", + "benunit", + ) + ) + for i, unit in enumerate(units): + lcwra_elements, carer_elements = published_elements( + *[code(c) for c in unit["claimants"]] + ) + np.testing.assert_allclose( + carer[i], + carer_elements * CARER_ELEMENT[year] * 12, + atol=0.01, + err_msg=str(unit), + ) + assert (lcwra[i] > 0) == (lcwra_elements == 1), unit + if year == 2025: + np.testing.assert_allclose( + lcwra[i], + lcwra_elements * LCWRA_ELEMENT_2025 * 12, + atol=0.01, + err_msg=str(unit), + ) + # Reg. 22(1)(b)(i): a work allowance needs a claimant with limited + # capability or responsibility for a child or qualifying young person. + claimant_limited = any( + c["uc_limited_capability_for_WRA"] for c in unit["claimants"] + ) + assert work_allowance[i] == (claimant_limited or responsible[i] > 0), unit + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_carers_of_the_same_person_have_at_most_one_carer_element(units, year): + sim = Simulation(situation=situation(units, year, same_person=True)) + carer = np.asarray(sim.calculate("uc_carer_element", year)) + for i, unit in enumerate(units): + _, carer_elements = published_elements(*[code(c) for c in unit["claimants"]]) + np.testing.assert_allclose( + carer[i], + min(carer_elements, 1) * CARER_ELEMENT[year] * 12, + atol=0.01, + err_msg=str(unit), + ) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_both_claimants_limited_capability_gives_one_lcwra_element(units, year): + one = Simulation( + situation=situation( + units, + year, + claimant_overrides=dict(uc_limited_capability_for_WRA=False), + first_claimant_overrides=dict(uc_limited_capability_for_WRA=True), + ) + ) + both = Simulation( + situation=situation( + units, year, claimant_overrides=dict(uc_limited_capability_for_WRA=True) + ) + ) + amount_one = np.asarray(one.calculate("uc_LCWRA_element", year)) + amount_both = np.asarray(both.calculate("uc_LCWRA_element", year)) + assert np.all(amount_one > 0), units + np.testing.assert_allclose(amount_both, amount_one, atol=0.01, err_msg=str(units)) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_an_award_has_at_most_one_element_for_each_claimant(units, year): + sim = Simulation(situation=situation(units, year)) + lcwra = np.asarray(sim.calculate("uc_LCWRA_element", year)) > 0 + carer_elements = np.rint( + np.asarray(sim.calculate("uc_carer_element", year)) / (CARER_ELEMENT[year] * 12) + ) + claimants = np.array([len(unit["claimants"]) for unit in units]) + assert np.all(carer_elements <= claimants), units + assert np.all(lcwra.astype(int) + carer_elements <= claimants), units + if year == 2025: + amounts = np.asarray(sim.calculate("uc_LCWRA_element", year)) + one_element = LCWRA_ELEMENT_2025 * 12 + assert np.all( + np.isclose(amounts, 0, atol=0.01) + | np.isclose(amounts, one_element, atol=0.01) + ), units + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_a_claimants_limited_capability_never_lowers_the_elements(units, year): + variables = ["uc_LCWRA_element", "uc_carer_element", "uc_work_allowance"] + as_given = benunit_values( + Simulation(situation=situation(units, year)), year, variables + ) + all_limited = benunit_values( + Simulation( + situation=situation( + units, year, claimant_overrides=dict(uc_limited_capability_for_WRA=True) + ) + ), + year, + variables, + ) + total_given = as_given["uc_LCWRA_element"] + as_given["uc_carer_element"] + total_limited = all_limited["uc_LCWRA_element"] + all_limited["uc_carer_element"] + assert np.all(total_limited >= total_given - 0.01), units + assert np.all( + all_limited["uc_work_allowance"] >= as_given["uc_work_allowance"] - 0.01 + ), units + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_member_order_does_not_change_the_elements(units, year): + # The 2026 LCWRA amount is drawn per benefit unit by the rebalancing + # modifier, and does not depend on the order of the unit's members. + forward = Simulation(situation=situation(units, year)) + backward = Simulation(situation=situation(units, year, reverse=True)) + assert_same( + benunit_values(forward, year, ELEMENT_VARIABLES), + benunit_values(backward, year, ELEMENT_VARIABLES), + units, + ) + + +def test_published_table_covers_all_sixteen_combinations(): + # Every combination of two claimants' carer and LCWRA status resolves, + # and swapping the claimants gives the same award. + codes = ["", "C", "L", "CL"] + for first in codes: + for second in codes: + assert published_elements(first, second) == published_elements( + second, first + ) + assert len(F6037) == 12 + + +def claimant_with(status, age): + return dict( + age=age, + care_hours=35.0 if "C" in status else 0.0, + uc_limited_capability_for_WRA="L" in status, + ) + + +def test_model_matches_the_published_table_for_every_combination(): + # A deterministic run of every single and joint combination, for carers + # of different people and of the same person, in 2025-26. + year = 2025 + codes = ["", "C", "L", "CL"] + combinations = [(first,) for first in codes] + [ + (first, second) for first in codes for second in codes + ] + for same_person in [False, True]: + units = [ + dict( + claimants=[ + claimant_with(status, 45 - 5 * j) for j, status in enumerate(c) + ], + dependants=[], + tenure="OWNED_OUTRIGHT", + rent=0.0, + region="NORTH_EAST", + ) + for c in combinations + ] + sim = Simulation(situation=situation(units, year, same_person=same_person)) + lcwra = np.asarray(sim.calculate("uc_LCWRA_element", year)) + carer = np.asarray(sim.calculate("uc_carer_element", year)) + for i, c in enumerate(combinations): + lcwra_elements, carer_elements = published_elements(*c) + if same_person: + carer_elements = min(carer_elements, 1) + context = str((c, same_person)) + np.testing.assert_allclose( + lcwra[i], + lcwra_elements * LCWRA_ELEMENT_2025 * 12, + atol=0.01, + err_msg=context, + ) + np.testing.assert_allclose( + carer[i], + carer_elements * CARER_ELEMENT[year] * 12, + atol=0.01, + err_msg=context, + ) diff --git a/policyengine_uk/variables/contrib/disability_basic_income/disability_basic_income.py b/policyengine_uk/variables/contrib/disability_basic_income/disability_basic_income.py index 10e7aaeed6..20bb5fab8a 100644 --- a/policyengine_uk/variables/contrib/disability_basic_income/disability_basic_income.py +++ b/policyengine_uk/variables/contrib/disability_basic_income/disability_basic_income.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 disability_basic_income(Variable): @@ -13,8 +16,14 @@ def formula(person, period, parameters): amount = parameters(period).gov.contrib.disability_basic_income.amount receives_dla = person("dla", period) > 0 receives_pip = person("pip", period) > 0 - receives_uc_lcwra = person("uc_limited_capability_for_WRA", period) & ( - person.benunit("universal_credit", period) > 0 + # The LCWRA element is a claimant's (UC Regs 2013 reg. 27(1)): a + # child's or qualifying young person's limited capability does not + # put it in the award. + receives_uc_lcwra = ( + person("is_uc_assessed_claimant", period) + & ~other_member_of_single_claim(person, period) + & person("uc_limited_capability_for_WRA", period) + & (person.benunit("universal_credit", period) > 0) ) eligible = receives_dla | receives_pip | receives_uc_lcwra return eligible * amount * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py index ddbd0abac2..a6acb7f364 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py @@ -163,14 +163,13 @@ def received_by(benefits, members): # award. An element entered for the award is included exactly when it # is positive, whatever the members' circumstances (an entry of nil # means the award does not include it). An element a reform's formula - # gives is included when positive. The model's own formula still - # gives the element for any member's limited capability for work and - # work-related activity or caring responsibilities, but the element is - # for "a claimant" (regs 27(1), 29(1)). So the element it gives is - # included when positive unless the only members with the - # circumstance are not claimants. A circumstance entered for the - # family alone (benunit_has_carer) names no member, and a nil amount - # (benunit_has_carer entered as false, or a nil rate) is not included. + # gives is included when positive. The element is for "a claimant" + # (regs 27(1), 29(1)), and the model's own formulas give it only for a + # claimant's limited capability for work and work-related activity or + # caring responsibilities, so the element they give is included when + # positive; the check that a claimant has the circumstance guards + # against a formula that does not. A nil amount (a nil rate) is not + # included. # The Universal Credit rebalancing rescales the formula's elements # from 2026 (scenarios/uc_reform.py) and is read the same way. The # model does not have the reg 28 waiting period or regs 29(5)-(6), diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/receives_uc_carer_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/receives_uc_carer_element.py index 4ef487bd6c..31c9fa508c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/receives_uc_carer_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/receives_uc_carer_element.py @@ -6,12 +6,14 @@ class receives_uc_carer_element(Variable): entity = Person label = "receives the Universal Credit carer element" documentation = ( - "Whether this person is a carer in a benefit unit whose Universal " + "Whether this person is a claimant in respect of whom the Universal " "Credit award includes the carer element (Universal Credit " - "Regulations 2013 reg 29). The carer element is a benefit-unit " - "amount; this attributes it to the carer, and requires an award, " - "since uc_carer_element is calculated for any benefit unit with a " - "carer." + "Regulations 2013 reg 29; `uc_carer_element_included_for_claimant`), " + "on an award. The carer element is a benefit-unit amount; this " + "attributes it to the claimant it is included for, and requires an " + "award, since uc_carer_element is calculated whether or not there is " + "one. A child or qualifying young person who cares does not receive " + "it, nor does a carer who has only the LCWRA element (reg 29(4))." ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/29" @@ -21,4 +23,7 @@ def formula(person, period, parameters): award_includes_carer_element = (benunit("universal_credit", period) > 0) & ( benunit("uc_carer_element", period) > 0 ) - return person("is_carer_for_benefits", period) & award_includes_carer_element + included_for_this_claimant = person( + "uc_carer_element_included_for_claimant", period + ) + return included_for_this_claimant & award_includes_carer_element 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 1e5e36cb48..8acb565b4e 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,19 +1,25 @@ 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): value_type = float entity = BenUnit label = "Universal Credit carer element" + documentation = ( + "The amount an award includes because a claimant has regular and " + "substantial caring responsibilities for a severely disabled person: " + "the reg. 36 amount for each claimant in respect of whom the element " + "is included (`uc_carer_element_included_for_claimant`)." + ) definition_period = YEAR unit = GBP - defined_for = "benunit_has_carer" reference = [ dict( - title="Universal Credit Regulations 2013 reg. 29(1)", + title="Welfare Reform Act 2012 s. 12(2)(c)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/12", + ), + dict( + title="Universal Credit Regulations 2013 reg. 29", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/29", ), dict( @@ -24,14 +30,5 @@ class uc_carer_element(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.universal_credit.elements.carer - 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 + elements = add(benunit, period, ["uc_carer_element_included_for_claimant"]) + return elements * p.amount * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element_included_for_claimant.py b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element_included_for_claimant.py new file mode 100644 index 0000000000..277734efab --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/uc_carer_element_included_for_claimant.py @@ -0,0 +1,78 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + other_member_of_single_claim, +) + + +class uc_carer_element_included_for_claimant(Variable): + value_type = bool + entity = Person + label = "Universal Credit carer element included in respect of this claimant" + documentation = ( + "Whether the Universal Credit award includes the carer element in " + "respect of this person. Only a claimant (`is_uc_assessed_claimant`, " + "less the other member of a single claim under reg. 3(3)) " + "who has regular and substantial caring responsibilities for a " + "severely disabled person (`is_carer_for_benefits`) qualifies, so a " + "child or qualifying young person who cares does not. A claimant in " + "respect of whom the award's LCWRA element is included has only that " + "element. The LCWRA element is one per award; where it is included " + "(`uc_LCWRA_element` positive) and more than one claimant has limited " + "capability for work and work-related activity, the model takes it " + "to be included in respect of one who is not a carer if there is " + "one, and otherwise the elder, so the other keeps the carer element. " + "Where it is not included (an award of nil, as in the reg. 28 " + "waiting period), a carer's limited capability does not displace the " + "carer element. Joint claimants who both qualify have an element " + "each unless they care for the same severely disabled person " + "(`partners_care_for_same_severely_disabled_person`); then only one " + "is included, which they elect or the Secretary of State decides, " + "and the model takes the elder." + ) + definition_period = YEAR + reference = [ + dict( + title="Welfare Reform Act 2012 s. 12(2)(c)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/12", + ), + dict( + title="Universal Credit Regulations 2013 reg. 29(1) to (4)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/29", + ), + dict( + title="Universal Credit Regulations 2013 reg. 27(4)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/27", + ), + dict( + title="Universal Credit Regulations 2013 reg. 30", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/30", + ), + ] + + def formula(person, period, parameters): + benunit = person.benunit + age = person("age", period) + # Reg. 29(1): "where a claimant has regular and substantial caring + # responsibilities for a severely disabled person" (reg. 30). + claimant = person( + "is_uc_assessed_claimant", period + ) & ~other_member_of_single_claim(person, period) + carer = claimant & person("is_carer_for_benefits", period) + limited_capability = claimant & person("uc_limited_capability_for_WRA", period) + # Reg. 29(4): a carer with limited capability "(and, in the case of + # joint claimants, the LCWRA element has not been included in respect + # of the other claimant)" has "only the LCWRA element". The award has + # one LCWRA element (reg. 27(4)); in respect of whom it is included + # matters only when it is included. + lcwra_included = benunit("uc_LCWRA_element", period) > 0 + lcwra_rank = person.get_rank( + benunit, where(carer, 1_000, 0) - age, condition=limited_capability + ) + lcwra_claimant = limited_capability & lcwra_included & (lcwra_rank == 0) + qualifies = carer & ~lcwra_claimant + # Reg. 29(2): both joint claimants "if they both qualify for it, but + # only if they are not caring for the same severely disabled person"; + # reg. 29(3): otherwise one, by election or the Secretary of State. + same_person = benunit("partners_care_for_same_severely_disabled_person", period) + carer_rank = person.get_rank(benunit, -age, condition=qualifies) + return qualifies & (~same_person | (carer_rank == 0)) 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 f35e6103f1..76122045e0 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 @@ -8,11 +8,26 @@ class uc_LCWRA_element(Variable): value_type = float entity = BenUnit label = "Universal Credit limited capability for work-related-activity element" + documentation = ( + "The amount an award includes because a claimant has limited " + "capability for work and work-related activity. Only a claimant's " + "limited capability counts (`is_uc_assessed_claimant`, less the " + "other member of a single claim): a " + "disabled child or qualifying young person gives the award the " + "disabled child addition instead (`uc_individual_disabled_child_element`), " + "and 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. " + "Joint claimants who both have limited capability have one element." + ) definition_period = YEAR unit = GBP reference = [ dict( - title="Universal Credit Regulations 2013 reg. 27(1)", + title="Welfare Reform Act 2012 s. 12(2)(b)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/12", + ), + dict( + title="Universal Credit Regulations 2013 reg. 27(1) and (4)", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/27", ), dict( @@ -24,11 +39,13 @@ class uc_LCWRA_element(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.universal_credit.elements.disabled 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 + # Reg. 27(1): the element is "in respect of the fact that a claimant + # has limited capability for work and work-related activity". + claimant = person( + "is_uc_assessed_claimant", period + ) & ~other_member_of_single_claim(person, period) + limited_capability = claimant & person("uc_limited_capability_for_WRA", period) + # Reg. 27(4)(a): "In the case of joint claimants, where each of them + # has limited capability for work and work-related activity, the + # award is only to include one LCWRA element". + return benunit.any(limited_capability) * p.amount * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_WRA.py b/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_WRA.py index 8016ba9a63..cb195c4cae 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_WRA.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_WRA.py @@ -16,10 +16,8 @@ class uc_limited_capability_for_WRA(Variable): "does not include the support component (esa_includes_support_" "component) is taken not to have it, whatever the flag, though someone " "still in the ESA assessment phase may yet be found to. The model does " - "not infer it the other way, from the support component alone: the " - "Universal Credit LCWRA element and work allowance still read every " - "member of the benefit unit, not only the claimants. Users can set it " - "directly." + "not infer it the other way, from the support component alone. Users " + "can set it directly." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_work.py b/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_work.py index f7e27613a1..7be3da8691 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_work.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/disability_element/limited_work_ability/uc_limited_capability_for_work.py @@ -17,8 +17,7 @@ class uc_limited_capability_for_work(Variable): "work-related activity but not for work. Universal Credit also takes " "an ESA determination of limited capability for work (reg 39(1)(a)), " "so any ESA award of the person's own would imply it; the model does " - "not infer that yet, because the work allowance still reads every " - "member of the benefit unit, not only the claimants." + "not infer that." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py index f977ba70d8..ed06412407 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_allowance/is_uc_work_allowance_eligible.py @@ -10,9 +10,17 @@ class is_uc_work_allowance_eligible(Variable): def formula(benunit, period, parameters): person = benunit.members - # UC Regs 2013 reg 22(1)(b): responsibility for a child or qualifying - # young person, or limited capability for work. - has_LCW = benunit.any(person("uc_limited_capability_for_work", period)) + # Reg. 22(1)(b)(i): there is no work allowance "where a single + # claimant does not have, or neither of joint claimants has, + # responsibility for a child or qualifying young person or limited + # capability for work". A dependant's limited capability is not the + # claimant's. Where a member of a couple claims as a single person, + # the deduction is the one joint claimants would have (reg. 22(3)), + # so the partner's limited capability counts. + has_LCW = benunit.any( + person("is_uc_assessed_claimant", period) + & person("uc_limited_capability_for_work", period) + ) has_children = benunit( "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", period,