From 43f2deaba92d473cfe09d8b61f2e47a9061ab102 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 11:01:39 -0400 Subject: [PATCH 1/9] Apply the UC minimum income floor only to the all work-related requirements group UC Regs 2013 reg. 62(1)(b) applies the floor to a claimant who would, apart from regs. 62 and 90, fall within Welfare Reform Act 2012 s. 22. Add the work-related group (ss. 19 to 21, regs. 89 and 91), the responsible carer nomination (s. 19(6), reg. 86), the expected hours of reg. 88, and the individual and couple thresholds of reg. 90(2) and (3). Co-Authored-By: Claude Opus 5.5 --- .../uc-mif-work-related-groups.added.md | 1 + .../uc-mif-work-related-groups.fixed.md | 1 + .../programs/gov/dwp/universal-credit.ipynb | 2 +- .../no_requirements_partner_hours.yaml | 24 ++ ...erview_or_preparation_threshold_hours.yaml | 13 + .../child_age/interview_only.yaml | 22 + .../child_age/no_requirements.yaml | 11 + .../child_age/work_preparation.yaml | 21 + .../below_compulsory_school_age.yaml | 31 ++ .../expected_hours/child_age_limit.yaml | 11 + .../expected_hours/compulsory_school_age.yaml | 25 ++ .../income/income_floor/uc_MIF_applies.yaml | 208 ++++++++++ .../income_floor/uc_mif_earned_income.yaml | 362 ++++++++++++++++- .../income_floor/uc_minimum_income_floor.yaml | 159 ++++++++ .../uc_expected_hours.yaml | 87 ++++ .../uc_is_responsible_carer.yaml | 153 +++++++ ...ork_related_group_apart_from_earnings.yaml | 276 +++++++++++++ .../test_uc_work_related_groups_properties.py | 384 ++++++++++++++++++ .../utils/uc_work_related_requirements.py | 339 ++++++++++++++++ .../income/income_floor/uc_mif_applies.py | 25 +- .../uc_minimum_income_floor_gross.py | 56 ++- .../uc_minimum_income_floor_income_tax.py | 19 +- ...minimum_income_floor_national_insurance.py | 40 +- .../income/uc_individual_earned_income.py | 53 +-- .../uc_expected_hours.py | 35 ++ .../uc_has_limited_capability_for_work.py | 17 + .../uc_is_adopter_in_first_year.py | 19 + ...er_parent_or_new_friend_or_family_carer.py | 21 + .../uc_is_in_gainful_self_employment.py | 24 ++ ...in_pregnancy_or_post_confinement_period.py | 17 + .../uc_is_ineligible_partner.py | 22 + .../uc_is_responsible_carer.py | 50 +++ ...nsible_foster_parent_of_child_under_one.py | 17 + ...udent_with_no_work_related_requirements.py | 21 + ..._work_related_group_apart_from_earnings.py | 69 ++++ .../uc_youngest_child_age.py | 39 ++ 36 files changed, 2559 insertions(+), 115 deletions(-) create mode 100644 changelog.d/uc-mif-work-related-groups.added.md create mode 100644 changelog.d/uc-mif-work-related-groups.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/no_requirements_partner_hours.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/interview_or_preparation_threshold_hours.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/interview_only.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/no_requirements.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/work_preparation.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/below_compulsory_school_age.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/child_age_limit.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/compulsory_school_age.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_expected_hours.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml create mode 100644 policyengine_uk/tests/test_uc_work_related_groups_properties.py create mode 100644 policyengine_uk/utils/uc_work_related_requirements.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_has_limited_capability_for_work.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_adopter_in_first_year.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_foster_parent_or_new_friend_or_family_carer.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_gainful_self_employment.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_pregnancy_or_post_confinement_period.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_foster_parent_of_child_under_one.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_student_with_no_work_related_requirements.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py diff --git a/changelog.d/uc-mif-work-related-groups.added.md b/changelog.d/uc-mif-work-related-groups.added.md new file mode 100644 index 0000000000..072fa3d93d --- /dev/null +++ b/changelog.d/uc-mif-work-related-groups.added.md @@ -0,0 +1 @@ +Add `uc_work_related_group_apart_from_earnings`, `uc_is_responsible_carer`, `uc_expected_hours`, `uc_youngest_child_age` and `uc_is_in_gainful_self_employment`; the inputs `uc_has_limited_capability_for_work`, `uc_is_in_pregnancy_or_post_confinement_period`, `uc_is_adopter_in_first_year`, `uc_is_student_with_no_work_related_requirements`, `uc_is_responsible_foster_parent_of_child_under_one`, `uc_is_foster_parent_or_new_friend_or_family_carer` and `uc_is_ineligible_partner`; and the parameters under `gov.dwp.universal_credit.work_requirements` for the responsible carer's child ages and expected hours and the 16-hour threshold, with `gov.dwp.universal_credit.means_test.minimum_income_floor.no_requirements_partner_hours`. diff --git a/changelog.d/uc-mif-work-related-groups.fixed.md b/changelog.d/uc-mif-work-related-groups.fixed.md new file mode 100644 index 0000000000..d9d1bb74ef --- /dev/null +++ b/changelog.d/uc-mif-work-related-groups.fixed.md @@ -0,0 +1 @@ +Apply the Universal Credit minimum income floor only to claimants who would otherwise be subject to all work-related requirements (UC Regs 2013 reg. 62(1)(b)): not to claimants with limited capability for work or work-related activity, carers, claimants over State Pension Credit qualifying age, the responsible carer of a child under 3, or the prescribed descriptions of regs. 89 and 91 (Welfare Reform Act 2012 ss. 19 to 21). Set the floor from the expected hours of reg. 88 (30 a week for the responsible carer of a child under 13, as DWP does, in place of 35), and build the couple threshold from each partner's own threshold (reg. 90(2) and (3)): 16 hours for a partner in the work-focused interview or work preparation group, nothing for a partner with no work-related requirements, and 35 hours at the national living wage for a partner who cannot be a joint claimant. diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index dd182e4bca..f71f4102f3 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -44,7 +44,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: the maximum entitlement is reduced by the benunit's countable earnings above its work allowance (`uc_work_allowance`) at the published taper rate. Each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant with a self-employment profit or loss outside a start-up period is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor is the minimum wage for their age for 35 hours a week (reg 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, the sum of both partners' floors, and never above it (reg 62(3)). The combined earnings above the work allowance are then tapered (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter, currently 55%), plus all of its unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, contribution-based JSA, savings, dividend and property income, and the tariff income deemed from capital (reg 72), which replaces the actual yield of the capital it is charged on. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." + "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: the maximum entitlement is reduced by the benunit's countable earnings above its work allowance (`uc_work_allowance`) at the published taper rate. Each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where no nomination is supplied the model takes the one that gives the couple the higher award. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or 30 for the responsible carer of a child under 13; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, and nothing for a partner with no work-related requirements for a reason other than earnings. The combined earnings above the work allowance are then tapered (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter, currently 55%), plus all of its unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, contribution-based JSA, savings, dividend and property income, and the tariff income deemed from capital (reg 72), which replaces the actual yield of the capital it is charged on. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." }, { "cell_type": "code", diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/no_requirements_partner_hours.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/no_requirements_partner_hours.yaml new file mode 100644 index 0000000000..753c92ba29 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/no_requirements_partner_hours.yaml @@ -0,0 +1,24 @@ +description: >- + Weekly hours behind the amount that a joint claimant subject to no + work-related requirements, for a reason other than their earnings, adds to + the couple threshold for the Universal Credit minimum income floor. + Regulation 90(3)(a) makes the couple threshold "the sum of their individual + thresholds", and regulation 90(2) sets an individual threshold only for a + claimant who would otherwise be in the work-focused interview or work + preparation group (16 hours) or the all work-related requirements group (the + expected hours). It sets none for a claimant with limited capability for + work-related activity, a carer, the responsible carer of a child under 1 or + a claimant over State Pension Credit qualifying age, and neither the Advice + for Decision Making nor DWP's guidance on the floor gives one. The model + follows the text: such a partner adds nothing. +values: + 2013-04-29: 0 +metadata: + unit: hour + period: week + label: Universal Credit minimum income floor hours for a partner with no work-related requirements + reference: + - title: The Universal Credit Regulations 2013 reg. 90(2) and (3)(a) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/90 + - title: DWP, Advice for Decision Making, chapter H4, H4073-H4077 + href: https://www.gov.uk/government/publications/advice-for-decision-making-staff-guide diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/interview_or_preparation_threshold_hours.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/interview_or_preparation_threshold_hours.yaml new file mode 100644 index 0000000000..b782be7265 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/interview_or_preparation_threshold_hours.yaml @@ -0,0 +1,13 @@ +description: >- + Weekly hours that set the individual earnings threshold of a Universal Credit + claimant who would otherwise be subject to the work-focused interview + requirement only, or to the work preparation requirement. +values: + 2013-04-29: 16 +metadata: + unit: hour + period: week + label: Universal Credit individual threshold hours for the interview-only and work preparation groups + reference: + - title: The Universal Credit Regulations 2013 reg. 90(2)(a) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/90 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/interview_only.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/interview_only.yaml new file mode 100644 index 0000000000..50faaf6c65 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/interview_only.yaml @@ -0,0 +1,22 @@ +description: >- + Age below which a child aged at least 1 puts their responsible carer in the + Universal Credit group subject to the work-focused interview requirement + only. The Act as passed covered a child "aged at least 1 and ... under a + prescribed age", prescribed as 5 and then, from 28 April 2014, as 3. Since 3 + April 2017 the Act covers a child "who is aged 1". +values: + 2013-04-29: 5 + 2014-04-28: 3 + 2017-04-03: 2 +metadata: + unit: year + label: Universal Credit responsible carer work-focused interview only child age limit + reference: + - title: Welfare Reform Act 2012 s. 20(1)(a) + href: https://www.legislation.gov.uk/ukpga/2012/5/section/20 + - title: The Universal Credit Regulations 2013 reg. 91(1), as made (prescribed age 5) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/91/made + - title: The Income Support (Work-Related Activity) and Miscellaneous Amendments Regulations 2014 reg. 16 (prescribed age 3) + href: https://www.legislation.gov.uk/uksi/2014/1097/regulation/16/made + - title: Welfare Reform and Work Act 2016 s. 17(1)(a) (a child aged 1, from 3 April 2017) + href: https://www.legislation.gov.uk/ukpga/2016/7/section/17 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/no_requirements.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/no_requirements.yaml new file mode 100644 index 0000000000..c9f1e094c1 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/no_requirements.yaml @@ -0,0 +1,11 @@ +description: >- + Age below which a child puts their responsible carer in the Universal Credit + group subject to no work-related requirements. +values: + 2013-04-29: 1 +metadata: + unit: year + label: Universal Credit responsible carer no work-related requirements child age limit + reference: + - title: Welfare Reform Act 2012 s. 19(2)(c) + href: https://www.legislation.gov.uk/ukpga/2012/5/section/19 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/work_preparation.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/work_preparation.yaml new file mode 100644 index 0000000000..6796488ab6 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/child_age/work_preparation.yaml @@ -0,0 +1,21 @@ +description: >- + Age below which a child too old for the work-focused interview only group + puts their responsible carer in the Universal Credit group subject to the + work preparation requirement. From 28 April 2014 the group took the + responsible carer of a child aged 3 or 4, and since 3 April 2017 it takes + the responsible carer of a child aged 2. Before 28 April 2014 a child's age + did not put anyone in this group. +values: + 2013-04-29: 5 + 2014-04-28: 5 + 2017-04-03: 3 +metadata: + unit: year + label: Universal Credit responsible carer work preparation child age limit + reference: + - title: Welfare Reform Act 2012 s. 21(1)(aa) + href: https://www.legislation.gov.uk/ukpga/2012/5/section/21 + - title: The Universal Credit Regulations 2013 reg. 91A, as inserted from 28 April 2014 (a child aged 3 or 4) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/91A/2014-04-28 + - title: Welfare Reform and Work Act 2016 s. 17(1)(b) and (2)(b) (a child aged 2, from 3 April 2017) + href: https://www.legislation.gov.uk/ukpga/2016/7/section/17 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/below_compulsory_school_age.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/below_compulsory_school_age.yaml new file mode 100644 index 0000000000..f2a83f235e --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/below_compulsory_school_age.yaml @@ -0,0 +1,31 @@ +description: >- + Expected weekly hours DWP uses for the responsible carer of a child who has + not yet reached compulsory school age. The regulation leaves the number to + the Secretary of State ("compatible with those caring responsibilities"). + Until 3 April 2017 one rule covered every child under 13 (25 hours), though + the responsible carer of a child under 5 was not then in the all + work-related requirements group. DWP's stated expectation from 3 April 2017 + was 16 hours for a child aged 3 or 4. For the minimum income floor it became + a maximum of 30 hours for lead carers found gainfully self-employed from 31 + January 2024, and DWP began raising existing floors to it from February + 2025, the date used here. Work coaches can set fewer hours for a claimant's + circumstances; the model uses the maximum. +values: + 2013-04-29: 25 + 2017-04-03: 16 + 2025-02-01: 30 +metadata: + unit: hour + period: week + label: Universal Credit expected hours for the responsible carer of a child below compulsory school age + reference: + - title: The Universal Credit Regulations 2013 reg. 88(2)(aa) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/88 + - title: The Employment and Support Allowance and Universal Credit (Miscellaneous Amendments and Transitional and Savings Provisions) Regulations 2017 reg. 6 + href: https://www.legislation.gov.uk/uksi/2017/204/regulation/6/made + - title: DWP, Advice for Decision Making, chapter J2, J2086 + href: https://www.gov.uk/government/publications/advice-for-decision-making-staff-guide + - title: "DWP, Gainfully self-employed lead carers with youngest child aged 3-12 (increase in expected hours), deposited paper DEP2024-0673" + href: https://data.parliament.uk/DepositedPapers/Files/DEP2024-0673/073_Gainfully_SE_lead_carer_youngest_child_age_3_12_expctd_hrs_V1.0.pdf + - title: Written answer 129349, 28 April 2026 + href: https://questions-statements.parliament.uk/written-questions/detail/2026-04-22/129349 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/child_age_limit.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/child_age_limit.yaml new file mode 100644 index 0000000000..187a2af21b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/child_age_limit.yaml @@ -0,0 +1,11 @@ +description: >- + Age below which a child lowers the expected hours of their responsible carer + for Universal Credit. +values: + 2013-04-29: 13 +metadata: + unit: year + label: Universal Credit responsible carer reduced expected hours child age limit + reference: + - title: The Universal Credit Regulations 2013 reg. 88(2)(b) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/88 diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/compulsory_school_age.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/compulsory_school_age.yaml new file mode 100644 index 0000000000..cd19795f3d --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/work_requirements/responsible_carer/expected_hours/compulsory_school_age.yaml @@ -0,0 +1,25 @@ +description: >- + Expected weekly hours DWP uses for the responsible carer of a child who has + reached compulsory school age and is under 13. The regulation leaves the + number to the Secretary of State ("compatible with the child's normal school + hours"). DWP's stated expectation was 25 hours. For the minimum income + floor it became a maximum of 30 hours for lead carers found gainfully + self-employed from 31 January 2024, and DWP began raising existing floors to + it from February 2025, the date used here. Work coaches can set fewer hours + for a claimant's circumstances; the model uses the maximum. +values: + 2013-04-29: 25 + 2025-02-01: 30 +metadata: + unit: hour + period: week + label: Universal Credit expected hours for the responsible carer of a school-age child under 13 + reference: + - title: The Universal Credit Regulations 2013 reg. 88(2)(b) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/88 + - title: DWP, Advice for Decision Making, chapter J2, J2086 + href: https://www.gov.uk/government/publications/advice-for-decision-making-staff-guide + - title: "DWP, Gainfully self-employed lead carers with youngest child aged 3-12 (increase in expected hours), deposited paper DEP2024-0673" + href: https://data.parliament.uk/DepositedPapers/Files/DEP2024-0673/073_Gainfully_SE_lead_carer_youngest_child_age_3_12_expctd_hrs_V1.0.pdf + - title: Written answer 129349, 28 April 2026 + href: https://questions-statements.parliament.uk/written-questions/detail/2026-04-22/129349 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml index 3ed15e32c0..e169936bb9 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_MIF_applies.yaml @@ -1,3 +1,9 @@ +# UC Regs 2013 reg. 62(1): "This regulation applies to a claimant who (a) is +# in gainful self-employment (see regulation 64); and (b) would, apart from +# this regulation or regulation 90, fall within section 22 of the Act +# (claimants subject to all work-related requirements)." Reg. 62(5) leaves +# out assessment periods in a start-up period. + - name: No self-employment income period: 2020 absolute_error_margin: 0 @@ -40,3 +46,205 @@ self_employment_income: -1 output: uc_mif_applies: true + +- name: A trader who breaks even is still in gainful self-employment + period: 2026 + absolute_error_margin: 0 + input: + self_employment_income: 0 + uc_is_in_gainful_self_employment: true + output: + # Reg. 64 turns on the trade, not on the profit. Without the input the + # model reads no profit or loss as no self-employment. + uc_mif_applies: true + +- name: Self-employment that is not gainful + period: 2026 + absolute_error_margin: 0 + input: + self_employment_income: 2_000 + uc_is_in_gainful_self_employment: false + output: + # ADM H4061: "The minimum income floor does not apply to claimants who + # are not gainfully S/E for UC purposes". + uc_mif_applies: false + +- name: Earnings above the threshold do not take the claimant out of the floor's scope + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 60_000 + output: + # "apart from ... regulation 90": the earnings threshold is ignored. The + # floor applies but changes nothing above the threshold. + uc_mif_applies: true + +- name: Limited capability for work and work-related activity (section 19) + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 3_000 + uc_limited_capability_for_WRA: true + output: + uc_mif_applies: false + +- name: Carer for 35 hours a week (section 19) + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 3_000 + care_hours: 35 + output: + uc_mif_applies: false + +- name: Carer whose earnings are above the Carer's Allowance limit (section 19) + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 18_000 + care_hours: 35 + output: + # Reg. 30(1)(a)(i): a person who would satisfy the conditions for + # Carer's Allowance "but for the fact that their earnings have exceeded + # the limit" still has regular and substantial caring responsibilities. + uc_mif_applies: false + +- name: Limited capability for work (section 21) + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 3_000 + uc_has_limited_capability_for_work: true + output: + uc_mif_applies: false + +- name: Pregnant claimant within 11 weeks of the expected week of confinement (section 19) + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 3_000 + uc_is_in_pregnancy_or_post_confinement_period: true + output: + uc_mif_applies: false + +- name: Foster parent (section 20) + period: 2026 + absolute_error_margin: 0 + input: + age: 30 + self_employment_income: 3_000 + uc_is_foster_parent_or_new_friend_or_family_carer: true + output: + uc_mif_applies: false + +- name: Lone parent of a baby (section 19) + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 30 + self_employment_income: 3_000 + child: + age: 0 + output: + # ADM H4061 example 3: when her son is born the claimant "moves into + # the no work-related requirements group. The minimum income floor no + # longer applies". + uc_mif_applies: [false, false] + +- name: Lone parent of a child aged 1 (section 20) + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 30 + self_employment_income: 3_000 + child: + age: 1 + output: + uc_mif_applies: [false, false] + +- name: Lone parent of a child aged 2 (section 21) + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 30 + self_employment_income: 3_000 + child: + age: 2 + output: + uc_mif_applies: [false, false] + +- name: Lone parent of a child aged 3 (section 22) + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 30 + self_employment_income: 3_000 + child: + age: 3 + output: + uc_mif_applies: [true, false] + +- name: Couple with a baby, both self-employed, only the responsible carer escapes the floor + period: 2026 + absolute_error_margin: 0 + input: + people: + nominated: + age: 30 + self_employment_income: 3_000 + uc_is_responsible_carer: true + partner: + age: 32 + self_employment_income: 3_000 + uc_is_responsible_carer: false + child: + age: 0 + output: + # Reg. 86(2): "Only one of joint claimants may be nominated as a + # responsible carer". + uc_mif_applies: [false, true, false] + +- name: Mixed-age couple, the partner over State Pension Credit qualifying age (section 19) + period: 2026 + absolute_error_margin: 0 + input: + people: + older: + age: 70 + self_employment_income: 3_000 + younger: + age: 40 + self_employment_income: 3_000 + output: + uc_mif_applies: [false, true] + +- name: A partner who cannot be a joint claimant is not a claimant + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + self_employment_income: 3_000 + partner: + age: 32 + self_employment_income: 3_000 + uc_is_ineligible_partner: true + output: + # ADM H4077 note: "the MIF is not applied to the ineligible claimant's + # actual earnings." + uc_mif_applies: [true, false] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml index 61a86ddfcb..6221c2b5ae 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml @@ -246,9 +246,9 @@ employment_income: 25_000 would_claim_marriage_allowance: false child_1: - age: 5 + age: 15 child_2: - age: 3 + age: 14 households: household: members: [self_employed, employee, child_1, child_2] @@ -260,17 +260,55 @@ # Employee: 25,000 - (25,000 - 12,570) x 20% - (25,000 - 12,569.96) x 8% # = 25,000 - 2,486 - 994.40. uc_individual_earned_income_before_mif: [6_000, 21_519.60, 0, 0] - # The couple threshold is 2 x 20,386.028 = 40,772.056. The employee is - # 1,133.57 over their own floor, so the self-employed partner is treated - # as having 20,386.03 - 1,133.57 = 40,772.056 - 21,519.597 = 19,252.46, - # and the couple has the couple threshold. + # The children are 13 or over, so both partners have 35 expected hours + # and the couple threshold is 2 x 20,386.028 = 40,772.056. The employee + # is 1,133.57 over their own floor, so the self-employed partner is + # treated as having 20,386.03 - 1,133.57 = 40,772.056 - 21,519.597 = + # 19,252.46, and the couple has the couple threshold. uc_individual_earned_income: [19_252.46, 21_519.60, 0, 0] # Less the work allowance of 427 x 12 = 5,124. uc_earned_income: 35_648.06 + +- name: Couple with children under 13, the responsible carer's floor uses 30 hours + period: 2026 + absolute_error_margin: 0.01 + input: + people: + self_employed: + age: 30 + self_employment_income: 6_000 + would_claim_marriage_allowance: false + employee: + age: 30 + employment_income: 25_000 + would_claim_marriage_allowance: false + child_1: + age: 5 + child_2: + age: 3 + households: + household: + members: [self_employed, employee, child_1, child_2] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 12_000 + output: + uc_is_responsible_carer: [true, false, false, false] + uc_mif_applies: [true, false, false, false] + uc_individual_earned_income_before_mif: [6_000, 21_519.60, 0, 0] + # The responsible carer's threshold is the 30-hour 17,940.624 and the + # partner's the 35-hour 20,386.028, so the couple threshold is + # 38,326.652. The self-employed partner is treated as having 17,940.62 + # less the 1,133.57 by which the employee is over their own threshold: + # 38,326.652 - 21,519.597 = 16,807.06. Before this change both + # thresholds used 35 hours and the amount was 19,252.46. + uc_individual_earned_income: [16_807.06, 21_519.60, 0, 0] + # Less the work allowance of 427 x 12 = 5,124. + uc_earned_income: 33_202.65 # 666.97 x 12 + 2 x 303.94 x 12 + 12,000. uc_maximum_amount: 27_298.20 - # 27,298.20 - 0.55 x 35,648.06. - universal_credit: 7_691.77 + # 27,298.20 - 0.55 x 33,202.65. + universal_credit: 9_036.74 - name: Floor does not apply to a dependent child with self-employment income period: 2026 @@ -356,3 +394,311 @@ # from the -3,000 case above. uc_mif_applies: false uc_individual_earned_income: 0 + +# Scope and thresholds by work-related group (reg. 62(1)(b), reg. 88(2), +# reg. 90(2) and (3)). 2026-27 amounts for a person aged 21 or over in +# England: 35-hour net floor 20,386.03; 30-hour net floor 17,940.62 +# (19,827.60 gross); 16-hour threshold 10,574.72 (below the personal +# allowance, so the same net). + +- name: Lone parent of a school-age child has the 30-hour floor + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + self_employment_income: 5_000 + child: + age: 5 + households: + household: + members: [parent, child] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_mif_applies: [true, false] + uc_individual_earned_income: [17_940.62, 0] + # Less the work allowance of 427 x 12 = 5,124. + uc_earned_income: 12_816.62 + # 424.90 x 12 + 303.94 x 12 + 9,600. + uc_maximum_amount: 18_346.08 + # 18,346.08 - 0.55 x 12,816.62. With the 35-hour floor of 20,386.03, as + # before this change, it was 9,951.96. + universal_credit: 11_296.94 + +- name: Lone parent of a child aged 1 has no floor + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + self_employment_income: 5_000 + child: + age: 1 + households: + household: + members: [parent, child] + region: NORTH_EAST + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_mif_applies: [false, false] + uc_individual_earned_income: [5_000, 0] + # 5,000 is under the work allowance of 5,124. + uc_earned_income: 0 + universal_credit_pre_benefit_cap: 18_346.08 + +- name: Partner with no work-related requirements adds nothing to the couple threshold + period: 2026 + absolute_error_margin: 0.01 + input: + people: + self_employed: + age: 30 + self_employment_income: 5_000 + would_claim_marriage_allowance: false + partner: + age: 30 + uc_limited_capability_for_WRA: true + would_claim_marriage_allowance: false + households: + household: + members: [self_employed, partner] + region: NORTH_EAST + output: + uc_minimum_income_floor: [20_386.03, 0] + # The couple threshold is the claimant's own threshold; the partner has + # no earned income, so the floor is not reduced. + uc_individual_earned_income: [20_386.03, 0] + +- name: Earnings of a partner with no work-related requirements reduce the floor pound for pound + period: 2026 + absolute_error_margin: 0.01 + input: + people: + self_employed: + age: 30 + self_employment_income: 5_000 + would_claim_marriage_allowance: false + partner: + age: 30 + employment_income: 7_200 + uc_limited_capability_for_WRA: true + would_claim_marriage_allowance: false + households: + household: + members: [self_employed, partner] + region: NORTH_EAST + output: + uc_mif_applies: [true, false] + # 7,200 is below the personal allowance and the primary threshold. + uc_individual_earned_income_before_mif: [5_000, 7_200] + # Reg. 62(3): the threshold "minus any amount by which that amount of + # earned income combined with their partner's earned income would + # exceed the couple threshold": 20,386.03 - (20,386.03 + 7,200 - + # 20,386.03). + uc_individual_earned_income: [13_186.03, 7_200] + +- name: Mixed-age couple, the older partner's earnings reduce the floor + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + employment_income: 6_000 + would_claim_marriage_allowance: false + self_employed: + age: 40 + self_employment_income: 3_600 + would_claim_marriage_allowance: false + households: + household: + members: [older, self_employed] + region: NORTH_EAST + output: + uc_mif_applies: [false, true] + uc_minimum_income_floor: [0, 20_386.03] + # 20,386.03 - 6,000. + uc_individual_earned_income: [6_000, 14_386.03] + +- name: Partner in the work-focused interview group adds a 16-hour threshold + period: 2026 + absolute_error_margin: 0.01 + input: + people: + self_employed: + age: 30 + self_employment_income: 4_800 + would_claim_marriage_allowance: false + uc_is_responsible_carer: false + carer: + age: 30 + employment_income: 12_000 + would_claim_marriage_allowance: false + uc_is_responsible_carer: true + child: + age: 1 + households: + household: + members: [self_employed, carer, child] + region: NORTH_EAST + output: + uc_mif_applies: [true, false, false] + uc_minimum_income_floor: [20_386.03, 10_574.72, 0] + # 12,000 is below the personal allowance and the primary threshold. It + # is over the carer's 16-hour threshold, which would put them in section + # 19 by earnings (reg. 90(1)); the threshold still counts at 16 hours, + # as reg. 90(2)(a) asks where they "would otherwise" fall. + uc_individual_earned_income_before_mif: [4_800, 12_000, 0] + # Couple threshold 20,386.028 + 10,574.72 = 30,960.748. The floor is + # 20,386.03 - (20,386.03 + 12,000 - 30,960.75) = 30,960.75 - 12,000. + uc_individual_earned_income: [18_960.75, 12_000, 0] + +- name: Partner in the work preparation group who is also self-employed has no floor + period: 2026 + absolute_error_margin: 0.01 + input: + people: + self_employed: + age: 30 + self_employment_income: 4_800 + would_claim_marriage_allowance: false + uc_is_responsible_carer: false + carer: + age: 30 + self_employment_income: 2_400 + would_claim_marriage_allowance: false + uc_is_responsible_carer: true + child: + age: 2 + households: + household: + members: [self_employed, carer, child] + region: NORTH_EAST + output: + uc_mif_applies: [true, false, false] + # 20,386.03 + 2,400 is below the couple threshold of 30,960.75, so the + # floor is not reduced. + uc_individual_earned_income: [20_386.03, 2_400, 0] + +- name: Couple with a baby, both self-employed, default nomination + period: 2026 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 35 + self_employment_income: 15_000 + would_claim_marriage_allowance: false + younger: + age: 30 + self_employment_income: 2_000 + would_claim_marriage_allowance: false + child: + age: 0 + households: + household: + members: [elder, younger, child] + region: NORTH_EAST + output: + # Elder: 15,000 - (15,000 - 1,000 - 12,570) x 20% - (15,000 - 12,570) + # x 6% = 14,568.20. + uc_individual_earned_income_before_mif: [14_568.20, 2_000, 0] + # The responsible carer of a child under 1 is in section 19 and adds + # nothing to the couple threshold, so the couple threshold is the other + # partner's 20,386.03 and the couple is treated as having 20,386.03 + # whichever is nominated: the elder by default. + uc_is_responsible_carer: [true, false, false] + uc_mif_applies: [false, true, false] + # 20,386.03 - 14,568.20. + uc_individual_earned_income: [14_568.20, 5_817.83, 0] + +- name: Partner who cannot be a joint claimant, earnings below the 35-hour amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + self_employment_income: 4_800 + would_claim_marriage_allowance: false + partner: + age: 30 + employment_income: 14_400 + uc_is_ineligible_partner: true + would_claim_marriage_allowance: false + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + uc_mif_applies: [true, false] + # Partner: 14,400 - (14,400 - 12,570) x 20% - (14,400 - 12,569.96) x 8% + # = 14,400 - 366 - 146.40. + uc_individual_earned_income_before_mif: [4_800, 13_887.60] + # Reg. 90(3)(b): the couple threshold is the claimant's threshold plus + # the pay for 35 hours at the national living wage, 2 x 20,386.028 net. + # 20,386.03 + 13,887.60 is below it, so the floor is not reduced. + uc_individual_earned_income: [20_386.03, 13_887.60] + +- name: Partner who cannot be a joint claimant, earnings above the 35-hour amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + self_employment_income: 4_800 + would_claim_marriage_allowance: false + partner: + age: 30 + employment_income: 30_000 + self_employment_income: 500 + uc_is_ineligible_partner: true + would_claim_marriage_allowance: false + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # The floor never applies to the partner (ADM H4077 note). + uc_mif_applies: [true, false] + # Partner: profits of 500 are within the 1,000 trading allowance, so + # 30,500 - (30,000 - 12,570) x 20% - (30,000 - 12,569.96) x 8% + # = 30,500 - 3,486 - 1,394.40. + uc_individual_earned_income_before_mif: [4_800, 25_619.60] + # 40,772.056 - 25,619.597. + uc_individual_earned_income: [15_152.46, 25_619.60] + +- name: Claimant aged 19 whose partner aged 17 cannot be a joint claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 19 + is_uc_claimant: true + self_employment_income: 3_600 + would_claim_marriage_allowance: false + partner: + age: 17 + is_uc_claimant: true + employment_income: 24_000 + uc_is_ineligible_partner: true + would_claim_marriage_allowance: false + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # Partner: 24,000 - (24,000 - 12,570) x 20% - (24,000 - 12,569.96) x 8% + # = 24,000 - 2,286 - 914.40. + uc_individual_earned_income_before_mif: [3_600, 20_799.60] + # Claimant's threshold 17,880.98 (10.85 an hour); the partner's amount + # uses the national living wage, not the 8.00 for a 17-year-old: + # 20,386.028. Couple threshold 38,267.008, less 20,799.597. + uc_individual_earned_income: [17_467.41, 20_799.60] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml index 827cb3912a..068f0f7079 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml @@ -90,6 +90,10 @@ input: age: 70 region: NORTH_EAST + # A claimant over State Pension Credit qualifying age is subject to no + # work-related requirements and has no threshold; the group is set here + # to isolate the deduction. + uc_work_related_group_apart_from_earnings: ALL_REQUIREMENTS output: uc_minimum_income_floor_gross: 23_132.20 uc_minimum_income_floor_income_tax: 2_112.44 @@ -123,3 +127,158 @@ output: # 7.20 x 35 x 52. uc_minimum_income_floor_gross: 13_104 + +# Individual thresholds by work-related group and expected hours. Reg. 90(2): +# "(a) 16 hours per week, in the case of a claimant who would otherwise fall +# within section 20 ... or section 21 ...; or (b) the expected number of hours +# per week in the case of a claimant who would otherwise fall within section +# 22". 2026 amounts for a person aged 21 or over: 30 hours 12.71 x 30 x 52 = +# 19,827.60; 16 hours 12.71 x 16 x 52 = 10,574.72. + +- name: Responsible carer of a school-age child has a 30-hour threshold in 2026 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + child: + age: 5 + households: + household: + members: [parent, child] + region: NORTH_EAST + output: + uc_expected_hours: [30, 35] + # 12.71 x 30 x 52, or 1,652.30 a month. + uc_minimum_income_floor_gross: [19_827.60, 0] + # (19,827.60 - 12,570) x 20%. + uc_minimum_income_floor_income_tax: [1_451.52, 0] + # (19,827.60 - 12,570) x 6%. + uc_minimum_income_floor_national_insurance: [435.46, 0] + # 19,827.60 - 1,451.52 - 435.46, or 1,495.05 a month. + uc_minimum_income_floor: [17_940.62, 0] + +- name: A claimant in the work-focused interview group has a 16-hour threshold + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + child: + age: 1 + households: + household: + members: [parent, child] + region: NORTH_EAST + output: + # 12.71 x 16 x 52, or 881.23 a month: below the personal allowance and + # the Class 4 lower profits limit, so the net amount is the same. + uc_minimum_income_floor_gross: [10_574.72, 0] + uc_minimum_income_floor_income_tax: [0, 0] + uc_minimum_income_floor_national_insurance: [0, 0] + uc_minimum_income_floor: [10_574.72, 0] + +- name: A claimant aged 19 in the work preparation group has a 16-hour threshold at the rate for their age + period: 2026 + absolute_error_margin: 0.01 + input: + age: 19 + uc_has_limited_capability_for_work: true + output: + # 10.85 x 16 x 52. + uc_minimum_income_floor_gross: 9_027.20 + +- name: ADM J2082, Alison, aged 20 with limited capability for work at 5.30 an hour + period: 2016 + absolute_error_margin: 0.01 + input: + age: 20 + uc_has_limited_capability_for_work: true + output: + # "Alison's individual earnings threshold is 5.30 X 16 = 84.80, converted + # to a monthly amount - 84.80 x 52 / 12 = 367 per month": 367.47 before + # the whole-pound rounding of reg. 6(1A), or 4,409.60 a year. + uc_minimum_income_floor_gross: 4_409.60 + +- name: ADM J2086 example 1, Richard, lone parent of a three-year-old at 7.50 an hour + period: 2017 + absolute_error_margin: 0.01 + input: + people: + richard: + age: 27 + cassius: + age: 3 + output: + uc_expected_hours: [16, 35] + # "7.50 x 16 = 120 x 52 / 12 = 520": 6,240 a year. + uc_minimum_income_floor_gross: [6_240, 0] + +- name: ADM J2086 example 2, Laura, responsible carer of a nine-year-old at 7.05 an hour + period: 2017 + absolute_error_margin: 0.01 + input: + people: + laura: + age: 23 + son: + age: 9 + output: + uc_expected_hours: [25, 35] + # "7.05 x 25 = 176.25 x 52 / 12": 763.75 a month (the ADM prints + # 763.25, and 763 after rounding down), or 9,165 a year. + uc_minimum_income_floor_gross: [9_165, 0] + +- name: ADM J2085 example, Louise, expected hours of 30 agreed for her circumstances + period: 2017 + absolute_error_margin: 0.01 + input: + people: + louise: + age: 40 + uc_expected_hours: 30 + daughter: + age: 15 + output: + # "7.50 x 30 = 225 x 52 / 12 = 975": 11,700 a year. Her daughter is 15, + # so the hours are the work coach's (reg. 88(2)(a)) and come in as an + # input. + uc_minimum_income_floor_gross: [11_700, 0] + +- name: A claimant subject to no work-related requirements has no threshold + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + uc_limited_capability_for_WRA: true + output: + # Reg. 90(2) sets a threshold only for a claimant who would otherwise + # fall within section 20, 21 or 22. + uc_minimum_income_floor_gross: 0 + uc_minimum_income_floor: 0 + +- name: A partner aged 17 who cannot be a joint claimant adds 35 hours at the national living wage + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 19 + is_uc_claimant: true + partner: + age: 17 + is_uc_claimant: true + uc_is_ineligible_partner: true + households: + household: + members: [claimant, partner] + region: NORTH_EAST + output: + # Reg. 90(3)(b)(ii): "the amount a person would be paid for 35 hours + # per week at the hourly rate specified in regulation 4 of the National + # Minimum Wage Regulations", 12.71 whatever the partner's age (the rate + # for a 17-year-old is 8.00). The claimant: 10.85 x 35 x 52. + uc_minimum_income_floor_gross: [19_747, 23_132.20] + uc_minimum_income_floor: [17_880.98, 20_386.03] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_expected_hours.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_expected_hours.yaml new file mode 100644 index 0000000000..1e5363f6f5 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_expected_hours.yaml @@ -0,0 +1,87 @@ +# UC Regs 2013 reg. 88(1): the expected number of hours per week "is 35 +# unless some lesser number of hours applies under paragraph (2)". The lesser +# numbers are whatever "the Secretary of State considers is compatible" with +# caring for a child below compulsory school age (reg. 88(2)(aa)) or with the +# normal school hours of a child under 13 (reg. 88(2)(b)). The model uses the +# hours DWP says it uses: "self-employed individuals with children aged 3-12 +# typically have their Minimum Income Floor set using a maximum of 30 hours +# per week" (written answer 129349, 28 April 2026); before DWP raised +# existing floors from February 2025, 16 hours for a child aged 3 or 4 and 25 +# for a school-age child (Advice for Decision Making J2086). + +- name: A claimant with no child is expected to work 35 hours + period: 2026 + input: + age: 30 + output: + uc_expected_hours: 35 + +- name: Responsible carer of a child aged 3 in 2026 + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 3 + output: + uc_expected_hours: [30, 35] + +- name: Responsible carer of a child aged 12 in 2026 + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 12 + output: + uc_expected_hours: [30, 35] + +- name: A child aged 13 does not lower the expected hours + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 13 + output: + uc_expected_hours: [35, 35] + +- name: In a couple only the responsible carer has the lesser hours + period: 2026 + input: + people: + nominated: + age: 30 + uc_is_responsible_carer: true + partner: + age: 32 + uc_is_responsible_carer: false + child: + age: 7 + output: + uc_expected_hours: [30, 35, 35] + +- name: Responsible carer of a child aged 4 before DWP raised the hours + period: 2024 + input: + people: + parent: + age: 30 + child: + age: 4 + output: + uc_expected_hours: [16, 35] + +- name: Responsible carer of a school-age child before DWP raised the hours + period: 2024 + input: + people: + parent: + age: 30 + child: + age: 9 + output: + uc_expected_hours: [25, 35] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml new file mode 100644 index 0000000000..19cf35154b --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml @@ -0,0 +1,153 @@ +# Welfare Reform Act 2012 s. 19(6): the responsible carer for a child is "a +# single person who is responsible for the child", or "a person who is a +# member of a couple where (i) the person or the other member of the couple +# is responsible for the child, and (ii) the person has been nominated by the +# couple jointly as responsible for the child". UC Regs 2013 reg. 86(2): +# "Only one of joint claimants may be nominated", and the nomination "applies +# to all the children" (reg. 86(3)). +# +# The nomination is the couple's choice and data seldom record it. Without +# an input the model takes the nomination that leaves the couple the lower +# combined earned income after the minimum income floor (the higher award), +# and the elder claimant where it makes no difference. +# +# 2026-27 amounts for a claimant aged 21 or over in England: net floor +# 20,386.03 for 35 hours (see uc_minimum_income_floor.yaml); 16-hour +# threshold 12.71 x 16 x 52 = 10,574.72, below the personal allowance. + +- name: A claimant with no child is not a responsible carer + period: 2026 + input: + age: 30 + output: + uc_is_responsible_carer: false + +- name: A lone parent is the responsible carer + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 4 + output: + uc_is_responsible_carer: [true, false] + +- name: A couple with no floor at stake default to the elder claimant + period: 2026 + input: + people: + younger: + age: 30 + employment_income: 20_000 + elder: + age: 35 + child: + age: 4 + output: + uc_is_responsible_carer: [false, true, false] + +- name: A couple nominate the self-employed partner the floor would otherwise apply to + period: 2026 + input: + people: + elder: + age: 35 + employment_income: 12_000 + self_employed: + age: 30 + self_employment_income: 5_000 + child: + age: 1 + output: + # Nominating the self-employed partner puts them in section 20, so no + # floor: combined earned income 17,000. Nominating the elder leaves the + # self-employed partner in section 22 with the floor. + uc_is_responsible_carer: [false, true, false] + uc_mif_applies: [false, false, false] + uc_individual_earned_income: [12_000, 5_000, 0] + +- name: Two self-employed partners default to the nomination that leaves the lower combined earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 35 + self_employment_income: 15_000 + would_claim_marriage_allowance: false + younger: + age: 30 + self_employment_income: 2_000 + would_claim_marriage_allowance: false + child: + age: 1 + households: + household: + members: [elder, younger, child] + region: NORTH_EAST + output: + # Elder: 15,000 - (15,000 - 1,000 - 12,570) x 20% - (15,000 - 12,570) + # x 6% = 15,000 - 286 - 145.80. + uc_individual_earned_income_before_mif: [14_568.20, 2_000, 0] + # Couple threshold either way: 20,386.03 + 10,574.72 = 30,960.75. + # Nominating the younger: the elder is treated as having 20,386.03 and + # the couple has 22,386.03. Nominating the elder: the younger would be + # treated as having 30,960.75 - 14,568.20 = 16,392.55 and the couple + # would have 30,960.75. + uc_is_responsible_carer: [false, true, false] + uc_mif_applies: [true, false, false] + uc_individual_earned_income: [20_386.03, 2_000, 0] + +- name: A recorded nomination overrides the default + period: 2026 + absolute_error_margin: 0.01 + input: + people: + elder: + age: 35 + self_employment_income: 15_000 + would_claim_marriage_allowance: false + uc_is_responsible_carer: true + younger: + age: 30 + self_employment_income: 2_000 + would_claim_marriage_allowance: false + uc_is_responsible_carer: false + child: + age: 1 + households: + household: + members: [elder, younger, child] + region: NORTH_EAST + output: + uc_mif_applies: [false, true, false] + # 30,960.75 - 14,568.20. + uc_individual_earned_income: [14_568.20, 16_392.55, 0] + +- name: A child looked after by a local authority has no responsible carer + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 4 + is_looked_after_by_local_authority: true + output: + # UC Regs reg. 4(6)(a). + uc_is_responsible_carer: [false, false] + +- name: A claimant whose partner cannot be a joint claimant is the responsible carer + period: 2026 + input: + people: + partner: + age: 35 + uc_is_ineligible_partner: true + claimant: + age: 30 + child: + age: 4 + output: + uc_is_responsible_carer: [false, true, false] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml new file mode 100644 index 0000000000..3c3298e0af --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.yaml @@ -0,0 +1,276 @@ +# Welfare Reform Act 2012 ss. 19 to 22 put each Universal Credit claimant in +# one of four groups. The tests follow the order of the Act: section 19 (no +# work-related requirements), section 20 (work-focused interview requirement +# only), section 21 for a claimant who "does not fall within section 19 or +# 20" (work preparation requirement), and section 22 for a claimant "not +# falling within any of sections 19 to 21" (all work-related requirements). +# The variable leaves out the earnings routes of UC Regs 2013 reg. 90, as +# reg. 62(1)(b) and reg. 90(2) both ask where the claimant "would otherwise" +# fall. + +- name: A claimant with no special circumstances is subject to all work-related requirements + period: 2026 + input: + age: 30 + output: + uc_work_related_group_apart_from_earnings: ALL_REQUIREMENTS + +- name: Earnings above the individual threshold do not change the group apart from earnings + period: 2026 + input: + age: 30 + employment_income: 50_000 + output: + # Reg. 90(1) would put this claimant in section 19; reg. 62(1)(b) asks + # where they would fall "apart from ... regulation 90". + uc_work_related_group_apart_from_earnings: ALL_REQUIREMENTS + +- name: Limited capability for work and work-related activity, section 19(2)(a) + period: 2026 + input: + age: 30 + uc_limited_capability_for_WRA: true + output: + uc_work_related_group_apart_from_earnings: NO_REQUIREMENTS + +- name: Regular and substantial caring responsibilities, section 19(2)(b) + period: 2026 + input: + age: 30 + care_hours: 35 + output: + is_carer_for_benefits: true + uc_work_related_group_apart_from_earnings: NO_REQUIREMENTS + +- name: Caring for fewer than 35 hours a week leaves the claimant in section 22 + period: 2026 + input: + age: 30 + care_hours: 34 + output: + uc_work_related_group_apart_from_earnings: ALL_REQUIREMENTS + +- name: Lone parent of a child under 1, section 19(2)(c) + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 0 + output: + uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, NOT_A_CLAIMANT] + +- name: Lone parent of a child aged 1, section 20(1)(a) + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 1 + output: + uc_work_related_group_apart_from_earnings: [INTERVIEW_ONLY, NOT_A_CLAIMANT] + +- name: Lone parent of a child aged 2, section 21(1)(aa) + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 2 + output: + uc_work_related_group_apart_from_earnings: [WORK_PREPARATION, NOT_A_CLAIMANT] + +- name: Lone parent of a child aged 3 is subject to all work-related requirements + period: 2026 + input: + people: + parent: + age: 30 + child: + age: 3 + output: + uc_work_related_group_apart_from_earnings: [ALL_REQUIREMENTS, NOT_A_CLAIMANT] + +- name: The youngest child sets the lone parent's group + period: 2026 + input: + people: + parent: + age: 30 + older_child: + age: 9 + baby: + age: 0 + output: + uc_youngest_child_age: 0 + uc_work_related_group_apart_from_earnings: + [NO_REQUIREMENTS, NOT_A_CLAIMANT, NOT_A_CLAIMANT] + +- name: In a couple only the nominated responsible carer takes the child's group + period: 2026 + input: + people: + nominated: + age: 30 + uc_is_responsible_carer: true + partner: + age: 32 + uc_is_responsible_carer: false + child: + age: 1 + output: + # Section 19(6)(b): the member of the couple "nominated by the couple + # jointly as responsible for the child". The partner is in section 22. + uc_work_related_group_apart_from_earnings: + [INTERVIEW_ONLY, ALL_REQUIREMENTS, NOT_A_CLAIMANT] + +- name: A claimant over State Pension Credit qualifying age, regulation 89(1)(a) + period: 2026 + input: + people: + older: + age: 70 + younger: + age: 40 + output: + uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, ALL_REQUIREMENTS] + +- name: Pregnancy within 11 weeks of the expected week of confinement, regulation 89(1)(c) + period: 2026 + input: + age: 30 + uc_is_in_pregnancy_or_post_confinement_period: true + output: + uc_work_related_group_apart_from_earnings: NO_REQUIREMENTS + +- name: An adopter within 12 months of the placement, regulation 89(1)(d) + period: 2026 + input: + age: 30 + uc_is_adopter_in_first_year: true + output: + uc_work_related_group_apart_from_earnings: NO_REQUIREMENTS + +- name: A student with student income taken into account, regulation 89(1)(e) + period: 2026 + input: + age: 30 + uc_is_student_with_no_work_related_requirements: true + output: + uc_work_related_group_apart_from_earnings: NO_REQUIREMENTS + +- name: The responsible foster parent of a child under 1, regulation 89(1)(f) + period: 2026 + input: + age: 30 + uc_is_responsible_foster_parent_of_child_under_one: true + output: + uc_work_related_group_apart_from_earnings: NO_REQUIREMENTS + +- name: A foster parent of a child aged at least 1, regulation 91(2) + period: 2026 + input: + age: 30 + uc_is_foster_parent_or_new_friend_or_family_carer: true + output: + uc_work_related_group_apart_from_earnings: INTERVIEW_ONLY + +- name: Limited capability for work, section 21(1)(a) + period: 2026 + input: + age: 30 + uc_has_limited_capability_for_work: true + output: + uc_work_related_group_apart_from_earnings: WORK_PREPARATION + +- name: Section 21 takes only a claimant who does not fall within section 19 or 20 + period: 2026 + input: + people: + parent: + age: 30 + uc_has_limited_capability_for_work: true + child: + age: 1 + output: + # Limited capability for work (section 21) and the responsible carer of + # a child aged 1 (section 20): section 20 comes first. + uc_work_related_group_apart_from_earnings: [INTERVIEW_ONLY, NOT_A_CLAIMANT] + +- name: Section 19 comes before section 20 + period: 2026 + input: + people: + parent: + age: 30 + uc_limited_capability_for_WRA: true + child: + age: 1 + output: + uc_work_related_group_apart_from_earnings: [NO_REQUIREMENTS, NOT_A_CLAIMANT] + +- name: A partner who cannot be a joint claimant is in no group + period: 2026 + input: + people: + claimant: + age: 30 + partner: + age: 32 + uc_is_ineligible_partner: true + output: + uc_work_related_group_apart_from_earnings: [ALL_REQUIREMENTS, NOT_A_CLAIMANT] + +- name: A qualifying young person is not a child for the responsible carer rules + period: 2026 + input: + people: + parent: + age: 40 + young_person: + age: 17 + current_education: UPPER_SECONDARY + output: + # Welfare Reform Act 2012 s. 40: a child is a person under the age of 16. + uc_is_responsible_carer: [false, false] + uc_work_related_group_apart_from_earnings: [ALL_REQUIREMENTS, NOT_A_CLAIMANT] + +- name: From 28 April 2014 to 2 April 2017 a child aged 4 put the responsible carer in section 21 + period: 2016 + input: + people: + parent: + age: 30 + child: + age: 4 + output: + # UC Regs reg. 91A as then in force: "the responsible carer for a child + # aged 3 or 4". + uc_work_related_group_apart_from_earnings: [WORK_PREPARATION, NOT_A_CLAIMANT] + +- name: From 28 April 2014 to 2 April 2017 a child aged 2 put the responsible carer in section 20 + period: 2016 + input: + people: + parent: + age: 30 + child: + age: 2 + output: + # Section 20(1)(a) as then in force: "a child who is aged at least 1 and + # is under a prescribed age", prescribed as 3 (reg. 91(1)). + uc_work_related_group_apart_from_earnings: [INTERVIEW_ONLY, NOT_A_CLAIMANT] + +- name: In 2016 a child aged 5 left the responsible carer subject to all work-related requirements + period: 2016 + input: + people: + parent: + age: 30 + child: + age: 5 + output: + uc_work_related_group_apart_from_earnings: [ALL_REQUIREMENTS, NOT_A_CLAIMANT] diff --git a/policyengine_uk/tests/test_uc_work_related_groups_properties.py b/policyengine_uk/tests/test_uc_work_related_groups_properties.py new file mode 100644 index 0000000000..d642702f06 --- /dev/null +++ b/policyengine_uk/tests/test_uc_work_related_groups_properties.py @@ -0,0 +1,384 @@ +"""Property-based tests for the scope of the Universal Credit minimum income floor. + +UC Regs 2013 reg. 62(1)(b) applies the floor only to a claimant who "would, +apart from this regulation or regulation 90, fall within section 22 of the +Act (claimants subject to all work-related requirements)". Welfare Reform Act +2012 ss. 19 to 21 take out claimants with limited capability for work or +work-related activity, carers, the responsible carer of a child under 3, +claimants over State Pension Credit qualifying age and the prescribed +descriptions of regs. 89 and 91. Regulation 88(2) lowers the expected hours of +the responsible carer of a child under 13, reg. 90(2)(a) gives a claimant who +would otherwise be in section 20 or 21 a 16-hour threshold, and only one member +of a couple is the responsible carer (reg. 86(2)). + +Invariants, for any generated population of single people, couples and +mixed-age couples, with children of any age, disability, caring, the +prescribed circumstances, employment, self-employment, pension contributions +and start-up periods, in England, Wales and Scotland: + +1. Scope: the floor applies only to a claimant in the all work-related + requirements group, in gainful self-employment, outside a start-up period. + Everyone else keeps their actual earned income, and nobody's earned income + is lowered. +2. Thresholds: a person's gross threshold is the minimum wage for their age + times 52 times their expected hours (section 22), 16 (sections 20 and 21) + or nothing (section 19 and anyone who is not a claimant). Expected hours + are 35, or the lesser number for the responsible carer of a child under + 13, and never more than 35. +3. Nomination: a benefit unit has one responsible carer if it has a child + under 16 and none otherwise; the responsible carer is always a claimant. +4. The default nomination is the one that gives the couple the higher award: + nominating the other member never lowers the couple's combined earned + income or raises their Universal Credit. This runs the whole model both + ways, so it also checks the functions the default nomination evaluates + against the variables that apply them. +5. Differential against the floor without the scope rules: the same people + with everyone forced into the all work-related requirements group at 35 + hours (the rule before this change). The scope rules never raise anyone's + earned income, so they never lower Universal Credit before the benefit + cap. +6. Monotone: more earnings never lower a benefit unit's earned income or + raise its Universal Credit before the benefit cap, whichever member the + default nomination picks. The intended exception is a loss raised to + exactly zero, which the model reads as no self-employment. +7. A partner who cannot be a joint claimant (reg. 3(3)) never has the floor, + and adds the same amount to the couple threshold whatever their age. + +Marriage Allowance is switched off throughout: a transfer can lower one +partner's earned income when the other's earnings rise. +""" + +import numpy as np +from hypothesis import HealthCheck, assume, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2016: a child aged 1 or 2 puts the responsible carer in section 20 and a +# child aged 3 or 4 in section 21. 2020: sections 20 and 21 take children +# aged 1 and 2, expected hours 16 and 25. 2026: expected hours 30. +YEARS = [2016, 2020, 2026] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +WORKING_AGE = st.integers(18, 64) +PENSION_AGE = st.integers(68, 80) +SHAPES = { + "single": [WORKING_AGE], + "couple": [WORKING_AGE, WORKING_AGE], + "mixed_age": [PENSION_AGE, WORKING_AGE], +} +self_employment = st.one_of(st.just(0.0), st.floats(-10_000, 40_000)) +employment = st.one_of(st.just(0.0), st.floats(0, 40_000)) +pension = st.one_of(st.just(0.0), st.floats(0, 4_000)) +bumps = st.floats(1, 10_000) +rarely = st.integers(0, 5).map(lambda n: n == 0) +CIRCUMSTANCES = [ + "uc_limited_capability_for_WRA", + "uc_has_limited_capability_for_work", + "uc_is_in_pregnancy_or_post_confinement_period", + "uc_is_adopter_in_first_year", + "uc_is_student_with_no_work_related_requirements", + "uc_is_responsible_foster_parent_of_child_under_one", + "uc_is_foster_parent_or_new_friend_or_family_carer", +] +PERSON_VARIABLES = [ + "age", + "is_uc_claimant", + "uc_is_ineligible_partner", + "uc_is_responsible_carer", + "uc_is_in_gainful_self_employment", + "uc_is_in_startup_period", + "uc_expected_hours", + "uc_mif_applies", + "uc_individual_earned_income_before_mif", + "uc_individual_earned_income", + "uc_minimum_income_floor", + "uc_minimum_income_floor_gross", +] +BENUNIT_VARIABLES = [ + "uc_youngest_child_age", + "uc_earned_income", + "universal_credit_pre_benefit_cap", +] +GROUP = "uc_work_related_group_apart_from_earnings" + + +@st.composite +def families(draw, ineligible_partners=False): + shape = draw(st.sampled_from(list(SHAPES))) + adults = [] + for age in SHAPES[shape]: + adult = dict( + age=draw(age), + self_employment_income=draw(self_employment), + employment_income=draw(employment), + pension_contributions=draw(pension), + uc_is_in_startup_period=draw(rarely), + care_hours=draw(st.sampled_from([0, 0, 0, 20, 35])), + ) + for circumstance in CIRCUMSTANCES: + adult[circumstance] = draw(rarely) + adults.append(adult) + if ineligible_partners and shape == "couple": + adults[1]["uc_is_ineligible_partner"] = draw(st.booleans()) + return dict( + adults=adults, + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], + rent=draw(st.floats(0, 15_000)), + region=draw(st.sampled_from(REGIONS)), + ) + + +populations = st.lists(families(), min_size=1, max_size=8) + + +def situation(units, year, bump=None, overrides=None): + """One simulation holding every family. + + ``bump`` is (family index, adult index, variable, amount) to add. + ``overrides`` maps a person's name to extra inputs. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, adult in enumerate(unit["adults"]): + name = f"p{i}_{j}" + person = {k: {year: v} for k, v in adult.items()} + if bump is not None and bump[:2] == (i, j): + variable, amount = bump[2], bump[3] + person[variable] = {year: adult[variable] + amount} + person["would_claim_marriage_allowance"] = {year: False} + people[name] = person + names.append(name) + for k, age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {year: age}} + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: "RENT_FROM_COUNCIL"}, + "region": {year: unit["region"]}, + } + for name, inputs in (overrides or {}).items(): + people[name].update({k: {year: v} for k, v in inputs.items()}) + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, **kwargs): + inputs = situation(units, year, **kwargs) + sim = Simulation(situation=inputs) + values = {v: np.asarray(sim.calculate(v, year)) for v in PERSON_VARIABLES} + values["group"] = np.asarray(sim.calculate(GROUP, year)).astype(str) + for v in BENUNIT_VARIABLES: + values[v] = np.asarray(sim.calculate(v, year)) + values[f"person_{v}"] = np.asarray(sim.calculate(v, year, map_to="person")) + values["names"] = list(inputs["people"]) + parameters = sim.tax_benefit_system.parameters(f"{year}-01-01") + wage = parameters.gov.hmrc.minimum_wage.non_apprentice + values["wage"] = np.asarray(wage.calc(values["age"])) + values["national_living_wage"] = float(wage.calc(np.array([200.0]))[0]) + values["parameters"] = parameters.gov.dwp.universal_credit.work_requirements + + def per_unit(person_values): + return np.asarray( + sim.map_result( + sim.map_result(person_values, "person", "benunit"), + "benunit", + "person", + ) + ) + + values["responsible_carers_in_unit"] = per_unit( + values["uc_is_responsible_carer"].astype(float) + ) + values["claimants_in_unit"] = per_unit(values["is_uc_claimant"].astype(float)) + return values + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_floor_applies_only_to_the_all_requirements_group(units, year): + v = calculate(units, year) + applies = v["uc_mif_applies"].astype(bool) + in_scope = ( + (v["group"] == "ALL_REQUIREMENTS") + & v["uc_is_in_gainful_self_employment"].astype(bool) + & ~v["uc_is_in_startup_period"].astype(bool) + ) + np.testing.assert_array_equal(applies, in_scope, err_msg=str(units)) + # Sections 19 to 21, dependants, start-up periods: actual earned income. + before = v["uc_individual_earned_income_before_mif"] + after = v["uc_individual_earned_income"] + np.testing.assert_allclose( + after[~applies], before[~applies], atol=0.01, err_msg=str(units) + ) + assert np.all(after >= before - 0.01), units + # The group follows the Act for the circumstances the test sets. + claimant = v["is_uc_claimant"].astype(bool) + assert np.all(v["group"][~claimant] == "NOT_A_CLAIMANT"), units + assert np.all(v["group"][claimant] != "NOT_A_CLAIMANT"), units + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_thresholds_follow_the_group_and_expected_hours(units, year): + v = calculate(units, year) + p = v["parameters"] + hours = v["uc_expected_hours"] + default = p.default_expected_hours + assert np.all(hours <= default), units + youngest = v["person_uc_youngest_child_age"] + limit = p.responsible_carer.expected_hours.child_age_limit + lesser = v["uc_is_responsible_carer"].astype(bool) & (youngest < limit) + assert np.all(hours[~lesser] == default), units + group = v["group"] + threshold_hours = np.select( + [ + group == "ALL_REQUIREMENTS", + (group == "INTERVIEW_ONLY") | (group == "WORK_PREPARATION"), + ], + [hours, p.interview_or_preparation_threshold_hours], + default=0, + ) + np.testing.assert_allclose( + v["uc_minimum_income_floor_gross"], + v["wage"] * threshold_hours * 52, + atol=0.01, + err_msg=str(units), + ) + net = v["uc_minimum_income_floor"] + assert np.all(net <= v["uc_minimum_income_floor_gross"] + 0.01), units + assert np.all(net >= 0), units + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_one_responsible_carer_where_there_is_a_child(units, year): + v = calculate(units, year) + carer = v["uc_is_responsible_carer"].astype(bool) + assert np.all(v["is_uc_claimant"].astype(bool)[carer]), units + has_child = v["person_uc_youngest_child_age"] < np.inf + np.testing.assert_array_equal( + v["responsible_carers_in_unit"], + has_child.astype(float), + err_msg=str(units), + ) + for i, unit in enumerate(units): + assert has_child[v["names"].index(f"p{i}_0")] == bool(unit["children"]) + + +def other_nomination(units, v): + """Inputs that nominate the other member of every couple with a child.""" + overrides = {} + for i, unit in enumerate(units): + names = [f"p{i}_{j}" for j in range(len(unit["adults"]))] + nominated = [ + bool(v["uc_is_responsible_carer"][v["names"].index(name)]) for name in names + ] + if len(names) == 2 and any(nominated): + for name, is_nominated in zip(names, nominated): + overrides[name] = {"uc_is_responsible_carer": not is_nominated} + return overrides + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_default_nomination_gives_the_couple_the_higher_award(units, year): + default = calculate(units, year) + overrides = other_nomination(units, default) + assume(overrides) + other = calculate(units, year, overrides=overrides) + assert np.all(other["uc_earned_income"] >= default["uc_earned_income"] - 0.01), ( + units + ) + assert np.all( + other["universal_credit_pre_benefit_cap"] + <= default["universal_credit_pre_benefit_cap"] + 0.01 + ), units + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_scope_rules_never_raise_earned_income(units, year): + v = calculate(units, year) + # The floor without the scope rules: every claimant in the all + # work-related requirements group with 35 expected hours. + overrides = { + name: { + GROUP: "ALL_REQUIREMENTS" if claimant else "NOT_A_CLAIMANT", + "uc_expected_hours": 35, + } + for name, claimant in zip(v["names"], v["is_uc_claimant"]) + } + unrestricted = calculate(units, year, overrides=overrides) + assert np.all( + v["uc_individual_earned_income"] + <= unrestricted["uc_individual_earned_income"] + 0.01 + ), units + assert np.all( + v["universal_credit_pre_benefit_cap"] + >= unrestricted["universal_credit_pre_benefit_cap"] - 0.01 + ), units + + +@st.composite +def bumped(draw): + units = draw(populations) + i = draw(st.integers(0, len(units) - 1)) + j = draw(st.integers(0, len(units[i]["adults"]) - 1)) + variable = draw(st.sampled_from(["self_employment_income", "employment_income"])) + amount = draw(bumps) + # The intended exception to monotonicity: a loss raised to exactly zero. + assume(units[i]["adults"][j][variable] + amount != 0) + return units, (i, j, variable, amount) + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +def test_more_earnings_never_lower_earned_income_or_raise_uc(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump) + # The exception again: profits of zero raised above zero start + # self-employment and can bring the floor; that raises earned income. + assert np.all(high["uc_earned_income"] >= low["uc_earned_income"] - 0.01), ( + bump, + units, + ) + assert np.all( + high["universal_credit_pre_benefit_cap"] + <= low["universal_credit_pre_benefit_cap"] + 0.01 + ), (bump, units) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(ineligible_partners=True), min_size=1, max_size=8), + year=st.sampled_from(YEARS), +) +def test_partner_who_cannot_be_a_joint_claimant(units, year): + v = calculate(units, year) + ineligible = v["uc_is_ineligible_partner"].astype(bool) + assume(ineligible.any()) + assert not v["uc_mif_applies"][ineligible].any(), units + assert np.all(v["group"][ineligible] == "NOT_A_CLAIMANT"), units + np.testing.assert_allclose( + v["uc_individual_earned_income"][ineligible], + v["uc_individual_earned_income_before_mif"][ineligible], + atol=0.01, + ) + # Reg. 90(3)(b)(ii): 35 hours at the national living wage, whatever the + # partner's age. + np.testing.assert_allclose( + v["uc_minimum_income_floor_gross"][ineligible], + v["parameters"].default_expected_hours * 52 * v["national_living_wage"], + atol=0.01, + err_msg=str(units), + ) diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py new file mode 100644 index 0000000000..0a3e8fe106 --- /dev/null +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -0,0 +1,339 @@ +"""Universal Credit work-related groups and the minimum income floor. + +Welfare Reform Act 2012 ss. 19 to 22 sort claimants into four groups: no +work-related requirements (s. 19), the work-focused interview requirement only +(s. 20), the work preparation requirement (s. 21) and all work-related +requirements (s. 22). The minimum income floor of the Universal Credit +Regulations 2013 reg. 62 applies only to a claimant who "would, apart from this +regulation or regulation 90, fall within section 22", and the group sets each +claimant's individual threshold (reg. 90(2)). + +A member of a couple is the responsible carer for a child only if the couple +nominate them (s. 19(6)(b)), and only one of them can be nominated (reg. 86(2)). +Several rules turn on that nomination: the group of the responsible carer of a +child under 3 (ss. 19(2)(c), 20(1)(a), 21(1)(aa)), the expected hours of the +responsible carer of a child under 13 (reg. 88(2)(aa) and (b)), and so each +partner's threshold and the couple threshold. Each rule is written here once, +as a function of the nomination. The variables call these functions with the +recorded nomination. The default nomination (uc_is_responsible_carer) calls +them with each member of the couple in turn and keeps the nomination that +leaves the couple the lower combined earned income. +""" + +from policyengine_uk.model_api import * + +# The groups in the order the Act tests them: section 21 covers a claimant +# who "does not fall within section 19 or 20", and section 22 one "not +# falling within any of sections 19 to 21". +NO_REQUIREMENTS = 0 +INTERVIEW_ONLY = 1 +WORK_PREPARATION = 2 +ALL_REQUIREMENTS = 3 +NOT_A_CLAIMANT = 4 + + +def couple_members(person, period): + """The claimant and any partner: at most two people in a benefit unit. + + A claim has at most two claimants. Where the data flag more (an adult + child in the parents' benefit unit), the two eldest are the couple. + """ + age = person("age", period) + flagged = person("is_uc_claimant", period) + return flagged & (person.get_rank(person.benunit, -age, condition=flagged) < 2) + + +def claimants(person, period): + """The single claimant or the joint claimants. + + A partner who cannot be a joint claimant (reg. 3(3)) is a member of the + couple but not a claimant: no work-related group or floor applies to them. + """ + return couple_members(person, period) & ~person("uc_is_ineligible_partner", period) + + +def work_related_group(person, period, parameters, responsible_carer): + """Each person's group apart from regs. 62 and 90, given the nomination. + + Regulation 90 puts a claimant whose earnings reach their threshold, and + one treated as having the minimum income floor, in section 19. Regulation + 62(1)(b) asks where the claimant would fall apart from those routes, so + neither is tested here. + """ + p = parameters(period).gov.dwp.universal_credit.work_requirements + child_age = p.responsible_carer.child_age + youngest = person.benunit("uc_youngest_child_age", period) + no_requirements = ( + # s. 19(2)(a): limited capability for work and work-related activity. + person("uc_limited_capability_for_WRA", period) + # s. 19(2)(b) with reg. 30: regular and substantial caring + # responsibilities for a severely disabled person. The same flag + # covers the 35-hour carers of reg. 89(1)(b). + | person("is_carer_for_benefits", period) + # s. 19(2)(c): the responsible carer for a child under the age of 1. + | (responsible_carer & (youngest < child_age.no_requirements)) + # Reg. 89(1)(a): the qualifying age for State Pension Credit. + | person("is_SP_age", period) + # Reg. 89(1)(c): 11 weeks before to 15 weeks after confinement. + | person("uc_is_in_pregnancy_or_post_confinement_period", period) + # Reg. 89(1)(d): an adopter in the 12 months after placement. + | person("uc_is_adopter_in_first_year", period) + # Reg. 89(1)(da) and (e): students. + | person("uc_is_student_with_no_work_related_requirements", period) + # Reg. 89(1)(f): the responsible foster parent of a child under 1. + | person("uc_is_responsible_foster_parent_of_child_under_one", period) + ) + interview_only = ( + # s. 20(1)(a): the responsible carer for a child aged 1 (before 3 + # April 2017, aged at least 1 and under the prescribed age). + (responsible_carer & (youngest < child_age.interview_only)) + # Reg. 91(2): foster parents, and friend or family carers in their + # first 12 months. + | person("uc_is_foster_parent_or_new_friend_or_family_carer", period) + ) + work_preparation = ( + # s. 21(1)(a): limited capability for work. + person("uc_has_limited_capability_for_work", period) + # s. 21(1)(aa): the responsible carer for a child aged 2 (from 28 + # April 2014 to 2 April 2017, reg. 91A: aged 3 or 4). + | (responsible_carer & (youngest < child_age.work_preparation)) + ) + return select( + [ + ~claimants(person, period), + no_requirements, + interview_only, + work_preparation, + ], + [NOT_A_CLAIMANT, NO_REQUIREMENTS, INTERVIEW_ONLY, WORK_PREPARATION], + default=ALL_REQUIREMENTS, + ) + + +def expected_hours(person, period, parameters, responsible_carer): + """Expected hours of work each week (reg. 88), given the nomination. + + The number is 35 unless a lesser number applies. The regulation leaves + each lesser number to the Secretary of State. The model takes the hours + DWP says it uses for the responsible carer of a child under 13, and 35 + for everyone else: it has no rule for the hours of a relevant carer + (reg. 88(2)(a)) or a claimant with an impairment (reg. 88(2)(c)), which + uc_expected_hours can take as an input. + """ + p = parameters(period) + hours = p.gov.dwp.universal_credit.work_requirements + carer_hours = hours.responsible_carer.expected_hours + youngest = person.benunit("uc_youngest_child_age", period) + under_limit = responsible_carer & (youngest < carer_hours.child_age_limit) + of_school_age = p.gov.dfe.compulsory_school_age.calc(youngest) + lesser = where( + of_school_age, + # Reg. 88(2)(b): a child of compulsory school age under 13. + carer_hours.compulsory_school_age, + # Reg. 88(2)(aa): a child who has not reached compulsory school age. + carer_hours.below_compulsory_school_age, + ) + return where( + under_limit, + min_(lesser, hours.default_expected_hours), + hours.default_expected_hours, + ) + + +def threshold_hours(person, period, parameters, group, hours): + """Weekly hours behind each person's amount in the couple threshold. + + Regulation 90(2) gives a claimant who would otherwise be in section 22 + their expected hours, and one who would otherwise be in section 20 or 21 + 16 hours. It sets no threshold for a claimant in section 19 for a reason + other than earnings; the parameter records the hours the model uses for + such a partner (none). For a partner who is not a joint claimant, reg. + 90(3)(b)(ii) adds the pay for 35 hours. + """ + p = parameters(period).gov.dwp.universal_credit + work = p.work_requirements + floor = p.means_test.minimum_income_floor + ineligible_partner = couple_members(person, period) & person( + "uc_is_ineligible_partner", period + ) + return select( + [ + ineligible_partner, + group == ALL_REQUIREMENTS, + (group == INTERVIEW_ONLY) | (group == WORK_PREPARATION), + group == NO_REQUIREMENTS, + ], + [ + work.default_expected_hours, + hours, + work.interview_or_preparation_threshold_hours, + floor.no_requirements_partner_hours, + ], + default=0, + ) + + +def gross_threshold(person, period, parameters, weekly_hours): + """The reg. 90(2) or 90(3)(b)(ii) amount for the hours, over a year. + + Regulation 90(2) uses the rate "a person of the same age as the claimant + would be paid" under the National Minimum Wage Regulations 2015 reg. 4 or + 4A(1)(a) to (c): the rate for their age, never the apprenticeship rate of + reg. 4A(1)(d). Regulation 90(3)(b)(ii) uses "the hourly rate specified in + regulation 4", the national living wage rate, whatever the partner's age. + """ + wage = parameters(period).gov.hmrc.minimum_wage.non_apprentice + ineligible_partner = person("uc_is_ineligible_partner", period) + hourly_rate = where( + ineligible_partner, + # The scale's top rate: the national living wage (the adult rate + # before 1 April 2016). + wage.calc(np.full(person.count, 200.0)), + wage.calc(person("age", period)), + ) + # Regulation 6(1A)(a) disregards fractions of a pound only in amounts + # calculated for reg. 90 itself. Floors DWP has issued keep the pence: + # 1,642.72 a month for 2025-26 from a threshold of 1,851.85 (12.21 x + # 35 x 52 / 12; University of Bath IPR, "Going it alone", 2025, + # "information supplied by the DWP"), where a whole-pound threshold + # would give 1,642.09. So the threshold here is not rounded. + return hourly_rate * weekly_hours * WEEKS_IN_YEAR + + +def income_tax_on_threshold(person, period, parameters, threshold): + """Income tax on the threshold as the person's only income. + + Regulation 62(4)(b) deducts "such amount for income tax ... as the + Secretary of State considers appropriate". The model treats the threshold + as the person's only income: only the standard personal allowance + applies, and no other allowance or relief. + """ + income_tax = parameters(period).gov.hmrc.income_tax + taxable = max_(0, threshold - income_tax.allowances.personal_allowance.amount) + return where( + person("pays_scottish_income_tax", period), + income_tax.rates.scotland.rates.calc(taxable), + income_tax.rates.uk.calc(taxable), + ) + + +def national_insurance_on_threshold(person, period, parameters, threshold): + """National Insurance on the threshold as the person's only earnings. + + Class 2 and Class 4 on self-employed profits of that amount, as in DWP's + figures, or primary Class 1 on pay of that amount where the parameter + selects it (reg. 62(4)(b)). + """ + p = parameters(period) + ni = p.gov.hmrc.national_insurance + # Primary Class 1 on pay equal to the threshold, with the annual + # thresholds (weekly amounts times 52) the model uses for employees. + class_1 = ni.class_1 + primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR + upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR + employee = class_1.rates.employee.main * max_( + 0, min_(threshold, upper_earnings_limit) - primary_threshold + ) + class_1.rates.employee.additional * max_(0, threshold - upper_earnings_limit) + # Class 2 and Class 4 on profits equal to the threshold. + class_2 = ( + (threshold >= ni.class_2.small_profits_threshold) + * ni.class_2.flat_rate + * WEEKS_IN_YEAR + ) + class_4 = ni.class_4 + class_4_amount = class_4.rates.main * max_( + 0, + min_(threshold, class_4.thresholds.upper_profits_limit) + - class_4.thresholds.lower_profits_limit, + ) + class_4.rates.additional * max_( + 0, threshold - class_4.thresholds.upper_profits_limit + ) + self_employed = class_2 + class_4_amount + floor = p.gov.dwp.universal_credit.means_test.minimum_income_floor + amount = where(floor.self_employed_national_insurance, self_employed, employee) + # No primary Class 1, Class 2 or Class 4 is due from anyone the model + # treats as not liable (under 16 or over State Pension age). + return person("ni_liable", period) * amount + + +def net_threshold(person, period, parameters, gross): + """The threshold converted to a net amount (reg. 62(4)).""" + deductions = income_tax_on_threshold( + person, period, parameters, gross + ) + national_insurance_on_threshold(person, period, parameters, gross) + return max_(0, gross - deductions) + + +def floor_applies(person, period, group): + """Whether reg. 62 applies to the person, given their group. + + Regulation 62(1): a claimant who is in gainful self-employment and would, + apart from regs. 62 and 90, fall within section 22; reg. 62(5) leaves out + start-up periods. A dependant, or a partner who is not a joint claimant, + is in no group. + """ + return ( + (group == ALL_REQUIREMENTS) + & person("uc_is_in_gainful_self_employment", period) + & ~person("uc_is_in_startup_period", period) + ) + + +def treated_earned_income(person, earned_income, threshold, applies, couple): + """Earned income after the floor (reg. 62(2) and (3)). + + A single claimant below their threshold is treated as having the + threshold (reg. 62(2)). A member of a couple is treated as having it only + while the couple's combined earned income is also below the couple + threshold, less any amount by which it and the partner's earned income + would exceed that threshold (reg. 62(3)). For a single claimant the + couple threshold is their own and reg. 62(3) reduces to reg. 62(2). + Partners' earned income is their actual earned income: when both are + below their thresholds each is treated as having their own, whichever + partner's floor is applied first. + """ + couple_earned_income = earned_income * couple + partner_earned_income = ( + person.benunit.sum(couple_earned_income) - couple_earned_income + ) + # Reg. 90(3): the sum of the joint claimants' individual thresholds, or + # the claimant's threshold and the 35-hour amount for a partner who is + # not a joint claimant. + couple_threshold = person.benunit.sum(threshold * couple) + below_individual_threshold = earned_income < threshold + below_couple_threshold = earned_income + partner_earned_income < couple_threshold + floor = threshold - max_(0, threshold + partner_earned_income - couple_threshold) + return where( + applies & couple & below_individual_threshold & below_couple_threshold, + floor, + earned_income, + ) + + +def combined_earned_income(person, period, parameters, responsible_carer): + """The couple's combined earned income after the floor, for a nomination. + + Returned for each person's benefit unit. + """ + couple = couple_members(person, period) + group = work_related_group(person, period, parameters, responsible_carer) + hours = expected_hours(person, period, parameters, responsible_carer) + threshold = net_threshold( + person, + period, + parameters, + gross_threshold( + person, + period, + parameters, + threshold_hours(person, period, parameters, group, hours), + ), + ) + treated = treated_earned_income( + person, + person("uc_individual_earned_income_before_mif", period), + threshold, + floor_applies(person, period, group), + couple, + ) + return person.benunit.sum(treated * couple) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py index d4ddb8a5c1..b9b284803e 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py @@ -1,4 +1,9 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + ALL_REQUIREMENTS, + claimants, + floor_applies, +) class uc_mif_applies(Variable): @@ -7,16 +12,20 @@ class uc_mif_applies(Variable): label = "Universal Credit minimum income floor applies" documentation = ( "Whether the minimum income floor applies to this person: a claimant " - "with a self-employment profit or loss, outside a start-up period." + "in gainful self-employment, outside a start-up period, who would " + "apart from the floor and the earnings thresholds be subject to all " + "work-related requirements." ) reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/62" definition_period = YEAR def formula(person, period, parameters): - # Reg. 62(1) applies to a claimant, not a dependant, in gainful - # self-employment: a trade carried on in expectation of profit (reg. - # 64), so a trading loss counts (ADM H4503). - claimant = person("is_uc_claimant", period) - has_self_empl_income = person("self_employment_income", period) != 0 - in_startup_period = person("uc_is_in_startup_period", period) - return claimant & has_self_empl_income & ~in_startup_period + # Reg. 62(1) applies to a claimant, not a dependant, who (a) is in + # gainful self-employment and (b) "would, apart from this regulation + # or regulation 90, fall within section 22 of the Act". Reg. 62(5) + # leaves out start-up periods. + group = person("uc_work_related_group_apart_from_earnings", period) + all_requirements = group == group.possible_values.ALL_REQUIREMENTS + return claimants(person, period) & floor_applies( + person, period, where(all_requirements, ALL_REQUIREMENTS, -1) + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py index 4786b79206..67fc46febb 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py @@ -1,4 +1,12 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + ALL_REQUIREMENTS, + INTERVIEW_ONLY, + NO_REQUIREMENTS, + WORK_PREPARATION, + gross_threshold, + threshold_hours, +) class uc_minimum_income_floor_gross(Variable): @@ -8,14 +16,23 @@ class uc_minimum_income_floor_gross(Variable): documentation = ( "The person's individual threshold before the deductions for income " "tax and National Insurance: the National Minimum Wage hourly rate " - "for their age (never the apprenticeship rate) times their expected " - "hours of work each week, over a year." + "for their age (never the apprenticeship rate) times a number of " + "hours each week, over a year. The hours are the person's expected " + "hours where they would otherwise be subject to all work-related " + "requirements, and 16 where they would otherwise be subject to the " + "work-focused interview requirement only or the work preparation " + "requirement. The regulations set no threshold for a claimant " + "subject to no work-related requirements for a reason other than " + "earnings, who adds nothing to a couple threshold. A partner who " + "cannot be a joint claimant has no threshold either; the amount here " + "is what they add to the couple threshold, the pay for 35 hours at " + "the national living wage." ) definition_period = YEAR unit = GBP reference = [ dict( - title="Universal Credit Regulations 2013 reg. 90(2)", + title="Universal Credit Regulations 2013 reg. 90(2) and (3)", href="https://www.legislation.gov.uk/uksi/2013/376/regulation/90", ), dict( @@ -29,18 +46,23 @@ class uc_minimum_income_floor_gross(Variable): ] def formula(person, period, parameters): - p = parameters(period) - expected_hours = ( - p.gov.dwp.universal_credit.work_requirements.default_expected_hours + group = person("uc_work_related_group_apart_from_earnings", period) + groups = group.possible_values + group_index = select( + [ + group == groups.NO_REQUIREMENTS, + group == groups.INTERVIEW_ONLY, + group == groups.WORK_PREPARATION, + group == groups.ALL_REQUIREMENTS, + ], + [NO_REQUIREMENTS, INTERVIEW_ONLY, WORK_PREPARATION, ALL_REQUIREMENTS], + default=-1, ) - # Reg. 90(2) uses the rate "a person of the same age as the claimant - # would be paid" under NMW Regs reg. 4 or 4A(1)(a) to (c): the rate - # for their age, never the apprenticeship rate of reg. 4A(1)(d). - hourly_rate = p.gov.hmrc.minimum_wage.non_apprentice.calc(person("age", period)) - # Reg. 6(1A)(a) disregards fractions of a pound only in amounts - # calculated for reg. 90 itself. Floors DWP has issued keep the pence: - # 1,642.72 a month for 2025-26 from a threshold of 1,851.85 (12.21 x - # 35 x 52 / 12; University of Bath IPR, "Going it alone", 2025, - # "information supplied by the DWP"), where a whole-pound threshold - # would give 1,642.09. So the threshold here is not rounded. - return hourly_rate * expected_hours * WEEKS_IN_YEAR + hours = threshold_hours( + person, + period, + parameters, + group_index, + person("uc_expected_hours", period), + ) + return gross_threshold(person, period, parameters, hours) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py index c570e6eb1a..4fe72a4b6a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + income_tax_on_threshold, +) class uc_minimum_income_floor_income_tax(Variable): @@ -27,15 +30,9 @@ class uc_minimum_income_floor_income_tax(Variable): ] def formula(person, period, parameters): - # Reg. 62(4)(b) deducts "such amount for income tax ... as the - # Secretary of State considers appropriate". The model treats the - # threshold as the person's only income: only the standard personal - # allowance applies, and no other allowance or relief. - income_tax = parameters(period).gov.hmrc.income_tax - threshold = person("uc_minimum_income_floor_gross", period) - taxable = max_(0, threshold - income_tax.allowances.personal_allowance.amount) - return where( - person("pays_scottish_income_tax", period), - income_tax.rates.scotland.rates.calc(taxable), - income_tax.rates.uk.calc(taxable), + return income_tax_on_threshold( + person, + period, + parameters, + person("uc_minimum_income_floor_gross", period), ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py index 4b3f4bff03..9b8648ef17 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + national_insurance_on_threshold, +) class uc_minimum_income_floor_national_insurance(Variable): @@ -28,36 +31,9 @@ class uc_minimum_income_floor_national_insurance(Variable): ] def formula(person, period, parameters): - p = parameters(period) - ni = p.gov.hmrc.national_insurance - threshold = person("uc_minimum_income_floor_gross", period) - # Primary Class 1 on pay equal to the threshold, with the annual - # thresholds (weekly amounts times 52) the model uses for employees. - class_1 = ni.class_1 - primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR - upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR - employee = class_1.rates.employee.main * max_( - 0, min_(threshold, upper_earnings_limit) - primary_threshold - ) + class_1.rates.employee.additional * max_( - 0, threshold - upper_earnings_limit + return national_insurance_on_threshold( + person, + period, + parameters, + person("uc_minimum_income_floor_gross", period), ) - # Class 2 and Class 4 on profits equal to the threshold. - class_2 = ( - (threshold >= ni.class_2.small_profits_threshold) - * ni.class_2.flat_rate - * WEEKS_IN_YEAR - ) - class_4 = ni.class_4 - class_4_amount = class_4.rates.main * max_( - 0, - min_(threshold, class_4.thresholds.upper_profits_limit) - - class_4.thresholds.lower_profits_limit, - ) + class_4.rates.additional * max_( - 0, threshold - class_4.thresholds.upper_profits_limit - ) - self_employed = class_2 + class_4_amount - mif = p.gov.dwp.universal_credit.means_test.minimum_income_floor - amount = where(mif.self_employed_national_insurance, self_employed, employee) - # No primary Class 1, Class 2 or Class 4 is due from anyone the model - # treats as not liable (under 16 or over State Pension age). - return person("ni_liable", period) * amount diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py index ba90cccf7e..b471097b68 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + couple_members, + treated_earned_income, +) class uc_individual_earned_income(Variable): @@ -29,43 +33,16 @@ class uc_individual_earned_income(Variable): ] def formula(person, period, parameters): - earned_income = person("uc_individual_earned_income_before_mif", period) # Reg. 62 compares earned income, after the deductions, with the - # net threshold. A single claimant below their threshold is treated - # as having the threshold (reg. 62(2)). A member of a couple is - # treated as having it only while the couple's combined earned - # income is also below the couple threshold, less any amount by - # which it and the partner's earned income would exceed that - # threshold (reg. 62(3)). The couple threshold is the sum of the - # joint claimants' individual thresholds (reg. 90(3)(a)); for a - # single claimant it is their own, and reg. 62(3) reduces to - # reg. 62(2). Partners' earned income is their actual earned income: - # when both are below their thresholds each is treated as having - # their own, whichever partner's floor is applied first. A claim has - # at most two claimants; where the data flag more (an adult child in - # the parents' benefit unit), the two eldest are the couple. - age = person("age", period) - is_claimant = person("is_uc_claimant", period) - claimant = is_claimant & ( - person.get_rank(person.benunit, -age, condition=is_claimant) < 2 + # net threshold (reg. 62(2) and (3)). The couple threshold is the + # sum of the joint claimants' individual thresholds (reg. 90(3)(a)), + # or the claimant's threshold and a 35-hour amount for a partner who + # is not a joint claimant (reg. 90(3)(b)); each is in + # uc_minimum_income_floor. + return treated_earned_income( + person, + person("uc_individual_earned_income_before_mif", period), + person("uc_minimum_income_floor", period), + person("uc_mif_applies", period), + couple_members(person, period), ) - claimant_earned_income = earned_income * claimant - partner_earned_income = ( - person.benunit.sum(claimant_earned_income) - claimant_earned_income - ) - individual_threshold = person("uc_minimum_income_floor", period) - couple_threshold = person.benunit.sum(individual_threshold * claimant) - below_individual_threshold = earned_income < individual_threshold - below_couple_threshold = ( - earned_income + partner_earned_income < couple_threshold - ) - floor = individual_threshold - max_( - 0, individual_threshold + partner_earned_income - couple_threshold - ) - floor_applies = ( - person("uc_mif_applies", period) - & claimant - & below_individual_threshold - & below_couple_threshold - ) - return where(floor_applies, floor, earned_income) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py new file mode 100644 index 0000000000..c5640ca97b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py @@ -0,0 +1,35 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import expected_hours + + +class uc_expected_hours(Variable): + value_type = float + entity = Person + label = "Universal Credit expected hours of work each week" + documentation = ( + "The expected number of hours per week: 35 unless a lesser number " + "applies. The regulation leaves each lesser number to the Secretary " + "of State. The model uses the hours DWP says it uses for the " + "responsible carer of a child under 13 (a maximum of 30 since 2025; " + "before that 16 for a child aged 3 or 4 and 25 for a school-age " + "child). It sets no lesser number for a parent who is not the " + "responsible carer or for a claimant with a physical or mental " + "impairment: supply this variable where one applies." + ) + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 88", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/88", + ), + dict( + title="Written answer 129349, 28 April 2026", + href="https://questions-statements.parliament.uk/written-questions/detail/2026-04-22/129349", + ), + ] + definition_period = YEAR + unit = "hour" + + def formula(person, period, parameters): + return expected_hours( + person, period, parameters, person("uc_is_responsible_carer", period) + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_has_limited_capability_for_work.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_has_limited_capability_for_work.py new file mode 100644 index 0000000000..c12ca4c1bb --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_has_limited_capability_for_work.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class uc_has_limited_capability_for_work(Variable): + value_type = bool + entity = Person + label = "Has limited capability for work, for Universal Credit" + documentation = ( + "Whether the claimant has limited capability for work. A claimant " + "who has it, and does not also have limited capability for " + "work-related activity, is subject to the work preparation " + "requirement. An input: the model identifies limited capability for " + "work-related activity only (uc_limited_capability_for_WRA)." + ) + reference = "https://www.legislation.gov.uk/ukpga/2012/5/section/21" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_adopter_in_first_year.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_adopter_in_first_year.py new file mode 100644 index 0000000000..dbe2f30a91 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_adopter_in_first_year.py @@ -0,0 +1,19 @@ +from policyengine_uk.model_api import * + + +class uc_is_adopter_in_first_year(Variable): + value_type = bool + entity = Person + label = "Adopter within 12 months of the placement, for Universal Credit" + documentation = ( + "Whether the claimant is an adopter and it is 12 months or less since " + "the child was placed with them (or since a date they chose within " + "the 14 days before the expected placement). An adopter is someone " + "matched with a child for adoption who is, or is intended to be, the " + "responsible carer, other than a foster parent or close relative of " + "the child. Such a claimant is subject to no work-related " + "requirements. An input: the model has no adoption data." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/89" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_foster_parent_or_new_friend_or_family_carer.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_foster_parent_or_new_friend_or_family_carer.py new file mode 100644 index 0000000000..39f4e70bb0 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_foster_parent_or_new_friend_or_family_carer.py @@ -0,0 +1,21 @@ +from policyengine_uk.model_api import * + + +class uc_is_foster_parent_or_new_friend_or_family_carer(Variable): + value_type = bool + entity = Person + label = "Foster parent, or friend or family carer in their first 12 months, for Universal Credit" + documentation = ( + "Whether the Universal Credit Regulations 2013 reg. 91(2) puts the " + "claimant in the group subject to the work-focused interview " + "requirement only: the responsible foster parent of a child aged at " + "least 1; a foster parent of a child or qualifying young person " + "whose care needs make a work search requirement unreasonable; a " + "foster parent between placements for up to 8 weeks; or a friend or " + "family carer who took on a child within the past 12 months and is " + "the responsible carer for that child. An input: the model has no " + "fostering or kinship care data." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/91" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_gainful_self_employment.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_gainful_self_employment.py new file mode 100644 index 0000000000..dc14ab3c79 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_gainful_self_employment.py @@ -0,0 +1,24 @@ +from policyengine_uk.model_api import * + + +class uc_is_in_gainful_self_employment(Variable): + value_type = bool + entity = Person + label = "In gainful self-employment for Universal Credit" + documentation = ( + "Whether the person is in gainful self-employment: carrying on a " + "trade, profession or vocation as their main employment, with " + "self-employed earnings from it, that is organised, developed, " + "regular and carried on in expectation of profit. The Secretary of " + "State determines it. Without an input, the model reads any " + "self-employment profit or loss as gainful self-employment, and no " + "profit or loss as none; set this for a trader who breaks even, or " + "for self-employment that is not gainful." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/64" + definition_period = YEAR + + def formula(person, period, parameters): + # A trade carried on in expectation of profit (reg. 64(c)), so a + # trading loss counts (ADM H4503). + return person("self_employment_income", period) != 0 diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_pregnancy_or_post_confinement_period.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_pregnancy_or_post_confinement_period.py new file mode 100644 index 0000000000..fc36119a75 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_in_pregnancy_or_post_confinement_period.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class uc_is_in_pregnancy_or_post_confinement_period(Variable): + value_type = bool + entity = Person + label = "Pregnant within 11 weeks of the expected week of confinement, or within 15 weeks after it, for Universal Credit" + documentation = ( + 'Whether the claimant "is pregnant and it is 11 weeks or less before ' + "her expected week of confinement, or was pregnant and it is 15 weeks " + 'or less since the date of her confinement". Such a claimant is ' + "subject to no work-related requirements. An input: the model has no " + "pregnancy data." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/89" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py new file mode 100644 index 0000000000..09deb21191 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_ineligible_partner.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class uc_is_ineligible_partner(Variable): + value_type = bool + entity = Person + label = "Partner who cannot be a joint Universal Credit claimant" + documentation = ( + "Whether this person is the member of a couple whose partner claims " + "Universal Credit as a single person under the Universal Credit " + "Regulations 2013 reg. 3(3): this person is under 18 and cannot claim " + "at that age, is not in Great Britain, is a prisoner, is excluded by " + "reg. 19, or is subject to immigration control. They are in no work-related group and the " + "minimum income floor never applies to them, but their earned income " + "counts as their partner's, and they add the pay for 35 hours at " + "the national living wage to the couple threshold (reg. 90(3)(b)). " + "An input. The model does not yet give such a couple the single " + "claimant's standard allowance of reg. 36(3)." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/3" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py new file mode 100644 index 0000000000..62e465bb3b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py @@ -0,0 +1,50 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + claimants, + combined_earned_income, +) + + +class uc_is_responsible_carer(Variable): + value_type = bool + entity = Person + label = "Responsible carer for a child, for Universal Credit" + documentation = ( + "Whether the person is the responsible carer for the children in " + "their benefit unit: a single claimant who is responsible for a " + "child, or the member of a couple whom the couple have jointly " + "nominated. Only one joint claimant can be nominated, and the " + "nomination covers all their children. Datasets and households " + "should supply the nomination where they know it. Without one, the " + "model takes the nomination that leaves the couple the lower " + "combined earned income after the minimum income floor, which is the " + "nomination that gives them the higher award: the choice is the " + "couple's. Where the nomination makes no difference the elder " + "claimant is the responsible carer." + ) + reference = [ + dict( + title="Welfare Reform Act 2012 s. 19(6)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/19", + ), + dict( + title="Universal Credit Regulations 2013 reg. 86", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/86", + ), + ] + definition_period = YEAR + + def formula(person, period, parameters): + claimant = claimants(person, period) + # s. 19(6): a single person who is responsible for the child, or the + # nominated member of a couple either of whom is responsible for it. + has_child = person.benunit("uc_youngest_child_age", period) < np.inf + rank = person.get_rank(person.benunit, -person("age", period), claimant) + elder = claimant & (rank == 0) + younger = claimant & (rank == 1) + # Reg. 86(2) and (4): the couple choose which of them to nominate. + # The floor is the only consequence of the choice in the model. + if_elder = combined_earned_income(person, period, parameters, elder) + if_younger = combined_earned_income(person, period, parameters, younger) + nominate_younger = person.benunit.any(younger) & (if_younger < if_elder) + return has_child & where(nominate_younger, younger, elder) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_foster_parent_of_child_under_one.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_foster_parent_of_child_under_one.py new file mode 100644 index 0000000000..d2c85cd0fb --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_foster_parent_of_child_under_one.py @@ -0,0 +1,17 @@ +from policyengine_uk.model_api import * + + +class uc_is_responsible_foster_parent_of_child_under_one(Variable): + value_type = bool + entity = Person + label = "Responsible foster parent of a child under 1, for Universal Credit" + documentation = ( + "Whether the claimant is the responsible foster parent of a child " + "under the age of 1: the only foster parent, or the member of a " + "fostering couple nominated as responsible. Such a claimant is " + "subject to no work-related requirements. An input: a foster child " + "is not a member of the benefit unit." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/89" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_student_with_no_work_related_requirements.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_student_with_no_work_related_requirements.py new file mode 100644 index 0000000000..1f6256160c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_student_with_no_work_related_requirements.py @@ -0,0 +1,21 @@ +from policyengine_uk.model_api import * + + +class uc_is_student_with_no_work_related_requirements(Variable): + value_type = bool + entity = Person + label = "Student subject to no Universal Credit work-related requirements" + documentation = ( + "Whether the claimant is a student whom the Universal Credit " + "Regulations 2013 reg. 89(1)(da) or (e) puts in the group subject to " + "no work-related requirements: a member of a couple entitled under " + "reg. 3(2)(b), or a claimant exempt from the education condition " + "under reg. 14, with student income taken into account in the award; " + "or a claimant under 21 in non-advanced education without parental " + "support. A part-time postgraduate loan does not count as student " + "income here (reg. 89(4)). An input: the model does not assess " + "student income for Universal Credit." + ) + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/89" + definition_period = YEAR + default_value = False diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py new file mode 100644 index 0000000000..54676a264c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py @@ -0,0 +1,69 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import ( + ALL_REQUIREMENTS, + INTERVIEW_ONLY, + NO_REQUIREMENTS, + WORK_PREPARATION, + work_related_group, +) + + +class UCWorkRelatedGroup(Enum): + NO_REQUIREMENTS = "No work-related requirements" + INTERVIEW_ONLY = "Work-focused interview requirement only" + WORK_PREPARATION = "Work preparation requirement" + ALL_REQUIREMENTS = "All work-related requirements" + NOT_A_CLAIMANT = "Not a claimant" + + +class uc_work_related_group_apart_from_earnings(Variable): + value_type = Enum + possible_values = UCWorkRelatedGroup + default_value = UCWorkRelatedGroup.NOT_A_CLAIMANT + entity = Person + label = "Universal Credit work-related group, apart from the earnings thresholds" + documentation = ( + "The work-related group the claimant would fall within apart from " + "regulations 62 and 90 of the Universal Credit Regulations 2013: " + "before asking whether their earnings reach their threshold, or " + "whether the minimum income floor treats them as having such " + "earnings, either of which would put them in the group subject to no " + "work-related requirements. This is the test for the minimum income " + "floor (reg. 62(1)(b)) and for each claimant's individual threshold " + "(reg. 90(2))." + ) + reference = [ + dict( + title="Welfare Reform Act 2012 ss. 19 to 22", + href="https://www.legislation.gov.uk/ukpga/2012/5/part/1/chapter/2/crossheading/application-of-workrelated-requirements", + ), + dict( + title="Universal Credit Regulations 2013 regs. 89 and 91", + href="https://www.legislation.gov.uk/uksi/2013/376/part/8/chapter/1/crossheading/workrelated-groups", + ), + dict( + title="Universal Credit Regulations 2013 reg. 62(1)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/62", + ), + ] + definition_period = YEAR + + def formula(person, period, parameters): + group = work_related_group( + person, period, parameters, person("uc_is_responsible_carer", period) + ) + return select( + [ + group == NO_REQUIREMENTS, + group == INTERVIEW_ONLY, + group == WORK_PREPARATION, + group == ALL_REQUIREMENTS, + ], + [ + UCWorkRelatedGroup.NO_REQUIREMENTS, + UCWorkRelatedGroup.INTERVIEW_ONLY, + UCWorkRelatedGroup.WORK_PREPARATION, + UCWorkRelatedGroup.ALL_REQUIREMENTS, + ], + default=UCWorkRelatedGroup.NOT_A_CLAIMANT, + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py new file mode 100644 index 0000000000..bc69077ae6 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.uc_work_related_requirements import couple_members + + +class uc_youngest_child_age(Variable): + value_type = float + entity = BenUnit + label = ( + "Age of the youngest child the Universal Credit claimants are responsible for" + ) + documentation = ( + "The age of the youngest child in the benefit unit: a person under " + "16 who normally lives with the claimants and is not looked after by " + "a local authority. Infinite where there is no such child." + ) + reference = [ + dict( + title="Welfare Reform Act 2012 s. 40 (child: a person under the age of 16)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/40", + ), + dict( + title="Universal Credit Regulations 2013 reg. 4", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/4", + ), + ] + definition_period = YEAR + unit = "year" + + def formula(benunit, period, parameters): + person = benunit.members + # Reg. 4(2): a person is responsible for a child who normally lives + # with them (here, a child in their benefit unit), but not for one + # looked after by a local authority (reg. 4(6)(a)). + child = ( + person("is_child_for_universal_credit", period) + & ~person("is_looked_after_by_local_authority", period) + & ~couple_members(person, period) + ) + return benunit.min(where(child, person("age", period), np.inf)) From fe04591060f91c5f7ff70279cfe787403cbd374a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:42:01 -0400 Subject: [PATCH 2/9] Keep each rule in its variable; default the nomination to the claimant with fewer paid hours The responsible carer is the couple's nomination (WRA 2012 s. 19(6), UC Regs reg. 86). Without one, take the claimant who works fewer hours, then the elder, rather than the nomination that maximises the award. Put the group, hours and threshold rules back in their own variables. Co-Authored-By: Claude Opus 5.5 --- .../income_floor/uc_mif_earned_income.yaml | 13 +- .../uc_is_responsible_carer.yaml | 66 ++-- .../test_uc_work_related_groups_properties.py | 145 +++++--- .../utils/uc_work_related_requirements.py | 326 +----------------- .../income/income_floor/uc_mif_applies.py | 16 +- .../uc_minimum_income_floor_gross.py | 69 ++-- .../uc_minimum_income_floor_income_tax.py | 19 +- ...minimum_income_floor_national_insurance.py | 40 ++- .../income/uc_individual_earned_income.py | 53 ++- .../uc_expected_hours.py | 22 +- .../uc_is_responsible_carer.py | 34 +- ..._work_related_group_apart_from_earnings.py | 67 +++- 12 files changed, 362 insertions(+), 508 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml index 6221c2b5ae..70ae1e6b68 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml @@ -277,10 +277,12 @@ self_employed: age: 30 self_employment_income: 6_000 + hours_worked: 1_040 would_claim_marriage_allowance: false employee: age: 30 employment_income: 25_000 + hours_worked: 1_950 would_claim_marriage_allowance: false child_1: age: 5 @@ -296,8 +298,9 @@ uc_is_responsible_carer: [true, false, false, false] uc_mif_applies: [true, false, false, false] uc_individual_earned_income_before_mif: [6_000, 21_519.60, 0, 0] - # The responsible carer's threshold is the 30-hour 17,940.624 and the - # partner's the 35-hour 20,386.028, so the couple threshold is + # The self-employed partner works fewer hours, so is the responsible + # carer by default. Their threshold is the 30-hour 17,940.624 and the + # employee's the 35-hour 20,386.028, so the couple threshold is # 38,326.652. The self-employed partner is treated as having 17,940.62 # less the 1,133.57 by which the employee is over their own threshold: # 38,326.652 - 21,519.597 = 16,807.06. Before this change both @@ -585,7 +588,7 @@ # floor is not reduced. uc_individual_earned_income: [20_386.03, 2_400, 0] -- name: Couple with a baby, both self-employed, default nomination +- name: Couple with a baby, both self-employed, the elder nominated by default period: 2026 absolute_error_margin: 0.01 input: @@ -610,8 +613,8 @@ uc_individual_earned_income_before_mif: [14_568.20, 2_000, 0] # The responsible carer of a child under 1 is in section 19 and adds # nothing to the couple threshold, so the couple threshold is the other - # partner's 20,386.03 and the couple is treated as having 20,386.03 - # whichever is nominated: the elder by default. + # partner's 20,386.03. No hours are given, so the elder is the + # responsible carer by default. uc_is_responsible_carer: [true, false, false] uc_mif_applies: [false, true, false] # 20,386.03 - 14,568.20. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml index 19cf35154b..5565c17e48 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml @@ -7,9 +7,8 @@ # to all the children" (reg. 86(3)). # # The nomination is the couple's choice and data seldom record it. Without -# an input the model takes the nomination that leaves the couple the lower -# combined earned income after the minimum income floor (the higher award), -# and the elder claimant where it makes no difference. +# an input the model takes the claimant who works fewer hours as the main +# carer, and the elder where their hours are the same. # # 2026-27 amounts for a claimant aged 21 or over in England: net floor # 20,386.03 for 35 hours (see uc_minimum_income_floor.yaml); 16-hour @@ -33,69 +32,65 @@ output: uc_is_responsible_carer: [true, false] -- name: A couple with no floor at stake default to the elder claimant +- name: In a couple the claimant who works fewer hours is the responsible carer by default period: 2026 input: people: - younger: - age: 30 - employment_income: 20_000 - elder: + full_time: age: 35 + hours_worked: 1_950 + part_time: + age: 30 + hours_worked: 832 child: age: 4 output: uc_is_responsible_carer: [false, true, false] -- name: A couple nominate the self-employed partner the floor would otherwise apply to +- name: Where a couple work the same hours the elder is the responsible carer by default period: 2026 input: people: + younger: + age: 30 + hours_worked: 1_820 elder: age: 35 - employment_income: 12_000 - self_employed: - age: 30 - self_employment_income: 5_000 + hours_worked: 1_820 child: - age: 1 + age: 4 output: - # Nominating the self-employed partner puts them in section 20, so no - # floor: combined earned income 17,000. Nominating the elder leaves the - # self-employed partner in section 22 with the floor. uc_is_responsible_carer: [false, true, false] - uc_mif_applies: [false, false, false] - uc_individual_earned_income: [12_000, 5_000, 0] -- name: Two self-employed partners default to the nomination that leaves the lower combined earned income +- name: A self-employed partner who works more hours is not the default responsible carer and has the floor period: 2026 absolute_error_margin: 0.01 input: people: - elder: + self_employed: age: 35 self_employment_income: 15_000 + hours_worked: 2_080 would_claim_marriage_allowance: false - younger: + partner: age: 30 self_employment_income: 2_000 + hours_worked: 520 would_claim_marriage_allowance: false child: age: 1 households: household: - members: [elder, younger, child] + members: [self_employed, partner, child] region: NORTH_EAST output: - # Elder: 15,000 - (15,000 - 1,000 - 12,570) x 20% - (15,000 - 12,570) - # x 6% = 15,000 - 286 - 145.80. + # 15,000 - (15,000 - 1,000 - 12,570) x 20% - (15,000 - 12,570) x 6% + # = 15,000 - 286 - 145.80. uc_individual_earned_income_before_mif: [14_568.20, 2_000, 0] - # Couple threshold either way: 20,386.03 + 10,574.72 = 30,960.75. - # Nominating the younger: the elder is treated as having 20,386.03 and - # the couple has 22,386.03. Nominating the elder: the younger would be - # treated as having 30,960.75 - 14,568.20 = 16,392.55 and the couple - # would have 30,960.75. uc_is_responsible_carer: [false, true, false] + # The partner is in section 20 (a child aged 1): no floor, and a 16-hour + # threshold of 10,574.72 in the couple threshold of 30,960.75. The + # couple's 22,386.03 after the floor is below it. uc_mif_applies: [true, false, false] uc_individual_earned_income: [20_386.03, 2_000, 0] @@ -104,24 +99,28 @@ absolute_error_margin: 0.01 input: people: - elder: + self_employed: age: 35 self_employment_income: 15_000 + hours_worked: 2_080 would_claim_marriage_allowance: false uc_is_responsible_carer: true - younger: + partner: age: 30 self_employment_income: 2_000 + hours_worked: 520 would_claim_marriage_allowance: false uc_is_responsible_carer: false child: age: 1 households: household: - members: [elder, younger, child] + members: [self_employed, partner, child] region: NORTH_EAST output: uc_mif_applies: [false, true, false] + # The partner's floor of 20,386.03 less the 3,993.48 by which it and the + # nominated carer's 14,568.20 would exceed the couple threshold: # 30,960.75 - 14,568.20. uc_individual_earned_income: [14_568.20, 16_392.55, 0] @@ -147,6 +146,7 @@ uc_is_ineligible_partner: true claimant: age: 30 + hours_worked: 1_950 child: age: 4 output: diff --git a/policyengine_uk/tests/test_uc_work_related_groups_properties.py b/policyengine_uk/tests/test_uc_work_related_groups_properties.py index d642702f06..69986bbb76 100644 --- a/policyengine_uk/tests/test_uc_work_related_groups_properties.py +++ b/policyengine_uk/tests/test_uc_work_related_groups_properties.py @@ -26,21 +26,22 @@ are 35, or the lesser number for the responsible carer of a child under 13, and never more than 35. 3. Nomination: a benefit unit has one responsible carer if it has a child - under 16 and none otherwise; the responsible carer is always a claimant. -4. The default nomination is the one that gives the couple the higher award: - nominating the other member never lowers the couple's combined earned - income or raises their Universal Credit. This runs the whole model both - ways, so it also checks the functions the default nomination evaluates - against the variables that apply them. + under 16 and none otherwise, and the responsible carer is always a + claimant. Differential against the default rule written from its + description: the claimant with the fewest hours of paid work, then the + elder, then the first listed. +4. Whichever member of a couple is nominated, invariants 1 and 2 hold, and + the nomination changes nothing in a benefit unit with no child under 13 + or no claimant the floor could apply to. 5. Differential against the floor without the scope rules: the same people with everyone forced into the all work-related requirements group at 35 hours (the rule before this change). The scope rules never raise anyone's earned income, so they never lower Universal Credit before the benefit - cap. + cap, under the default nomination or the other one. 6. Monotone: more earnings never lower a benefit unit's earned income or - raise its Universal Credit before the benefit cap, whichever member the - default nomination picks. The intended exception is a loss raised to - exactly zero, which the model reads as no self-employment. + raise its Universal Credit before the benefit cap. The default + nomination does not depend on earnings. The intended exception is a loss + raised to exactly zero, which the model reads as no self-employment. 7. A partner who cannot be a joint claimant (reg. 3(3)) never has the floor, and adds the same amount to the couple threshold whatever their age. @@ -120,6 +121,7 @@ def families(draw, ineligible_partners=False): pension_contributions=draw(pension), uc_is_in_startup_period=draw(rarely), care_hours=draw(st.sampled_from([0, 0, 0, 20, 35])), + hours_worked=draw(st.sampled_from([0, 0, 832, 1_820, 2_080])), ) for circumstance in CIRCUMSTANCES: adult[circumstance] = draw(rarely) @@ -199,6 +201,13 @@ def per_unit(person_values): values["uc_is_responsible_carer"].astype(float) ) values["claimants_in_unit"] = per_unit(values["is_uc_claimant"].astype(float)) + values["self_employed_claimants_in_unit"] = per_unit( + ( + values["uc_is_in_gainful_self_employment"].astype(bool) + & values["is_uc_claimant"].astype(bool) + & ~values["uc_is_in_startup_period"].astype(bool) + ).astype(float) + ) return values @@ -206,21 +215,8 @@ def per_unit(person_values): @given(units=populations, year=st.sampled_from(YEARS)) def test_floor_applies_only_to_the_all_requirements_group(units, year): v = calculate(units, year) - applies = v["uc_mif_applies"].astype(bool) - in_scope = ( - (v["group"] == "ALL_REQUIREMENTS") - & v["uc_is_in_gainful_self_employment"].astype(bool) - & ~v["uc_is_in_startup_period"].astype(bool) - ) - np.testing.assert_array_equal(applies, in_scope, err_msg=str(units)) # Sections 19 to 21, dependants, start-up periods: actual earned income. - before = v["uc_individual_earned_income_before_mif"] - after = v["uc_individual_earned_income"] - np.testing.assert_allclose( - after[~applies], before[~applies], atol=0.01, err_msg=str(units) - ) - assert np.all(after >= before - 0.01), units - # The group follows the Act for the circumstances the test sets. + check_scope(v, units) claimant = v["is_uc_claimant"].astype(bool) assert np.all(v["group"][~claimant] == "NOT_A_CLAIMANT"), units assert np.all(v["group"][claimant] != "NOT_A_CLAIMANT"), units @@ -270,61 +266,108 @@ def test_one_responsible_carer_where_there_is_a_child(units, year): has_child.astype(float), err_msg=str(units), ) + # The default rule, from its description: fewest hours of paid work, + # then the elder, then the first listed. for i, unit in enumerate(units): - assert has_child[v["names"].index(f"p{i}_0")] == bool(unit["children"]) + adults = unit["adults"] + expected = min( + range(len(adults)), + key=lambda j: (adults[j]["hours_worked"], -adults[j]["age"], j), + ) + for j in range(len(adults)): + nominated = bool(carer[v["names"].index(f"p{i}_{j}")]) + assert nominated == (bool(unit["children"]) and j == expected), units def other_nomination(units, v): - """Inputs that nominate the other member of every couple with a child.""" - overrides = {} + """Inputs that nominate the other member of every couple with a child. + + An input set for some people sets it for everyone (the rest take the + default, False), so every person gets one: their default nomination, or + its opposite in a couple with a child. + """ + nominated = v["uc_is_responsible_carer"].astype(bool) + overrides = { + name: {"uc_is_responsible_carer": bool(value)} + for name, value in zip(v["names"], nominated) + } + flipped = False for i, unit in enumerate(units): names = [f"p{i}_{j}" for j in range(len(unit["adults"]))] - nominated = [ - bool(v["uc_is_responsible_carer"][v["names"].index(name)]) for name in names - ] - if len(names) == 2 and any(nominated): - for name, is_nominated in zip(names, nominated): - overrides[name] = {"uc_is_responsible_carer": not is_nominated} - return overrides + if len(names) == 2 and unit["children"]: + for name in names: + overrides[name]["uc_is_responsible_carer"] ^= True + flipped = True + return overrides if flipped else {} + + +def check_scope(v, units): + applies = v["uc_mif_applies"].astype(bool) + in_scope = ( + (v["group"] == "ALL_REQUIREMENTS") + & v["uc_is_in_gainful_self_employment"].astype(bool) + & ~v["uc_is_in_startup_period"].astype(bool) + ) + np.testing.assert_array_equal(applies, in_scope, err_msg=str(units)) + before = v["uc_individual_earned_income_before_mif"] + after = v["uc_individual_earned_income"] + np.testing.assert_allclose( + after[~applies], before[~applies], atol=0.01, err_msg=str(units) + ) + assert np.all(after >= before - 0.01), units @PROPERTY_SETTINGS @given(units=populations, year=st.sampled_from(YEARS)) -def test_default_nomination_gives_the_couple_the_higher_award(units, year): +def test_scope_holds_whichever_member_is_nominated(units, year): default = calculate(units, year) overrides = other_nomination(units, default) assume(overrides) other = calculate(units, year, overrides=overrides) - assert np.all(other["uc_earned_income"] >= default["uc_earned_income"] - 0.01), ( - units + check_scope(other, units) + np.testing.assert_array_equal( + other["responsible_carers_in_unit"], default["responsible_carers_in_unit"] + ) + # The nomination matters only through a child under 13 and a claimant + # the floor could apply to. + limit = default["parameters"].responsible_carer.expected_hours.child_age_limit + matters = (default["person_uc_youngest_child_age"] < limit) & ( + default["self_employed_claimants_in_unit"] > 0 + ) + np.testing.assert_allclose( + other["uc_individual_earned_income"][~matters], + default["uc_individual_earned_income"][~matters], + atol=0.01, + err_msg=str(units), ) - assert np.all( - other["universal_credit_pre_benefit_cap"] - <= default["universal_credit_pre_benefit_cap"] + 0.01 - ), units -@PROPERTY_SETTINGS -@given(units=populations, year=st.sampled_from(YEARS)) -def test_scope_rules_never_raise_earned_income(units, year): - v = calculate(units, year) - # The floor without the scope rules: every claimant in the all - # work-related requirements group with 35 expected hours. - overrides = { +def unrestricted(units, year, v): + """The floor without the scope rules, as before this change.""" + forced = { name: { GROUP: "ALL_REQUIREMENTS" if claimant else "NOT_A_CLAIMANT", "uc_expected_hours": 35, } for name, claimant in zip(v["names"], v["is_uc_claimant"]) } - unrestricted = calculate(units, year, overrides=overrides) + return calculate(units, year, overrides=forced) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS), other=st.booleans()) +def test_scope_rules_never_raise_earned_income(units, year, other): + v = calculate(units, year) + before_change = unrestricted(units, year, v) + if other: + v = calculate(units, year, overrides=other_nomination(units, v)) assert np.all( v["uc_individual_earned_income"] - <= unrestricted["uc_individual_earned_income"] + 0.01 + <= before_change["uc_individual_earned_income"] + 0.01 ), units assert np.all( v["universal_credit_pre_benefit_cap"] - >= unrestricted["universal_credit_pre_benefit_cap"] - 0.01 + >= before_change["universal_credit_pre_benefit_cap"] - 0.01 ), units diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index 0a3e8fe106..15370b2918 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -1,42 +1,17 @@ -"""Universal Credit work-related groups and the minimum income floor. +"""Who the claimants of a Universal Credit award are. -Welfare Reform Act 2012 ss. 19 to 22 sort claimants into four groups: no -work-related requirements (s. 19), the work-focused interview requirement only -(s. 20), the work preparation requirement (s. 21) and all work-related -requirements (s. 22). The minimum income floor of the Universal Credit -Regulations 2013 reg. 62 applies only to a claimant who "would, apart from this -regulation or regulation 90, fall within section 22", and the group sets each -claimant's individual threshold (reg. 90(2)). - -A member of a couple is the responsible carer for a child only if the couple -nominate them (s. 19(6)(b)), and only one of them can be nominated (reg. 86(2)). -Several rules turn on that nomination: the group of the responsible carer of a -child under 3 (ss. 19(2)(c), 20(1)(a), 21(1)(aa)), the expected hours of the -responsible carer of a child under 13 (reg. 88(2)(aa) and (b)), and so each -partner's threshold and the couple threshold. Each rule is written here once, -as a function of the nomination. The variables call these functions with the -recorded nomination. The default nomination (uc_is_responsible_carer) calls -them with each member of the couple in turn and keeps the nomination that -leaves the couple the lower combined earned income. +A claim is made by a single person or jointly by the two members of a couple +(Welfare Reform Act 2012 s. 2(1)). The work-related groups of ss. 19 to 22 and +the minimum income floor of the Universal Credit Regulations 2013 reg. 62 +apply to claimants. """ -from policyengine_uk.model_api import * - -# The groups in the order the Act tests them: section 21 covers a claimant -# who "does not fall within section 19 or 20", and section 22 one "not -# falling within any of sections 19 to 21". -NO_REQUIREMENTS = 0 -INTERVIEW_ONLY = 1 -WORK_PREPARATION = 2 -ALL_REQUIREMENTS = 3 -NOT_A_CLAIMANT = 4 - def couple_members(person, period): """The claimant and any partner: at most two people in a benefit unit. - A claim has at most two claimants. Where the data flag more (an adult - child in the parents' benefit unit), the two eldest are the couple. + Where the data flag more (an adult child in the parents' benefit unit), + the two eldest are the couple. """ age = person("age", period) flagged = person("is_uc_claimant", period) @@ -50,290 +25,3 @@ def claimants(person, period): couple but not a claimant: no work-related group or floor applies to them. """ return couple_members(person, period) & ~person("uc_is_ineligible_partner", period) - - -def work_related_group(person, period, parameters, responsible_carer): - """Each person's group apart from regs. 62 and 90, given the nomination. - - Regulation 90 puts a claimant whose earnings reach their threshold, and - one treated as having the minimum income floor, in section 19. Regulation - 62(1)(b) asks where the claimant would fall apart from those routes, so - neither is tested here. - """ - p = parameters(period).gov.dwp.universal_credit.work_requirements - child_age = p.responsible_carer.child_age - youngest = person.benunit("uc_youngest_child_age", period) - no_requirements = ( - # s. 19(2)(a): limited capability for work and work-related activity. - person("uc_limited_capability_for_WRA", period) - # s. 19(2)(b) with reg. 30: regular and substantial caring - # responsibilities for a severely disabled person. The same flag - # covers the 35-hour carers of reg. 89(1)(b). - | person("is_carer_for_benefits", period) - # s. 19(2)(c): the responsible carer for a child under the age of 1. - | (responsible_carer & (youngest < child_age.no_requirements)) - # Reg. 89(1)(a): the qualifying age for State Pension Credit. - | person("is_SP_age", period) - # Reg. 89(1)(c): 11 weeks before to 15 weeks after confinement. - | person("uc_is_in_pregnancy_or_post_confinement_period", period) - # Reg. 89(1)(d): an adopter in the 12 months after placement. - | person("uc_is_adopter_in_first_year", period) - # Reg. 89(1)(da) and (e): students. - | person("uc_is_student_with_no_work_related_requirements", period) - # Reg. 89(1)(f): the responsible foster parent of a child under 1. - | person("uc_is_responsible_foster_parent_of_child_under_one", period) - ) - interview_only = ( - # s. 20(1)(a): the responsible carer for a child aged 1 (before 3 - # April 2017, aged at least 1 and under the prescribed age). - (responsible_carer & (youngest < child_age.interview_only)) - # Reg. 91(2): foster parents, and friend or family carers in their - # first 12 months. - | person("uc_is_foster_parent_or_new_friend_or_family_carer", period) - ) - work_preparation = ( - # s. 21(1)(a): limited capability for work. - person("uc_has_limited_capability_for_work", period) - # s. 21(1)(aa): the responsible carer for a child aged 2 (from 28 - # April 2014 to 2 April 2017, reg. 91A: aged 3 or 4). - | (responsible_carer & (youngest < child_age.work_preparation)) - ) - return select( - [ - ~claimants(person, period), - no_requirements, - interview_only, - work_preparation, - ], - [NOT_A_CLAIMANT, NO_REQUIREMENTS, INTERVIEW_ONLY, WORK_PREPARATION], - default=ALL_REQUIREMENTS, - ) - - -def expected_hours(person, period, parameters, responsible_carer): - """Expected hours of work each week (reg. 88), given the nomination. - - The number is 35 unless a lesser number applies. The regulation leaves - each lesser number to the Secretary of State. The model takes the hours - DWP says it uses for the responsible carer of a child under 13, and 35 - for everyone else: it has no rule for the hours of a relevant carer - (reg. 88(2)(a)) or a claimant with an impairment (reg. 88(2)(c)), which - uc_expected_hours can take as an input. - """ - p = parameters(period) - hours = p.gov.dwp.universal_credit.work_requirements - carer_hours = hours.responsible_carer.expected_hours - youngest = person.benunit("uc_youngest_child_age", period) - under_limit = responsible_carer & (youngest < carer_hours.child_age_limit) - of_school_age = p.gov.dfe.compulsory_school_age.calc(youngest) - lesser = where( - of_school_age, - # Reg. 88(2)(b): a child of compulsory school age under 13. - carer_hours.compulsory_school_age, - # Reg. 88(2)(aa): a child who has not reached compulsory school age. - carer_hours.below_compulsory_school_age, - ) - return where( - under_limit, - min_(lesser, hours.default_expected_hours), - hours.default_expected_hours, - ) - - -def threshold_hours(person, period, parameters, group, hours): - """Weekly hours behind each person's amount in the couple threshold. - - Regulation 90(2) gives a claimant who would otherwise be in section 22 - their expected hours, and one who would otherwise be in section 20 or 21 - 16 hours. It sets no threshold for a claimant in section 19 for a reason - other than earnings; the parameter records the hours the model uses for - such a partner (none). For a partner who is not a joint claimant, reg. - 90(3)(b)(ii) adds the pay for 35 hours. - """ - p = parameters(period).gov.dwp.universal_credit - work = p.work_requirements - floor = p.means_test.minimum_income_floor - ineligible_partner = couple_members(person, period) & person( - "uc_is_ineligible_partner", period - ) - return select( - [ - ineligible_partner, - group == ALL_REQUIREMENTS, - (group == INTERVIEW_ONLY) | (group == WORK_PREPARATION), - group == NO_REQUIREMENTS, - ], - [ - work.default_expected_hours, - hours, - work.interview_or_preparation_threshold_hours, - floor.no_requirements_partner_hours, - ], - default=0, - ) - - -def gross_threshold(person, period, parameters, weekly_hours): - """The reg. 90(2) or 90(3)(b)(ii) amount for the hours, over a year. - - Regulation 90(2) uses the rate "a person of the same age as the claimant - would be paid" under the National Minimum Wage Regulations 2015 reg. 4 or - 4A(1)(a) to (c): the rate for their age, never the apprenticeship rate of - reg. 4A(1)(d). Regulation 90(3)(b)(ii) uses "the hourly rate specified in - regulation 4", the national living wage rate, whatever the partner's age. - """ - wage = parameters(period).gov.hmrc.minimum_wage.non_apprentice - ineligible_partner = person("uc_is_ineligible_partner", period) - hourly_rate = where( - ineligible_partner, - # The scale's top rate: the national living wage (the adult rate - # before 1 April 2016). - wage.calc(np.full(person.count, 200.0)), - wage.calc(person("age", period)), - ) - # Regulation 6(1A)(a) disregards fractions of a pound only in amounts - # calculated for reg. 90 itself. Floors DWP has issued keep the pence: - # 1,642.72 a month for 2025-26 from a threshold of 1,851.85 (12.21 x - # 35 x 52 / 12; University of Bath IPR, "Going it alone", 2025, - # "information supplied by the DWP"), where a whole-pound threshold - # would give 1,642.09. So the threshold here is not rounded. - return hourly_rate * weekly_hours * WEEKS_IN_YEAR - - -def income_tax_on_threshold(person, period, parameters, threshold): - """Income tax on the threshold as the person's only income. - - Regulation 62(4)(b) deducts "such amount for income tax ... as the - Secretary of State considers appropriate". The model treats the threshold - as the person's only income: only the standard personal allowance - applies, and no other allowance or relief. - """ - income_tax = parameters(period).gov.hmrc.income_tax - taxable = max_(0, threshold - income_tax.allowances.personal_allowance.amount) - return where( - person("pays_scottish_income_tax", period), - income_tax.rates.scotland.rates.calc(taxable), - income_tax.rates.uk.calc(taxable), - ) - - -def national_insurance_on_threshold(person, period, parameters, threshold): - """National Insurance on the threshold as the person's only earnings. - - Class 2 and Class 4 on self-employed profits of that amount, as in DWP's - figures, or primary Class 1 on pay of that amount where the parameter - selects it (reg. 62(4)(b)). - """ - p = parameters(period) - ni = p.gov.hmrc.national_insurance - # Primary Class 1 on pay equal to the threshold, with the annual - # thresholds (weekly amounts times 52) the model uses for employees. - class_1 = ni.class_1 - primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR - upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR - employee = class_1.rates.employee.main * max_( - 0, min_(threshold, upper_earnings_limit) - primary_threshold - ) + class_1.rates.employee.additional * max_(0, threshold - upper_earnings_limit) - # Class 2 and Class 4 on profits equal to the threshold. - class_2 = ( - (threshold >= ni.class_2.small_profits_threshold) - * ni.class_2.flat_rate - * WEEKS_IN_YEAR - ) - class_4 = ni.class_4 - class_4_amount = class_4.rates.main * max_( - 0, - min_(threshold, class_4.thresholds.upper_profits_limit) - - class_4.thresholds.lower_profits_limit, - ) + class_4.rates.additional * max_( - 0, threshold - class_4.thresholds.upper_profits_limit - ) - self_employed = class_2 + class_4_amount - floor = p.gov.dwp.universal_credit.means_test.minimum_income_floor - amount = where(floor.self_employed_national_insurance, self_employed, employee) - # No primary Class 1, Class 2 or Class 4 is due from anyone the model - # treats as not liable (under 16 or over State Pension age). - return person("ni_liable", period) * amount - - -def net_threshold(person, period, parameters, gross): - """The threshold converted to a net amount (reg. 62(4)).""" - deductions = income_tax_on_threshold( - person, period, parameters, gross - ) + national_insurance_on_threshold(person, period, parameters, gross) - return max_(0, gross - deductions) - - -def floor_applies(person, period, group): - """Whether reg. 62 applies to the person, given their group. - - Regulation 62(1): a claimant who is in gainful self-employment and would, - apart from regs. 62 and 90, fall within section 22; reg. 62(5) leaves out - start-up periods. A dependant, or a partner who is not a joint claimant, - is in no group. - """ - return ( - (group == ALL_REQUIREMENTS) - & person("uc_is_in_gainful_self_employment", period) - & ~person("uc_is_in_startup_period", period) - ) - - -def treated_earned_income(person, earned_income, threshold, applies, couple): - """Earned income after the floor (reg. 62(2) and (3)). - - A single claimant below their threshold is treated as having the - threshold (reg. 62(2)). A member of a couple is treated as having it only - while the couple's combined earned income is also below the couple - threshold, less any amount by which it and the partner's earned income - would exceed that threshold (reg. 62(3)). For a single claimant the - couple threshold is their own and reg. 62(3) reduces to reg. 62(2). - Partners' earned income is their actual earned income: when both are - below their thresholds each is treated as having their own, whichever - partner's floor is applied first. - """ - couple_earned_income = earned_income * couple - partner_earned_income = ( - person.benunit.sum(couple_earned_income) - couple_earned_income - ) - # Reg. 90(3): the sum of the joint claimants' individual thresholds, or - # the claimant's threshold and the 35-hour amount for a partner who is - # not a joint claimant. - couple_threshold = person.benunit.sum(threshold * couple) - below_individual_threshold = earned_income < threshold - below_couple_threshold = earned_income + partner_earned_income < couple_threshold - floor = threshold - max_(0, threshold + partner_earned_income - couple_threshold) - return where( - applies & couple & below_individual_threshold & below_couple_threshold, - floor, - earned_income, - ) - - -def combined_earned_income(person, period, parameters, responsible_carer): - """The couple's combined earned income after the floor, for a nomination. - - Returned for each person's benefit unit. - """ - couple = couple_members(person, period) - group = work_related_group(person, period, parameters, responsible_carer) - hours = expected_hours(person, period, parameters, responsible_carer) - threshold = net_threshold( - person, - period, - parameters, - gross_threshold( - person, - period, - parameters, - threshold_hours(person, period, parameters, group, hours), - ), - ) - treated = treated_earned_income( - person, - person("uc_individual_earned_income_before_mif", period), - threshold, - floor_applies(person, period, group), - couple, - ) - return person.benunit.sum(treated * couple) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py index b9b284803e..c24275f705 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_mif_applies.py @@ -1,9 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - ALL_REQUIREMENTS, - claimants, - floor_applies, -) class uc_mif_applies(Variable): @@ -22,10 +17,11 @@ class uc_mif_applies(Variable): def formula(person, period, parameters): # Reg. 62(1) applies to a claimant, not a dependant, who (a) is in # gainful self-employment and (b) "would, apart from this regulation - # or regulation 90, fall within section 22 of the Act". Reg. 62(5) - # leaves out start-up periods. + # or regulation 90, fall within section 22 of the Act". Only a + # claimant has a work-related group. group = person("uc_work_related_group_apart_from_earnings", period) all_requirements = group == group.possible_values.ALL_REQUIREMENTS - return claimants(person, period) & floor_applies( - person, period, where(all_requirements, ALL_REQUIREMENTS, -1) - ) + gainfully_self_employed = person("uc_is_in_gainful_self_employment", period) + # Reg. 62(5) leaves out assessment periods in a start-up period. + in_startup_period = person("uc_is_in_startup_period", period) + return all_requirements & gainfully_self_employed & ~in_startup_period diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py index 67fc46febb..f75871f9f5 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py @@ -1,12 +1,5 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - ALL_REQUIREMENTS, - INTERVIEW_ONLY, - NO_REQUIREMENTS, - WORK_PREPARATION, - gross_threshold, - threshold_hours, -) +from policyengine_uk.utils.uc_work_related_requirements import couple_members class uc_minimum_income_floor_gross(Variable): @@ -46,23 +39,57 @@ class uc_minimum_income_floor_gross(Variable): ] def formula(person, period, parameters): + p = parameters(period) + work = p.gov.dwp.universal_credit.work_requirements + floor = p.gov.dwp.universal_credit.means_test.minimum_income_floor + wage = p.gov.hmrc.minimum_wage.non_apprentice group = person("uc_work_related_group_apart_from_earnings", period) groups = group.possible_values - group_index = select( + ineligible_partner = couple_members(person, period) & person( + "uc_is_ineligible_partner", period + ) + hours = select( [ - group == groups.NO_REQUIREMENTS, - group == groups.INTERVIEW_ONLY, - group == groups.WORK_PREPARATION, + # Reg. 90(3)(b)(ii): a partner who is not a joint claimant + # adds "the amount a person would be paid for 35 hours per + # week" to the couple threshold. + ineligible_partner, + # Reg. 90(2)(b): "the expected number of hours per week in + # the case of a claimant who would otherwise fall within + # section 22". group == groups.ALL_REQUIREMENTS, + # Reg. 90(2)(a): "16 hours per week, in the case of a + # claimant who would otherwise fall within section 20 ... or + # section 21". + (group == groups.INTERVIEW_ONLY) | (group == groups.WORK_PREPARATION), + # Reg. 90(2) sets no threshold for a claimant in section 19 + # for a reason other than earnings; the parameter holds the + # hours the model uses for such a partner (none). + group == groups.NO_REQUIREMENTS, + ], + [ + work.default_expected_hours, + person("uc_expected_hours", period), + work.interview_or_preparation_threshold_hours, + floor.no_requirements_partner_hours, ], - [NO_REQUIREMENTS, INTERVIEW_ONLY, WORK_PREPARATION, ALL_REQUIREMENTS], - default=-1, + default=0, ) - hours = threshold_hours( - person, - period, - parameters, - group_index, - person("uc_expected_hours", period), + # Reg. 90(2) uses the rate "a person of the same age as the claimant + # would be paid" under NMW Regs reg. 4 or 4A(1)(a) to (c): the rate + # for their age, never the apprenticeship rate of reg. 4A(1)(d). Reg. + # 90(3)(b)(ii) uses "the hourly rate specified in regulation 4", the + # national living wage rate, whatever the partner's age: the scale's + # top rate (the adult rate before 1 April 2016). + hourly_rate = where( + ineligible_partner, + wage.calc(np.full(person.count, 200.0)), + wage.calc(person("age", period)), ) - return gross_threshold(person, period, parameters, hours) + # Reg. 6(1A)(a) disregards fractions of a pound only in amounts + # calculated for reg. 90 itself. Floors DWP has issued keep the pence: + # 1,642.72 a month for 2025-26 from a threshold of 1,851.85 (12.21 x + # 35 x 52 / 12; University of Bath IPR, "Going it alone", 2025, + # "information supplied by the DWP"), where a whole-pound threshold + # would give 1,642.09. So the threshold here is not rounded. + return hourly_rate * hours * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py index 4fe72a4b6a..c570e6eb1a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_income_tax.py @@ -1,7 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - income_tax_on_threshold, -) class uc_minimum_income_floor_income_tax(Variable): @@ -30,9 +27,15 @@ class uc_minimum_income_floor_income_tax(Variable): ] def formula(person, period, parameters): - return income_tax_on_threshold( - person, - period, - parameters, - person("uc_minimum_income_floor_gross", period), + # Reg. 62(4)(b) deducts "such amount for income tax ... as the + # Secretary of State considers appropriate". The model treats the + # threshold as the person's only income: only the standard personal + # allowance applies, and no other allowance or relief. + income_tax = parameters(period).gov.hmrc.income_tax + threshold = person("uc_minimum_income_floor_gross", period) + taxable = max_(0, threshold - income_tax.allowances.personal_allowance.amount) + return where( + person("pays_scottish_income_tax", period), + income_tax.rates.scotland.rates.calc(taxable), + income_tax.rates.uk.calc(taxable), ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py index 9b8648ef17..4b3f4bff03 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_national_insurance.py @@ -1,7 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - national_insurance_on_threshold, -) class uc_minimum_income_floor_national_insurance(Variable): @@ -31,9 +28,36 @@ class uc_minimum_income_floor_national_insurance(Variable): ] def formula(person, period, parameters): - return national_insurance_on_threshold( - person, - period, - parameters, - person("uc_minimum_income_floor_gross", period), + p = parameters(period) + ni = p.gov.hmrc.national_insurance + threshold = person("uc_minimum_income_floor_gross", period) + # Primary Class 1 on pay equal to the threshold, with the annual + # thresholds (weekly amounts times 52) the model uses for employees. + class_1 = ni.class_1 + primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR + upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR + employee = class_1.rates.employee.main * max_( + 0, min_(threshold, upper_earnings_limit) - primary_threshold + ) + class_1.rates.employee.additional * max_( + 0, threshold - upper_earnings_limit ) + # Class 2 and Class 4 on profits equal to the threshold. + class_2 = ( + (threshold >= ni.class_2.small_profits_threshold) + * ni.class_2.flat_rate + * WEEKS_IN_YEAR + ) + class_4 = ni.class_4 + class_4_amount = class_4.rates.main * max_( + 0, + min_(threshold, class_4.thresholds.upper_profits_limit) + - class_4.thresholds.lower_profits_limit, + ) + class_4.rates.additional * max_( + 0, threshold - class_4.thresholds.upper_profits_limit + ) + self_employed = class_2 + class_4_amount + mif = p.gov.dwp.universal_credit.means_test.minimum_income_floor + amount = where(mif.self_employed_national_insurance, self_employed, employee) + # No primary Class 1, Class 2 or Class 4 is due from anyone the model + # treats as not liable (under 16 or over State Pension age). + return person("ni_liable", period) * amount diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py index b471097b68..ba90cccf7e 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py @@ -1,8 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - couple_members, - treated_earned_income, -) class uc_individual_earned_income(Variable): @@ -33,16 +29,43 @@ class uc_individual_earned_income(Variable): ] def formula(person, period, parameters): + earned_income = person("uc_individual_earned_income_before_mif", period) # Reg. 62 compares earned income, after the deductions, with the - # net threshold (reg. 62(2) and (3)). The couple threshold is the - # sum of the joint claimants' individual thresholds (reg. 90(3)(a)), - # or the claimant's threshold and a 35-hour amount for a partner who - # is not a joint claimant (reg. 90(3)(b)); each is in - # uc_minimum_income_floor. - return treated_earned_income( - person, - person("uc_individual_earned_income_before_mif", period), - person("uc_minimum_income_floor", period), - person("uc_mif_applies", period), - couple_members(person, period), + # net threshold. A single claimant below their threshold is treated + # as having the threshold (reg. 62(2)). A member of a couple is + # treated as having it only while the couple's combined earned + # income is also below the couple threshold, less any amount by + # which it and the partner's earned income would exceed that + # threshold (reg. 62(3)). The couple threshold is the sum of the + # joint claimants' individual thresholds (reg. 90(3)(a)); for a + # single claimant it is their own, and reg. 62(3) reduces to + # reg. 62(2). Partners' earned income is their actual earned income: + # when both are below their thresholds each is treated as having + # their own, whichever partner's floor is applied first. A claim has + # at most two claimants; where the data flag more (an adult child in + # the parents' benefit unit), the two eldest are the couple. + age = person("age", period) + is_claimant = person("is_uc_claimant", period) + claimant = is_claimant & ( + person.get_rank(person.benunit, -age, condition=is_claimant) < 2 ) + claimant_earned_income = earned_income * claimant + partner_earned_income = ( + person.benunit.sum(claimant_earned_income) - claimant_earned_income + ) + individual_threshold = person("uc_minimum_income_floor", period) + couple_threshold = person.benunit.sum(individual_threshold * claimant) + below_individual_threshold = earned_income < individual_threshold + below_couple_threshold = ( + earned_income + partner_earned_income < couple_threshold + ) + floor = individual_threshold - max_( + 0, individual_threshold + partner_earned_income - couple_threshold + ) + floor_applies = ( + person("uc_mif_applies", period) + & claimant + & below_individual_threshold + & below_couple_threshold + ) + return where(floor_applies, floor, earned_income) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py index c5640ca97b..45d2225ab6 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_expected_hours.py @@ -1,5 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import expected_hours class uc_expected_hours(Variable): @@ -30,6 +29,23 @@ class uc_expected_hours(Variable): unit = "hour" def formula(person, period, parameters): - return expected_hours( - person, period, parameters, person("uc_is_responsible_carer", period) + p = parameters(period) + hours = p.gov.dwp.universal_credit.work_requirements + carer_hours = hours.responsible_carer.expected_hours + youngest = person.benunit("uc_youngest_child_age", period) + lesser = where( + p.gov.dfe.compulsory_school_age.calc(youngest), + # Reg. 88(2)(b): a child of compulsory school age under 13. + carer_hours.compulsory_school_age, + # Reg. 88(2)(aa): a child below compulsory school age. + carer_hours.below_compulsory_school_age, + ) + applies = person("uc_is_responsible_carer", period) & ( + youngest < carer_hours.child_age_limit + ) + # Reg. 88(1): 35 "unless some lesser number of hours applies". + return where( + applies, + min_(lesser, hours.default_expected_hours), + hours.default_expected_hours, ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py index 62e465bb3b..76dd335f9a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py @@ -1,8 +1,5 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - claimants, - combined_earned_income, -) +from policyengine_uk.utils.uc_work_related_requirements import claimants class uc_is_responsible_carer(Variable): @@ -14,13 +11,11 @@ class uc_is_responsible_carer(Variable): "their benefit unit: a single claimant who is responsible for a " "child, or the member of a couple whom the couple have jointly " "nominated. Only one joint claimant can be nominated, and the " - "nomination covers all their children. Datasets and households " - "should supply the nomination where they know it. Without one, the " - "model takes the nomination that leaves the couple the lower " - "combined earned income after the minimum income floor, which is the " - "nomination that gives them the higher award: the choice is the " - "couple's. Where the nomination makes no difference the elder " - "claimant is the responsible carer." + "nomination covers all their children. The nomination is the " + "couple's choice: datasets and households should supply it where " + "they know it. Without one, the model takes the claimant who works " + "fewer hours as the main carer, and the elder where their hours are " + "the same." ) reference = [ dict( @@ -39,12 +34,11 @@ def formula(person, period, parameters): # s. 19(6): a single person who is responsible for the child, or the # nominated member of a couple either of whom is responsible for it. has_child = person.benunit("uc_youngest_child_age", period) < np.inf - rank = person.get_rank(person.benunit, -person("age", period), claimant) - elder = claimant & (rank == 0) - younger = claimant & (rank == 1) - # Reg. 86(2) and (4): the couple choose which of them to nominate. - # The floor is the only consequence of the choice in the model. - if_elder = combined_earned_income(person, period, parameters, elder) - if_younger = combined_earned_income(person, period, parameters, younger) - nominate_younger = person.benunit.any(younger) & (if_younger < if_elder) - return has_child & where(nominate_younger, younger, elder) + # Reg. 86(2): "Only one of joint claimants may be nominated". The + # default nomination is the claimant with fewer hours of paid work, + # then the elder. A single claimant is the only candidate. + hours = person("hours_worked", period) + fewest_hours = person.benunit.min(where(claimant, hours, np.inf)) + candidate = claimant & (hours == fewest_hours) + eldest = person.get_rank(person.benunit, -person("age", period), candidate) == 0 + return has_child & candidate & eldest diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py index 54676a264c..16d6c1c59b 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py @@ -1,11 +1,5 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import ( - ALL_REQUIREMENTS, - INTERVIEW_ONLY, - NO_REQUIREMENTS, - WORK_PREPARATION, - work_related_group, -) +from policyengine_uk.utils.uc_work_related_requirements import claimants class UCWorkRelatedGroup(Enum): @@ -49,21 +43,64 @@ class uc_work_related_group_apart_from_earnings(Variable): definition_period = YEAR def formula(person, period, parameters): - group = work_related_group( - person, period, parameters, person("uc_is_responsible_carer", period) + p = parameters(period).gov.dwp.universal_credit.work_requirements + child_age = p.responsible_carer.child_age + responsible_carer = person("uc_is_responsible_carer", period) + youngest = person.benunit("uc_youngest_child_age", period) + no_requirements = ( + # s. 19(2)(a): limited capability for work and work-related + # activity. + person("uc_limited_capability_for_WRA", period) + # s. 19(2)(b) with reg. 30: regular and substantial caring + # responsibilities for a severely disabled person. The same flag + # covers the 35-hour carers of reg. 89(1)(b). + | person("is_carer_for_benefits", period) + # s. 19(2)(c): the responsible carer for a child under the age + # of 1. + | (responsible_carer & (youngest < child_age.no_requirements)) + # Reg. 89(1)(a): the qualifying age for State Pension Credit. + | person("is_SP_age", period) + # Reg. 89(1)(c): 11 weeks before to 15 weeks after confinement. + | person("uc_is_in_pregnancy_or_post_confinement_period", period) + # Reg. 89(1)(d): an adopter in the 12 months after placement. + | person("uc_is_adopter_in_first_year", period) + # Reg. 89(1)(da) and (e): students. + | person("uc_is_student_with_no_work_related_requirements", period) + # Reg. 89(1)(f): the responsible foster parent of a child under 1. + | person("uc_is_responsible_foster_parent_of_child_under_one", period) ) + interview_only = ( + # s. 20(1)(a): the responsible carer for a child aged 1 (before 3 + # April 2017, aged at least 1 and under the prescribed age). + (responsible_carer & (youngest < child_age.interview_only)) + # Reg. 91(2): foster parents, and friend or family carers in + # their first 12 months. + | person("uc_is_foster_parent_or_new_friend_or_family_carer", period) + ) + work_preparation = ( + # s. 21(1)(a): limited capability for work. + person("uc_has_limited_capability_for_work", period) + # s. 21(1)(aa): the responsible carer for a child aged 2 (from 28 + # April 2014 to 2 April 2017, reg. 91A: aged 3 or 4). + | (responsible_carer & (youngest < child_age.work_preparation)) + ) + # The Act tests the groups in order: section 21 covers a claimant + # who "does not fall within section 19 or 20", and section 22 one + # "not falling within any of sections 19 to 21". The earnings routes + # into section 19 (reg. 90) are left out: reg. 62(1)(b) and reg. + # 90(2) ask where the claimant would otherwise fall. return select( [ - group == NO_REQUIREMENTS, - group == INTERVIEW_ONLY, - group == WORK_PREPARATION, - group == ALL_REQUIREMENTS, + ~claimants(person, period), + no_requirements, + interview_only, + work_preparation, ], [ + UCWorkRelatedGroup.NOT_A_CLAIMANT, UCWorkRelatedGroup.NO_REQUIREMENTS, UCWorkRelatedGroup.INTERVIEW_ONLY, UCWorkRelatedGroup.WORK_PREPARATION, - UCWorkRelatedGroup.ALL_REQUIREMENTS, ], - default=UCWorkRelatedGroup.NOT_A_CLAIMANT, + default=UCWorkRelatedGroup.ALL_REQUIREMENTS, ) From 8288259f3f344de04f21dc0464b2f048641fd46c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 12:54:04 -0400 Subject: [PATCH 3/9] Declare the claimant and partner in the UC work-related group property fixtures Main's claimant-or-partner inference (#1896) presumes a member under 20 who is at least 16 years younger than the claimant to be the claimant's child. The work-related group properties generate couples such as 52 and 18 without relationship inputs, so after main was merged in the 18-year-old was read as a child, the reg 3(3) flag landed on someone outside the couple, and test_partner_who_cannot_be_a_joint_claimant failed (it fails the same way on 6db0ab118, before this branch's own changes). Set is_claimant_or_partner from each generated role, as #1949 did for its fixtures in a57caf1dd. (cherry picked from commit 0dfcc26a7d2522fbdd936dd9adf1e8a99a073f30) Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_work_related_groups_properties.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_uc_work_related_groups_properties.py b/policyengine_uk/tests/test_uc_work_related_groups_properties.py index 69986bbb76..cd1225e5b5 100644 --- a/policyengine_uk/tests/test_uc_work_related_groups_properties.py +++ b/policyengine_uk/tests/test_uc_work_related_groups_properties.py @@ -151,6 +151,10 @@ def situation(units, year, bump=None, overrides=None): for j, adult in enumerate(unit["adults"]): name = f"p{i}_{j}" person = {k: {year: v} for k, v in adult.items()} + # The generated adults are the claimant and partner; say so, so the + # claimant-or-partner presumption (a member under 20 and much + # younger is the head's child) does not apply. Children get False. + person["is_claimant_or_partner"] = {year: True} if bump is not None and bump[:2] == (i, j): variable, amount = bump[2], bump[3] person[variable] = {year: adult[variable] + amount} @@ -159,7 +163,7 @@ def situation(units, year, bump=None, overrides=None): names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunits[f"b{i}"] = {"members": names} households[f"h{i}"] = { From 7a71c869e3867d063546affa5f79ac394e5d38a6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 12:54:49 -0400 Subject: [PATCH 4/9] Read is_uc_assessed_claimant for the couple instead of restating it couple_members() in utils/uc_work_related_requirements.py repeated main's is_uc_assessed_claimant formula line for line (is_uc_claimant, the two eldest). Read the variable instead, so the two cannot drift. No output changes: the formulas were identical. Review: vahid-ahmadi on #2081, item 5. Co-Authored-By: Claude Opus 5.5 --- .../utils/uc_work_related_requirements.py | 21 +++++++------------ .../uc_minimum_income_floor_gross.py | 3 +-- .../uc_youngest_child_age.py | 3 +-- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/policyengine_uk/utils/uc_work_related_requirements.py b/policyengine_uk/utils/uc_work_related_requirements.py index 15370b2918..3e6b956e69 100644 --- a/policyengine_uk/utils/uc_work_related_requirements.py +++ b/policyengine_uk/utils/uc_work_related_requirements.py @@ -7,21 +7,14 @@ """ -def couple_members(person, period): - """The claimant and any partner: at most two people in a benefit unit. - - Where the data flag more (an adult child in the parents' benefit unit), - the two eldest are the couple. - """ - age = person("age", period) - flagged = person("is_uc_claimant", period) - return flagged & (person.get_rank(person.benunit, -age, condition=flagged) < 2) - - def claimants(person, period): """The single claimant or the joint claimants. - A partner who cannot be a joint claimant (reg. 3(3)) is a member of the - couple but not a claimant: no work-related group or floor applies to them. + The members of the couple are `is_uc_assessed_claimant`: at most two + people in a benefit unit. A partner who cannot be a joint claimant (reg. + 3(3)) is a member of the couple but not a claimant: no work-related group + or floor applies to them. """ - return couple_members(person, period) & ~person("uc_is_ineligible_partner", period) + return person("is_uc_assessed_claimant", period) & ~person( + "uc_is_ineligible_partner", period + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py index f75871f9f5..0e283f787e 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py @@ -1,5 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import couple_members class uc_minimum_income_floor_gross(Variable): @@ -45,7 +44,7 @@ def formula(person, period, parameters): wage = p.gov.hmrc.minimum_wage.non_apprentice group = person("uc_work_related_group_apart_from_earnings", period) groups = group.possible_values - ineligible_partner = couple_members(person, period) & person( + ineligible_partner = person("is_uc_assessed_claimant", period) & person( "uc_is_ineligible_partner", period ) hours = select( diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py index bc69077ae6..a0ccd66faa 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_youngest_child_age.py @@ -1,5 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.uc_work_related_requirements import couple_members class uc_youngest_child_age(Variable): @@ -34,6 +33,6 @@ def formula(benunit, period, parameters): child = ( person("is_child_for_universal_credit", period) & ~person("is_looked_after_by_local_authority", period) - & ~couple_members(person, period) + & ~person("is_uc_assessed_claimant", period) ) return benunit.min(where(child, person("age", period), np.inf)) From 44bfd87d29da58b57765f39f80a706df38eca507 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 12:54:54 -0400 Subject: [PATCH 5/9] Document the nomination default without hours and the reg 30(3) gap - uc_is_responsible_carer and the changelog: hours_worked defaults to 0, so a couple entered without hours is nominated by age; calculator and API users should set uc_is_responsible_carer or each member's hours. - uc_work_related_group_apart_from_earnings: is_carer_for_benefits does not apply reg 30(3) ("derives earned income from those caring responsibilities"), so a carer paid for that care still counts. No formula changes. Review: vahid-ahmadi on #2081, items 2 and 7. Co-Authored-By: Claude Opus 5.5 --- changelog.d/uc-mif-work-related-groups.added.md | 2 +- .../work_related_requirements/uc_is_responsible_carer.py | 5 ++++- .../uc_work_related_group_apart_from_earnings.py | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/changelog.d/uc-mif-work-related-groups.added.md b/changelog.d/uc-mif-work-related-groups.added.md index 072fa3d93d..caf9c4d5fe 100644 --- a/changelog.d/uc-mif-work-related-groups.added.md +++ b/changelog.d/uc-mif-work-related-groups.added.md @@ -1 +1 @@ -Add `uc_work_related_group_apart_from_earnings`, `uc_is_responsible_carer`, `uc_expected_hours`, `uc_youngest_child_age` and `uc_is_in_gainful_self_employment`; the inputs `uc_has_limited_capability_for_work`, `uc_is_in_pregnancy_or_post_confinement_period`, `uc_is_adopter_in_first_year`, `uc_is_student_with_no_work_related_requirements`, `uc_is_responsible_foster_parent_of_child_under_one`, `uc_is_foster_parent_or_new_friend_or_family_carer` and `uc_is_ineligible_partner`; and the parameters under `gov.dwp.universal_credit.work_requirements` for the responsible carer's child ages and expected hours and the 16-hour threshold, with `gov.dwp.universal_credit.means_test.minimum_income_floor.no_requirements_partner_hours`. +Add `uc_work_related_group_apart_from_earnings`, `uc_is_responsible_carer`, `uc_expected_hours`, `uc_youngest_child_age` and `uc_is_in_gainful_self_employment`; the inputs `uc_has_limited_capability_for_work`, `uc_is_in_pregnancy_or_post_confinement_period`, `uc_is_adopter_in_first_year`, `uc_is_student_with_no_work_related_requirements`, `uc_is_responsible_foster_parent_of_child_under_one`, `uc_is_foster_parent_or_new_friend_or_family_carer` and `uc_is_ineligible_partner`; and the parameters under `gov.dwp.universal_credit.work_requirements` for the responsible carer's child ages and expected hours and the 16-hour threshold, with `gov.dwp.universal_credit.means_test.minimum_income_floor.no_requirements_partner_hours`. Without a recorded nomination, `uc_is_responsible_carer` takes the joint claimant who works fewer hours (`hours_worked`), then the elder; `hours_worked` defaults to 0, so set `uc_is_responsible_carer` (or each member's hours) for a couple with a child entered without hours. diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py index 76dd335f9a..8ed9aca77e 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_is_responsible_carer.py @@ -15,7 +15,10 @@ class uc_is_responsible_carer(Variable): "couple's choice: datasets and households should supply it where " "they know it. Without one, the model takes the claimant who works " "fewer hours as the main carer, and the elder where their hours are " - "the same." + "the same. `hours_worked` is an input that defaults to 0, so for a " + "couple entered without hours the default falls to the elder: " + "calculator and API users should set `uc_is_responsible_carer` (or " + "each member's hours) for a couple with a child." ) reference = [ dict( diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py index 16d6c1c59b..21a7afb4b2 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/work_related_requirements/uc_work_related_group_apart_from_earnings.py @@ -53,7 +53,10 @@ def formula(person, period, parameters): person("uc_limited_capability_for_WRA", period) # s. 19(2)(b) with reg. 30: regular and substantial caring # responsibilities for a severely disabled person. The same flag - # covers the 35-hour carers of reg. 89(1)(b). + # covers the 35-hour carers of reg. 89(1)(b). It does not apply + # reg. 30(3), under which a person who "derives earned income + # from those caring responsibilities" is not such a carer, so a + # carer paid for that care still counts here. | person("is_carer_for_benefits", period) # s. 19(2)(c): the responsible carer for a child under the age # of 1. From 5ed0fd46ec30c2a1951b00d95dd683f7e0842beb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 13:33:32 -0400 Subject: [PATCH 6/9] Re-derive the floor tests main's trading allowance and State Pension age changes moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Three cases with £15,000 of profit (uc_mif_earned_income.yaml, "the elder nominated by default"; uc_is_responsible_carer.yaml, "works more hours" and "a recorded nomination"): main's #1880 fix gives no trading allowance relief on a profit above £1,000 when gross receipts are not given, so income tax is (15,000 - 12,570) x 20% = 486, not 286, and earnings before the floor are 14,368.20. The floored partner is 20,386.03 - 14,368.20 = 6,017.83, and 30,960.75 - 14,368.20 = 16,592.55 where the nominated carer has a 16-hour threshold. - uc_minimum_income_floor.yaml, main's #1973 case for someone reaching State Pension age after 6 April: at 66 they are over State Pension Credit qualifying age, so in section 19 (reg. 89(1)(a)) with no threshold. Set the group as the age-70 case does, to keep testing the notional Class 4. Review: vahid-ahmadi on #2081, item 1. Co-Authored-By: Claude Opus 5.5 --- .../income/income_floor/uc_mif_earned_income.yaml | 11 ++++++----- .../income_floor/uc_minimum_income_floor.yaml | 4 ++++ .../uc_is_responsible_carer.yaml | 15 ++++++++------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml index 37aa2ab17a..d116e15bb3 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml @@ -619,17 +619,18 @@ members: [elder, younger, child] region: NORTH_EAST output: - # Elder: 15,000 - (15,000 - 1,000 - 12,570) x 20% - (15,000 - 12,570) - # x 6% = 14,568.20. - uc_individual_earned_income_before_mif: [14_568.20, 2_000, 0] + # Elder: 15,000 - (15,000 - 12,570) x 20% - (15,000 - 12,570) x 6% + # = 15,000 - 486 - 145.80 = 14,368.20. Without gross receipts the + # trading allowance gives no relief on a profit above it. + uc_individual_earned_income_before_mif: [14_368.20, 2_000, 0] # The responsible carer of a child under 1 is in section 19 and adds # nothing to the couple threshold, so the couple threshold is the other # partner's 20,386.03. No hours are given, so the elder is the # responsible carer by default. uc_is_responsible_carer: [true, false, false] uc_mif_applies: [false, true, false] - # 20,386.03 - 14,568.20. - uc_individual_earned_income: [14_568.20, 5_817.83, 0] + # 20,386.03 - 14,368.20. + uc_individual_earned_income: [14_368.20, 6_017.83, 0] - name: Partner who cannot be a joint claimant, earnings below the 35-hour amount period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml index b1fb757703..939c0389e0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_minimum_income_floor.yaml @@ -112,6 +112,10 @@ # 91(a)), as on the person's actual profits. age: 66 region: NORTH_EAST + # A claimant over State Pension Credit qualifying age is subject to no + # work-related requirements and has no threshold; the group is set here + # to isolate the deduction. + uc_work_related_group_apart_from_earnings: ALL_REQUIREMENTS output: is_SP_age: true ni_class_4_liable: true diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml index 5565c17e48..75e1d0c0dc 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/work_related_requirements/uc_is_responsible_carer.yaml @@ -84,9 +84,10 @@ members: [self_employed, partner, child] region: NORTH_EAST output: - # 15,000 - (15,000 - 1,000 - 12,570) x 20% - (15,000 - 12,570) x 6% - # = 15,000 - 286 - 145.80. - uc_individual_earned_income_before_mif: [14_568.20, 2_000, 0] + # 15,000 - (15,000 - 12,570) x 20% - (15,000 - 12,570) x 6% + # = 15,000 - 486 - 145.80. Without gross receipts the trading allowance + # gives no relief on a profit above it. + uc_individual_earned_income_before_mif: [14_368.20, 2_000, 0] uc_is_responsible_carer: [false, true, false] # The partner is in section 20 (a child aged 1): no floor, and a 16-hour # threshold of 10,574.72 in the couple threshold of 30,960.75. The @@ -119,10 +120,10 @@ region: NORTH_EAST output: uc_mif_applies: [false, true, false] - # The partner's floor of 20,386.03 less the 3,993.48 by which it and the - # nominated carer's 14,568.20 would exceed the couple threshold: - # 30,960.75 - 14,568.20. - uc_individual_earned_income: [14_568.20, 16_392.55, 0] + # The partner's floor of 20,386.03 less the 3,793.48 by which it and the + # nominated carer's 14,368.20 would exceed the couple threshold: + # 30,960.75 - 14,368.20. + uc_individual_earned_income: [14_368.20, 16_592.55, 0] - name: A child looked after by a local authority has no responsible carer period: 2026 From 359c678357c5da1cf5176d2877027f3ac9f7c8de Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 15:44:46 -0400 Subject: [PATCH 7/9] Pin the #1973 couple case's award again and fix the notebook's floor text - uc_mif_earned_income.yaml, "Couple from gross incomes": the children became 15 and 14 in this PR (to keep 35 expected hours), and the universal_credit line was dropped. Both were born before 6 April 2017, so the first has the higher child element: uc_maximum_amount is 666.97 x 12 + (351.88 + 303.94) x 12 + 12,000 = 27,873.48 and universal_credit is 27,873.48 - 0.55 x 35,648.06 = 8,267.05. - universal-credit.ipynb: the nomination default ends with the elder and hours_worked defaults to 0; the responsible carer's hours are 30 only from February 2025 (16 and 25 from April 2017); the couple threshold includes 35 hours at the national living wage for a reg. 3(3) partner. Review: independent review of 5ed0fd46e, findings 3 and 4. Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/universal-credit.ipynb | 2 +- .../income/income_floor/uc_mif_earned_income.yaml | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index 25ad684456..eea4de43aa 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -44,7 +44,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: only the claimants' income counts: the single claimant, or the two joint claimants (`is_uc_assessed_claimant`; UC Regulations 2013 reg 22(1)). The earnings and unearned income of a child, a qualifying young person or anyone else in the benefit unit are theirs, not the claimant's, so the Universal Credit means test does not count them. Who the claimants are comes from `is_uc_claimant` (the claimant and partner); a member flagged there is taken as a claimant whatever their age. Each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): for example one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where the nomination is not supplied the model takes the claimant who works fewer hours. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or 30 for the responsible carer of a child under 13; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, and nothing for a partner with no work-related requirements for a reason other than earnings. The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the claimants' combined earnings above the work allowance (`uc_work_allowance`), and by all of the claimants' unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (the claimants earning enough, having a qualifying disability benefit, etc.). The earnings exception reads the claimants' earnings only (reg 82(1)(a)), the capped total counts only the claimants' own contributory benefits (WRA 2012 s.96(1)), and the childcare work condition tests only the claimants (reg 32(1)).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." + "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: only the claimants' income counts: the single claimant, or the two joint claimants (`is_uc_assessed_claimant`; UC Regulations 2013 reg 22(1)). The earnings and unearned income of a child, a qualifying young person or anyone else in the benefit unit are theirs, not the claimant's, so the Universal Credit means test does not count them. Who the claimants are comes from `is_uc_claimant` (the claimant and partner); a member flagged there is taken as a claimant whatever their age. Each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): for example one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where the nomination is not supplied the model takes the claimant who works fewer hours (`hours_worked`), then the elder. `hours_worked` defaults to 0, so for a couple entered without hours the default falls to the elder; supply `uc_is_responsible_carer` where the nomination is known. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or for the responsible carer of a child under 13 the hours DWP uses: 30 from February 2025, and from April 2017 until then 16 for a child under compulsory school age and 25 for an older child; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, nothing for a partner with no work-related requirements for a reason other than earnings, and 35 hours at the national living wage for a partner who cannot be a joint claimant (reg 90(3)(b)). The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the claimants' combined earnings above the work allowance (`uc_work_allowance`), and by all of the claimants' unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (the claimants earning enough, having a qualifying disability benefit, etc.). The earnings exception reads the claimants' earnings only (reg 82(1)(a)), the capped total counts only the claimants' own contributory benefits (WRA 2012 s.96(1)), and the childcare work condition tests only the claimants (reg 32(1)).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." }, { "cell_type": "code", diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml index d116e15bb3..79aa8f111c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml @@ -274,6 +274,12 @@ uc_individual_earned_income: [19_252.46, 21_519.60, 0, 0] # Less the work allowance of 427 x 12 = 5,124. uc_earned_income: 35_648.06 + # Both children were born before 6 April 2017, so the first has the + # higher child element (reg. 36): 666.97 x 12 + (351.88 + 303.94) x 12 + + # 12,000. + uc_maximum_amount: 27_873.48 + # 27,873.48 - 0.55 x 35,648.06. + universal_credit: 8_267.05 - name: Couple with children under 13, the responsible carer's floor uses 30 hours period: 2026 From 671da0f7e5387a02e9656fcc8cd996d3a0f573cb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 15:44:55 -0400 Subject: [PATCH 8/9] Give reg 90(3)(b)(ii)'s 35 hours their own parameter The amount a reg. 3(3) partner adds to the couple threshold reused work_requirements.default_expected_hours, the reg. 88(1) expected hours. Reg. 90(3)(b)(ii) sets its own "35 hours per week" (in force since 29 April 2013), so a reform of the expected hours should not move it. Add gov.dwp.universal_credit.means_test.minimum_income_floor.ineligible_partner_hours (35 from 2013-04-29). Both are 35 on every date, so no output changes. The property oracle now uses the regulation's 35 directly. Review: independent review of 5ed0fd46e, finding 4. Co-Authored-By: Claude Opus 5.5 --- .../uc-mif-work-related-groups.added.md | 2 +- .../ineligible_partner_hours.yaml | 20 +++++++++++++++++++ .../test_uc_work_related_groups_properties.py | 5 +++-- .../uc_minimum_income_floor_gross.py | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/ineligible_partner_hours.yaml diff --git a/changelog.d/uc-mif-work-related-groups.added.md b/changelog.d/uc-mif-work-related-groups.added.md index caf9c4d5fe..b50e96999d 100644 --- a/changelog.d/uc-mif-work-related-groups.added.md +++ b/changelog.d/uc-mif-work-related-groups.added.md @@ -1 +1 @@ -Add `uc_work_related_group_apart_from_earnings`, `uc_is_responsible_carer`, `uc_expected_hours`, `uc_youngest_child_age` and `uc_is_in_gainful_self_employment`; the inputs `uc_has_limited_capability_for_work`, `uc_is_in_pregnancy_or_post_confinement_period`, `uc_is_adopter_in_first_year`, `uc_is_student_with_no_work_related_requirements`, `uc_is_responsible_foster_parent_of_child_under_one`, `uc_is_foster_parent_or_new_friend_or_family_carer` and `uc_is_ineligible_partner`; and the parameters under `gov.dwp.universal_credit.work_requirements` for the responsible carer's child ages and expected hours and the 16-hour threshold, with `gov.dwp.universal_credit.means_test.minimum_income_floor.no_requirements_partner_hours`. Without a recorded nomination, `uc_is_responsible_carer` takes the joint claimant who works fewer hours (`hours_worked`), then the elder; `hours_worked` defaults to 0, so set `uc_is_responsible_carer` (or each member's hours) for a couple with a child entered without hours. +Add `uc_work_related_group_apart_from_earnings`, `uc_is_responsible_carer`, `uc_expected_hours`, `uc_youngest_child_age` and `uc_is_in_gainful_self_employment`; the inputs `uc_has_limited_capability_for_work`, `uc_is_in_pregnancy_or_post_confinement_period`, `uc_is_adopter_in_first_year`, `uc_is_student_with_no_work_related_requirements`, `uc_is_responsible_foster_parent_of_child_under_one`, `uc_is_foster_parent_or_new_friend_or_family_carer` and `uc_is_ineligible_partner`; and the parameters under `gov.dwp.universal_credit.work_requirements` for the responsible carer's child ages and expected hours and the 16-hour threshold, with `gov.dwp.universal_credit.means_test.minimum_income_floor.no_requirements_partner_hours` and `gov.dwp.universal_credit.means_test.minimum_income_floor.ineligible_partner_hours` (the 35 hours of reg. 90(3)(b)(ii)). Without a recorded nomination, `uc_is_responsible_carer` takes the joint claimant who works fewer hours (`hours_worked`), then the elder; `hours_worked` defaults to 0, so set `uc_is_responsible_carer` (or each member's hours) for a couple with a child entered without hours. diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/ineligible_partner_hours.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/ineligible_partner_hours.yaml new file mode 100644 index 0000000000..141e9dcf55 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/minimum_income_floor/ineligible_partner_hours.yaml @@ -0,0 +1,20 @@ +description: >- + Weekly hours behind the amount that a partner who cannot be a joint claimant + adds to the couple threshold for the Universal Credit minimum income floor, + where a member of a couple claims as a single person (regulation 3(3)). + Regulation 90(3)(b)(ii) adds "the amount a person would be paid for 35 hours + per week" at the national minimum wage rate of regulation 4 of the National + Minimum Wage Regulations (regulation 11 of the 1999 Regulations as made). + The 35 is set by regulation 90 itself, separately from the expected hours of + regulation 88. +values: + 2013-04-29: 35 +metadata: + unit: hour + period: week + label: Universal Credit minimum income floor hours for a partner who cannot be a joint claimant + reference: + - title: The Universal Credit Regulations 2013 reg. 90(3)(b)(ii) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/90 + - title: The Universal Credit Regulations 2013 reg. 90(3)(b)(ii), as made + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/90/2013-04-29 diff --git a/policyengine_uk/tests/test_uc_work_related_groups_properties.py b/policyengine_uk/tests/test_uc_work_related_groups_properties.py index cd1225e5b5..1f02ace64e 100644 --- a/policyengine_uk/tests/test_uc_work_related_groups_properties.py +++ b/policyengine_uk/tests/test_uc_work_related_groups_properties.py @@ -422,10 +422,11 @@ def test_partner_who_cannot_be_a_joint_claimant(units, year): atol=0.01, ) # Reg. 90(3)(b)(ii): 35 hours at the national living wage, whatever the - # partner's age. + # partner's age. The 35 is the regulation's own figure, not reg. 88's + # expected hours. np.testing.assert_allclose( v["uc_minimum_income_floor_gross"][ineligible], - v["parameters"].default_expected_hours * 52 * v["national_living_wage"], + 35 * 52 * v["national_living_wage"], atol=0.01, err_msg=str(units), ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py index 0e283f787e..896b58f394 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/income_floor/uc_minimum_income_floor_gross.py @@ -67,7 +67,7 @@ def formula(person, period, parameters): group == groups.NO_REQUIREMENTS, ], [ - work.default_expected_hours, + floor.ineligible_partner_hours, person("uc_expected_hours", period), work.interview_or_preparation_threshold_hours, floor.no_requirements_partner_hours, From 947fc3a23e8668f56591b56a41d4649c6c14f536 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 5 Oct 2026 16:37:26 -0400 Subject: [PATCH 9/9] Cite the reg 43 saving for the higher child element; date the notebook's hours - uc_mif_earned_income.yaml: the higher first-child element survives from 6 April 2017 only through the Universal Credit (Transitional Provisions) Regulations 2014 (SI 2014/1230) reg. 43, which needs one child born before that date. Comment only. - universal-credit.ipynb: 25 hours for any child under 13 before April 2017, and the adult rate before the national living wage (April 2016). Review: delta review of 671da0f7e, nits 1 and 2. Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/universal-credit.ipynb | 2 +- .../income/income_floor/uc_mif_earned_income.yaml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index eea4de43aa..7f2e0d6738 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -44,7 +44,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: only the claimants' income counts: the single claimant, or the two joint claimants (`is_uc_assessed_claimant`; UC Regulations 2013 reg 22(1)). The earnings and unearned income of a child, a qualifying young person or anyone else in the benefit unit are theirs, not the claimant's, so the Universal Credit means test does not count them. Who the claimants are comes from `is_uc_claimant` (the claimant and partner); a member flagged there is taken as a claimant whatever their age. Each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): for example one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where the nomination is not supplied the model takes the claimant who works fewer hours (`hours_worked`), then the elder. `hours_worked` defaults to 0, so for a couple entered without hours the default falls to the elder; supply `uc_is_responsible_carer` where the nomination is known. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or for the responsible carer of a child under 13 the hours DWP uses: 30 from February 2025, and from April 2017 until then 16 for a child under compulsory school age and 25 for an older child; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, nothing for a partner with no work-related requirements for a reason other than earnings, and 35 hours at the national living wage for a partner who cannot be a joint claimant (reg 90(3)(b)). The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the claimants' combined earnings above the work allowance (`uc_work_allowance`), and by all of the claimants' unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (the claimants earning enough, having a qualifying disability benefit, etc.). The earnings exception reads the claimants' earnings only (reg 82(1)(a)), the capped total counts only the claimants' own contributory benefits (WRA 2012 s.96(1)), and the childcare work condition tests only the claimants (reg 32(1)).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." + "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: only the claimants' income counts: the single claimant, or the two joint claimants (`is_uc_assessed_claimant`; UC Regulations 2013 reg 22(1)). The earnings and unearned income of a child, a qualifying young person or anyone else in the benefit unit are theirs, not the claimant's, so the Universal Credit means test does not count them. Who the claimants are comes from `is_uc_claimant` (the claimant and partner); a member flagged there is taken as a claimant whatever their age. Each person's earnings (`uc_individual_earned_income`) are net of their own relievable pension contributions and the income tax and National Insurance they pay on that employment or self-employment (UC Regulations 2013 regs 55(5) and 57(2)): `uc_income_tax_on_earnings` treats earnings as the lowest slice of the person's non-savings income, so tax on pensions, property, savings and dividends is never deducted, and one partner's tax never reduces the other's earnings. A claimant in gainful self-employment outside a start-up period who would otherwise be subject to all work-related requirements is subject to the minimum income floor (reg 62): when their earnings after those deductions are below `uc_minimum_income_floor`, they are treated as having that amount instead. The floor does not apply to a claimant in the no work-related requirements, work-focused interview or work preparation group (`uc_work_related_group_apart_from_earnings`; Welfare Reform Act 2012 ss 19 to 21): for example one with limited capability for work-related activity, a carer, a claimant over State Pension Credit qualifying age, or the responsible carer of a child under 3. Only one member of a couple is the responsible carer (`uc_is_responsible_carer`); where the nomination is not supplied the model takes the claimant who works fewer hours (`hours_worked`), then the elder. `hours_worked` defaults to 0, so for a couple entered without hours the default falls to the elder; supply `uc_is_responsible_carer` where the nomination is known. The floor is the minimum wage for their age for their expected hours (`uc_expected_hours`: 35 a week, or for the responsible carer of a child under 13 the hours DWP uses: 30 from February 2025, from April 2017 until then 16 for a child under compulsory school age and 25 for an older child, and 25 for any child under 13 before April 2017; regs 88 and 90(2)), less the income tax and the Class 2 and Class 4 National Insurance a self-employed person would pay on it (reg 62(4)(b) leaves this amount to the Secretary of State; this is the basis of DWP's guidance and figures, and setting the `gov.dwp.universal_credit.means_test.minimum_income_floor.self_employed_national_insurance` parameter to false deducts primary Class 1 instead). A member of a couple is lifted only while the couple's combined earnings are below the couple threshold, and never above it (reg 62(3)). The couple threshold is the sum of both partners' thresholds (reg 90(3)): 16 hours for a partner in the work-focused interview or work preparation group, nothing for a partner with no work-related requirements for a reason other than earnings, and 35 hours at the national living wage (the adult rate before April 2016) for a partner who cannot be a joint claimant (reg 90(3)(b)). The maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the claimants' combined earnings above the work allowance (`uc_work_allowance`), and by all of the claimants' unearned income (`uc_unearned_income`), pound for pound. Unearned income is the list in `gov.dwp.universal_credit.means_test.income_definitions.unearned`: retirement pension income (State Pension and private pensions, UC Regulations 2013 regs 66(1)(a) and 67), Carer's Allowance, carer support payment (up to the Carer's Allowance rate, from November 2023), contribution-based JSA and ESA, maternity allowance, industrial injuries benefit, and the tariff income deemed from capital (reg 72). Actual savings interest, dividends and ordinary rental income are excluded from UC unearned income at every capital level; capital counts through its tariff income instead. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (the claimants earning enough, having a qualifying disability benefit, etc.). The earnings exception reads the claimants' earnings only (reg 82(1)(a)), the capped total counts only the claimants' own contributory benefits (WRA 2012 s.96(1)), and the childcare work condition tests only the claimants (reg 32(1)).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." }, { "cell_type": "code", diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml index 79aa8f111c..a7c6abd945 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/income_floor/uc_mif_earned_income.yaml @@ -274,9 +274,9 @@ uc_individual_earned_income: [19_252.46, 21_519.60, 0, 0] # Less the work allowance of 427 x 12 = 5,124. uc_earned_income: 35_648.06 - # Both children were born before 6 April 2017, so the first has the - # higher child element (reg. 36): 666.97 x 12 + (351.88 + 303.94) x 12 + - # 12,000. + # A child was born before 6 April 2017, so the first child keeps the + # higher child element (reg. 36, saved by SI 2014/1230 reg. 43): 666.97 x + # 12 + (351.88 + 303.94) x 12 + 12,000. uc_maximum_amount: 27_873.48 # 27,873.48 - 0.55 x 35,648.06. universal_credit: 8_267.05