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..5a9758fb58 --- /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' circumstances only (UC Regs 2013 regs 27(1), 29(1) and 22(1)(b)): a disabled child gave the claimant the LCWRA element on top of the disabled child addition, a child or qualifying young person who was a carer gave the claimant the carer element, and a dependant's limited capability gave a work allowance. Joint claimants who both have limited capability now have one LCWRA element (reg. 27(4)), each joint claimant who is a carer has a carer element, and a carer who has the LCWRA element does not also have the carer element (reg. 29(2) and (4)). The disability basic income contrib reform's LCWRA route follows: only a claimant with limited capability qualifies. 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..e43469a8f4 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,335 @@ -- 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: - benunit_has_carer: false + age: 40 + care_hours: 0 output: uc_carer_element: 0 -- name: Carer gets element +- name: A single claimant caring 35 hours a week has the carer element, 2025-26 period: 2025 - absolute_error_margin: 100 + 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: + 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." The model +# does not record who is cared for and takes two carers to care for different +# people. + +- name: Joint claimants who are both carers 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] + output: + uc_carer_element: 5_024.16 # 209.34 * 12 * 2 + +# 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: 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: - benunit_has_carer: true + 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: 2417 + is_carer_for_benefits: [true, true] + uc_carer_element: 2_512.08 # 209.34 * 12, once + receives_uc_carer_element: [true, false] 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 d06e4aac78..94ac72cabe 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 @@ -23,3 +35,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/test_uc_claimant_elements_properties.py b/policyengine_uk/tests/test_uc_claimant_elements_properties.py new file mode 100644 index 0000000000..3194913ce9 --- /dev/null +++ b/policyengine_uk/tests/test_uc_claimant_elements_properties.py @@ -0,0 +1,335 @@ +"""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. The model includes the same number of each element for + all sixteen combinations, whatever the dependants' circumstances. 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)). +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): + """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. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + members = [] + for j, claimant in enumerate(unit["claimants"]): + values = {**claimant, **(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} + 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)) + 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_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 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..1c6410ba1d 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 @@ -13,8 +13,13 @@ 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_single_or_joint_claimant", 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/carer_element/receives_uc_carer_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/carer_element/receives_uc_carer_element.py index 4ef487bd6c..c175b2568f 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,16 @@ 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 " - "Credit award includes the carer element (Universal Credit " - "Regulations 2013 reg 29). The carer element is a benefit-unit " + "Whether this person is a claimant who is a carer, in a benefit unit " + "whose 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." + "since uc_carer_element is calculated whether or not there is one. " + "The element is for a claimant (reg 29(1)), so a child or qualifying " + "young person who cares does not receive it. Where both joint " + "claimants are carers with limited capability for work and " + "work-related activity, the award has one carer element (reg 29(4)) " + "and the model does not say whose, so it attributes it to both." ) definition_period = YEAR reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/29" @@ -21,4 +25,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 + claimant_carer = person("is_uc_single_or_joint_claimant", period) & person( + "is_carer_for_benefits", period + ) + return claimant_carer & 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..272f205487 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,21 +1,38 @@ 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. " + "Only a claimant's caring counts (`is_uc_single_or_joint_claimant`): a " + "child or qualifying young person who is a carer does not give the " + "claimants the element, nor does a partner who cannot be a joint " + "claimant (reg. 3(3)). Joint claimants who are both carers have an " + "element each; the model does not record who is cared for, and takes " + "two carers to care for different people. A claimant who is a carer " + "and has limited capability for work and work-related activity has " + "the LCWRA element instead, unless that element is included for the " + "other joint 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(1), (2) and (4)", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/29", ), + dict( + title="Universal Credit Regulations 2013 reg. 30", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/30", + ), dict( title="Universal Credit Regulations 2013 reg. 36(3)", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/36", @@ -25,13 +42,28 @@ 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 + # Reg. 29(1): the element is included "where a claimant has regular + # and substantial caring responsibilities for a severely disabled + # person" (reg. 30: `is_carer_for_benefits`). + claimant = person("is_uc_single_or_joint_claimant", period) + carer = claimant & person("is_carer_for_benefits", period) + limited_capability = claimant & person("uc_limited_capability_for_WRA", period) + # Reg. 29(2): "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". + carers_without_limited_capability = benunit.sum(carer & ~limited_capability) + # Reg. 29(4): where the carer "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". Where both joint claimants have limited capability the + # award has one LCWRA element (reg. 27(4)), for one of them, which + # leaves a carer element for the other. + carers_with_limited_capability = benunit.sum(carer & limited_capability) + claimants_with_limited_capability = benunit.sum(limited_capability) + carers_keeping_the_carer_element = min_( + carers_with_limited_capability, + max_(claimants_with_limited_capability - 1, 0), + ) + elements = carers_without_limited_capability + carers_keeping_the_carer_element + return elements * p.amount * MONTHS_IN_YEAR 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..dee0751142 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,18 +1,29 @@ 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): 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_single_or_joint_claimant`): 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 +35,11 @@ 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_single_or_joint_claimant", 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/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 46dbed7c6d..3597950c11 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,7 +10,17 @@ class is_uc_work_allowance_eligible(Variable): def formula(benunit, period, parameters): person = benunit.members - has_LCWRA = benunit.any(person("uc_limited_capability_for_WRA", 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_LCWRA = benunit.any( + person("is_uc_assessed_claimant", period) + & person("uc_limited_capability_for_WRA", period) + ) has_children = benunit( "is_responsible_for_child_or_qualifying_young_person_for_universal_credit", period,