diff --git a/changelog.d/uc-claimant-only-income.added.md b/changelog.d/uc-claimant-only-income.added.md new file mode 100644 index 000000000..742561aad --- /dev/null +++ b/changelog.d/uc-claimant-only-income.added.md @@ -0,0 +1 @@ +Add `is_uc_assessed_claimant`, the single claimant or the two joint claimants whose income a Universal Credit award assesses (`is_uc_claimant`, limited to the two eldest), and the `add_for_members` helper that sums person-level variables over chosen benefit-unit members. diff --git a/changelog.d/uc-claimant-only-income.fixed.md b/changelog.d/uc-claimant-only-income.fixed.md new file mode 100644 index 000000000..7549ebae5 --- /dev/null +++ b/changelog.d/uc-claimant-only-income.fixed.md @@ -0,0 +1 @@ +Count only the claimants' income in Universal Credit (UC Regs 2013 reg. 22(1)): `uc_earned_income` and `uc_unearned_income` summed every member of the benefit unit, so a dependent child's or qualifying young person's wages, profits, savings interest, dividends, rent or pension reduced the parents' award. The benefit cap earnings exception (reg. 82(1)(a)) and the childcare work condition (reg. 32(1)) now read the claimants' earnings and work only, so a dependant's job neither lifts the cap nor switches the childcare costs element on. diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index dd182e4bc..914707957 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 claimants' countable earnings above the work allowance (`uc_work_allowance`) at the published taper rate. Only 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, and never reduce the award. 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 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, 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 (the claimants earning enough, having a qualifying disability benefit, etc.). The earnings exception reads the claimants' earnings only (reg 82(1)(a)), as does the childcare work condition (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/model_api.py b/policyengine_uk/model_api.py index 367801d23..597de7c32 100644 --- a/policyengine_uk/model_api.py +++ b/policyengine_uk/model_api.py @@ -3,5 +3,6 @@ from policyengine_core import periods from microdf import MicroSeries, MicroDataFrame from policyengine_uk.utils.scenario import Scenario +from policyengine_uk.utils.benefit_unit import add_for_members GBP = "currency-GBP" diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml new file mode 100644 index 000000000..4abb22afd --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml @@ -0,0 +1,210 @@ +# A dependant's income is not the claimant's. Besides the income deducted +# from the award (reg. 22(1): income/uc_earned_income.yaml and +# income/uc_unearned_income.yaml), three other Universal Credit rules read +# members' income or work: the benefit cap's earnings exception (reg. +# 82(1)(a): "the claimant's earned income or, if the claimant is a member of a +# couple, the couple's combined earned income"), the benefit cap total (WRA +# 2012 s. 96(1): "the welfare benefits to which a single person or couple is +# entitled") and the childcare work condition (reg. 32(1): "the claimant is in +# paid work" and, in a couple, "the other member"). + +- name: A qualifying young person's earnings do not exempt the family from the benefit cap + # Reg. 80A(2)(d)(ii): £22,020 a year outside Greater London for a claimant + # responsible for a child or qualifying young person. Before this was + # fixed the young person's £12,000 met the earnings exception. + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + employment_income: 12_000 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + output: + is_benefit_cap_exempt_earnings: false + benefit_cap: 22_020 + +- name: The claimant's own earnings still meet the benefit cap earnings exception + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + employment_income: 12_000 + young_person: + age: 17 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + region: NORTH_EAST + output: + is_benefit_cap_exempt_earnings: true + +- name: Joint claimants' combined earnings meet the benefit cap earnings exception + # Neither partner's £6,000 reaches the threshold alone; together they do. + period: 2026 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 40 + is_parent: true + employment_income: 6_000 + parent_2: + age: 38 + is_parent: true + employment_income: 6_000 + child: + age: 5 + benunits: + benunit: + members: [parent_1, parent_2, child] + households: + household: + members: [parent_1, parent_2, child] + region: NORTH_EAST + output: + is_benefit_cap_exempt_earnings: true + +- name: A dependent 18-year-old's job makes no difference to the childcare work condition + # The working lone parent meets it whether or not the young person works. + # Before this was fixed an 18-year-old without a job blocked it, so their + # taking one switched the childcare costs element on. + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + employment_income: 15_000 + young_person: + age: 18 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, young_person, child] + output: + is_uc_claimant: [true, false, false] + uc_childcare_work_condition: true + +- name: A dependent 18-year-old's job does not meet the childcare work condition for a parent out of work + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 18 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + employment_income: 5_000 + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent, young_person, child] + output: + uc_childcare_work_condition: false + +- name: An adult child flagged as a third claimant neither meets nor blocks the childcare work condition + # Both members of the couple work, so reg. 32(1) is met whether or not + # their adult child does. + period: 2026 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 50 + employment_income: 15_000 + parent_2: + age: 48 + employment_income: 15_000 + adult_child: + age: 25 + child: + age: 3 + childcare_expenses: 5_000 + benunits: + benunit: + members: [parent_1, parent_2, adult_child, child] + output: + is_uc_assessed_claimant: [true, true, false, false] + uc_childcare_work_condition: true + +- name: A dependant's contributory benefit is not part of the benefit cap total + # The cap is £22,020 (reg. 80A(2)(d)(ii)). The parent's award before the + # cap is set to £30,000 and Child Benefit to nil, so the excess is 7,980. + # The young person's £5,000 is theirs, not the claimant's (WRA 2012 s. + # 96(1); reg. 80(1)): before this was fixed the excess was 12,980. + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + jsa_contrib: 5_000 + benunits: + benunit: + members: [parent, young_person] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 0 + households: + household: + members: [parent, young_person] + region: NORTH_EAST + output: + benefit_cap: 22_020 + benefit_cap_reduction: 30_000 - 22_020 + +- name: The claimant's own contributory benefit is part of the benefit cap total + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + jsa_contrib: 2_000 + young_person: + age: 17 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent, young_person] + universal_credit_pre_benefit_cap: 30_000 + child_benefit: 0 + households: + household: + members: [parent, young_person] + region: NORTH_EAST + output: + benefit_cap: 22_020 + benefit_cap_reduction: 30_000 + 2_000 - 22_020 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml index 378941b5d..258e035ad 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earned_income.yaml @@ -59,3 +59,116 @@ output: uc_individual_earned_income: [900, 450] uc_earned_income: 1_050 + +# Reg. 22(1)(b) deducts "the claimant's earned income (or, in the case of +# joint claimants, their combined earned income)". A child's or qualifying +# young person's earnings are theirs, not the claimant's. + +- name: A qualifying young person's earnings do not reduce a lone parent's Universal Credit + # Standard allowance £424.90 (single, 25 or over) and child element + # £351.88 (first child, born before 6 April 2017) a month: reg. 36 table, + # 2026-27. The parent has no earned income, so nothing is deducted. Before + # this was fixed the child's £12,000 less the £8,520 work allowance was + # tapered: uc_earned_income 3,480, UC £7,407.36. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 17 + current_education: UPPER_SECONDARY + employment_income: 12_000 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + # The child's own earnings are below the personal allowance and the + # primary threshold, so nothing is deducted from them. + uc_individual_earned_income: [0, 12_000] + uc_earned_income: 0 + universal_credit: (424.90 + 351.88) * 12 + +- name: Joint claimants' earnings combine and their qualifying young person's do not + # Higher work allowance £710 a month (responsible for a qualifying young + # person, no housing costs element): reg. 22(2)(a) table, 2026-27. The + # couple's earned income is 10,000 + 5,000 (below the personal allowance + # and primary threshold, so no deductions); less 8,520 is 6,480. Maximum + # amount (666.97 + 351.88) x 12 = 12,226.20, less 55% of 6,480 = 3,564. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent_1: + age: 40 + is_parent: true + employment_income: 10_000 + parent_2: + age: 38 + is_parent: true + employment_income: 5_000 + young_person: + age: 18 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + employment_income: 8_000 + benunits: + benunit: + members: [parent_1, parent_2, young_person] + households: + household: + members: [parent_1, parent_2, young_person] + output: + uc_individual_earned_income: [10_000, 5_000, 8_000] + uc_work_allowance: 710 * 12 + uc_earned_income: 10_000 + 5_000 - 710 * 12 + universal_credit: (666.97 + 351.88) * 12 - 0.55 * (15_000 - 710 * 12) + +- name: An adult child flagged as a third claimant does not add earned income + # Joint claimants with no child or qualifying young person and no limited + # capability for work have no work allowance (reg. 22(1)(b)(i)). + period: 2026 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 50 + employment_income: 10_000 + parent_2: + age: 48 + adult_child: + age: 25 + employment_income: 11_000 + benunits: + benunit: + members: [parent_1, parent_2, adult_child] + output: + is_uc_assessed_claimant: [true, true, false] + uc_work_allowance: 0 + uc_earned_income: 10_000 + +- name: A dependant's self-employed profits do not count and get no minimum income floor + # Before this was fixed the child's £12,000 (no tax or Class 4 below + # £12,570) less the £8,520 work allowance counted: 3,480. + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 17 + current_education: UPPER_SECONDARY + self_employment_income: 12_000 + benunits: + benunit: + members: [parent, child] + output: + uc_mif_applies: [false, false] + uc_earned_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml index b1db7dcd7..48c84bb30 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml @@ -251,3 +251,80 @@ uc_assessable_capital: 6_001 uc_tariff_income: 4.35 * 12 uc_unearned_income: 4.35 * 12 + 120 + +# Reg. 22(1)(a) deducts "all of the claimant's unearned income (or in the +# case of joint claimants all of their combined unearned income)". A +# dependant's income is not the claimant's. + +- name: A dependant's unearned income does not count and the claimant's does + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + private_pension_income: 1_000 + child: + age: 17 + current_education: UPPER_SECONDARY + savings_interest_income: 300 + dividend_income: 200 + property_income: 100 + private_pension_income: 400 + carers_allowance: 500 + benunits: + benunit: + members: [parent, child] + output: + uc_unearned_income: 1_000 + +- name: A dependant's savings interest does not wipe out a lone parent's Universal Credit + # Reg. 36 table, 2026-27: £424.90 + £351.88 a month. Before this was + # fixed the child's £12,000 counted pound for pound and UC was nil. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 17 + current_education: UPPER_SECONDARY + savings_interest_income: 12_000 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + uc_unearned_income: 0 + universal_credit: (424.90 + 351.88) * 12 + +- name: With tariff income, a dependant's savings interest is neither added nor taken off + # The household's £6,001 is £1 over the £6,000 lower limit: one £4.35 + # monthly step (reg. 72). The claimant's £120 of interest is replaced by + # the tariff income; the child's £500 never counted. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + is_parent: true + savings_interest_income: 120 + child: + age: 10 + savings_interest_income: 500 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + savings: 6_001 + output: + uc_tariff_income: 4.35 * 12 + uc_unearned_income: 4.35 * 12 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_assessed_claimant.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_assessed_claimant.yaml new file mode 100644 index 000000000..d10f832d6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_assessed_claimant.yaml @@ -0,0 +1,106 @@ +# A Universal Credit claim is made by a single person or jointly by the two +# members of a couple (Welfare Reform Act 2012 s. 2(1)); "claimant" means a +# single claimant or each of joint claimants (s. 40). Only claimants' income +# counts (UC Regs 2013 reg. 22(1)). + +- name: A lone parent is the claimant and their child is not + period: 2026 + absolute_error_margin: 0 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 17 + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [parent, child] + output: + is_uc_claimant: [true, false] + is_uc_assessed_claimant: [true, false] + +- name: Both members of a couple are claimants and their qualifying young person is not + period: 2026 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 40 + is_parent: true + parent_2: + age: 38 + is_parent: true + young_person: + age: 18 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + benunits: + benunit: + members: [parent_1, parent_2, young_person] + output: + is_child_or_qualifying_young_person_for_universal_credit: [false, false, true] + is_uc_claimant: [true, true, false] + is_uc_assessed_claimant: [true, true, false] + +- name: An adult child flagged as a claimant is not a third claimant + period: 2026 + absolute_error_margin: 0 + input: + people: + parent_1: + age: 50 + parent_2: + age: 48 + adult_child: + age: 25 + benunits: + benunit: + members: [parent_1, parent_2, adult_child] + output: + is_uc_claimant: [true, true, true] + is_uc_assessed_claimant: [true, true, false] + +- name: Of more than two members flagged as claimants, the two eldest are the claimants whatever their order + period: 2026 + absolute_error_margin: 0 + input: + people: + youngest: + age: 30 + is_uc_claimant: true + eldest: + age: 60 + is_uc_claimant: true + middle: + age: 45 + is_uc_claimant: true + benunits: + benunit: + members: [youngest, eldest, middle] + output: + is_uc_assessed_claimant: [false, true, true] + +- name: Flagged claimants of the same age rank in the order listed + # The Act fixes the number of claimants, not which of three flagged + # members they are; taking the eldest two, and the first listed on a tie, + # is a modelling rule. + period: 2026 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_uc_claimant: true + second: + age: 40 + is_uc_claimant: true + third: + age: 40 + is_uc_claimant: true + benunits: + benunit: + members: [first, second, third] + output: + is_uc_assessed_claimant: [true, true, false] diff --git a/policyengine_uk/tests/test_uc_claimant_income_properties.py b/policyengine_uk/tests/test_uc_claimant_income_properties.py new file mode 100644 index 000000000..e97635fcd --- /dev/null +++ b/policyengine_uk/tests/test_uc_claimant_income_properties.py @@ -0,0 +1,450 @@ +"""Property-based tests: Universal Credit counts only claimants' income. + +UC Regs 2013 reg. 22(1) deducts "all of the claimant's unearned income (or in +the case of joint claimants all of their combined unearned income)" and "the +claimant's earned income (or, in the case of joint claimants, their combined +earned income)"; WRA 2012 s. 8(4) says the same of the Act's deductions. A +claim is made by a single person or jointly by a couple (s. 2(1)), so it has +at most two claimants. A child's or qualifying young person's income, and the +income of anyone else in the benefit unit, is not the claimant's. The benefit +cap's earnings exception (reg. 82(1)(a)), the benefit cap total (WRA 2012 s. +96(1)) and the childcare work condition (reg. 32(1)) are framed the same way. + +Invariants, for any generated population of single claimants, couples and +mixed-age couples with up to four dependants aged 0 to 19 (in or out of +education), with or without rent, childcare costs and capital, in England, +Wales and Scotland. A third of the families are built to be over the benefit +cap, and a third to have working claimants paying for childcare: + +1. A dependant's income never changes Universal Credit. Giving every + dependant earnings, self-employment profits or losses, miscellaneous + income, savings interest, dividends, property income, a private pension or + contributory benefits leaves UC, UC before the benefit cap, the cap + reduction, earned income, unearned income and the childcare costs element + exactly as they are when the dependants have no income. + A separate property builds only families over the cap, each with a child + earning more than the earnings exception's threshold, and checks that the + cap still applies in full. +2. The assessed claimants are the claimants: in every generated family the + model assesses the claimant and partner and nobody else. +3. Differential against the regulations, for 2026-27: with earnings below the + personal allowance and primary threshold, earned income equals the + claimants' combined earnings less the reg. 22 work allowance (£710 a + month, or £427 with the housing costs element, where they are responsible + for a child or qualifying young person), and unearned income equals the + claimants' private pensions plus either their savings interest or, with + capital over £6,000, the reg. 72 tariff income of £4.35 a month for each + £250 or part. The amounts are written here from the regulations, not read + from the model. +4. There are at most two assessed claimants however many members are flagged + as claimants, they are flagged claimants, and they are every flagged + member when two or fewer are flagged. +5. Order does not matter: entering the people of every family in reverse + leaves the assessed claimants and UC unchanged. (Members of the same age + rank in the order listed, so with more than two flagged claimants and a + tie in age the order would decide; the generated families flag at most + two.) + +Marriage Allowance is switched off throughout, as in the other Universal +Credit property tests. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +YEARS = [2020, 2026, 2027] +TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +EDUCATION = ["UPPER_SECONDARY", "TERTIARY", "NOT_IN_EDUCATION"] +WORKING_AGE = st.integers(20, 64) +PENSION_AGE = st.integers(67, 80) +SHAPES = { + "single": [WORKING_AGE], + "couple": [WORKING_AGE, WORKING_AGE], + "mixed_age": [PENSION_AGE, WORKING_AGE], +} +amount = st.one_of(st.just(0.0), st.floats(0, 30_000)) +# What a dependant may receive. The last two are contributory benefits, which +# count towards the benefit cap total when they are the claimant's. +# Contributory ESA is left out: any member's ESA exempts the benefit unit from +# the cap in the model, which is a question of whose circumstances count, not +# whose income. +DEPENDANT_INCOME = { + "employment_income": amount, + "self_employment_income": st.one_of(st.just(0.0), st.floats(-5_000, 30_000)), + "miscellaneous_income": amount, + "savings_interest_income": amount, + "dividend_income": amount, + "property_income": amount, + "private_pension_income": amount, + "jsa_contrib": st.one_of(st.just(0.0), st.floats(0, 6_000)), + "incapacity_benefit": st.one_of(st.just(0.0), st.floats(0, 8_000)), +} +BENUNIT_VARIABLES = [ + "universal_credit", + "universal_credit_pre_benefit_cap", + "benefit_cap_reduction", + "uc_earned_income", + "uc_unearned_income", + "uc_childcare_element", +] + + +@st.composite +def dependants(draw, minimum=0, maximum=4, childcare=False): + members = [] + for _ in range(draw(st.integers(minimum, maximum))): + age = draw(st.integers(0, 19)) + member = dict(age=age, **{v: draw(s) for v, s in DEPENDANT_INCOME.items()}) + if age >= 16: + # At 18 and 19 only a qualifying young person is a dependant: + # in non-advanced education begun before 19 and, at 19, before + # the 1 September after their birthday (reg. 5(1)). Anyone else + # that age is an adult whom the calculator treats as a claimant. + member["current_education"] = ( + draw(st.sampled_from(EDUCATION)) if age < 18 else "UPPER_SECONDARY" + ) + member["age_started_or_accepted_current_education_or_training"] = draw( + st.integers(16, min(age, 18)) + ) + if age == 19: + member[ + "is_before_universal_credit_qualifying_young_person_terminal_date" + ] = True + members.append(member) + if childcare: + # A young child in paid childcare, and an 18-year-old at school whose + # job must neither meet nor block the parents' work condition. + members.append(dict(age=3, childcare_expenses=draw(st.floats(1_000, 9_000)))) + members.append( + dict( + age=18, + current_education="UPPER_SECONDARY", + age_started_or_accepted_current_education_or_training=17, + **{v: draw(s) for v, s in DEPENDANT_INCOME.items()}, + ) + ) + return members + + +@st.composite +def families(draw): + kind = draw(st.sampled_from(["general", "capped", "childcare"])) + shape = draw(st.sampled_from(list(SHAPES))) + if kind == "capped": + # Claimants without earnings, several children and a high council + # rent: over the cap unless something exempts them. + claimants = [dict(age=draw(age)) for age in SHAPES[shape]] + return dict( + claimants=claimants, + dependants=draw(dependants(minimum=3)), + tenure="RENT_FROM_COUNCIL", + rent=draw(st.floats(12_000, 30_000)), + region=draw(st.sampled_from(REGIONS)), + savings=0.0, + ) + if kind == "childcare": + # Every claimant works, so the work condition is the claimants' to + # meet. + claimants = [ + dict(age=draw(age), employment_income=draw(st.floats(3_000, 30_000))) + for age in SHAPES[shape] + ] + return dict( + claimants=claimants, + dependants=draw(dependants(maximum=1, childcare=True)), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.one_of(st.just(0.0), st.floats(0, 15_000))), + region=draw(st.sampled_from(REGIONS)), + savings=0.0, + ) + claimants = [ + dict( + age=draw(age), + employment_income=draw(amount), + savings_interest_income=draw(amount), + private_pension_income=draw(amount), + ) + for age in SHAPES[shape] + ] + return dict( + claimants=claimants, + dependants=draw(dependants()), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + region=draw(st.sampled_from(REGIONS)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + ) + + +populations = st.lists(families(), min_size=1, max_size=6) + + +def situation(units, year, zero_dependants=False, reverse=False, flag_all=False): + """One simulation holding every family. + + With ``zero_dependants`` every dependant's income is nil. With + ``reverse`` the people of each family are entered, and listed in their + benefit unit and household, in reverse order. With ``flag_all`` every + member is flagged as a claimant, as data or users may. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + members = [] + for j, claimant in enumerate(unit["claimants"]): + person = {k: {year: v} for k, v in claimant.items()} + person["is_parent"] = {year: bool(unit["dependants"])} + members.append((f"p{i}_{j}", person)) + for k, dependant in enumerate(unit["dependants"]): + person = { + key: {year: 0.0 if zero_dependants and key in DEPENDANT_INCOME else v} + for key, v in dependant.items() + } + members.append((f"d{i}_{k}", person)) + if reverse: + members = members[::-1] + names = [] + for name, person in members: + person["would_claim_marriage_allowance"] = {year: False} + if flag_all: + person["is_uc_claimant"] = {year: True} + people[name] = person + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "region": {year: unit["region"]}, + "savings": {year: unit["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def benunit_values(sim, year): + return {v: np.asarray(sim.calculate(v, year)) for v in BENUNIT_VARIABLES} + + +def built_claimants(units): + """Person flags, in order: the claimants the test built, then dependants.""" + flags = [] + for unit in units: + flags += [True] * len(unit["claimants"]) + [False] * len(unit["dependants"]) + return np.array(flags) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_dependants_income_never_changes_universal_credit(units, year): + with_income = Simulation(situation=situation(units, year)) + without_income = Simulation(situation=situation(units, year, zero_dependants=True)) + a = benunit_values(with_income, year) + b = benunit_values(without_income, year) + for v in BENUNIT_VARIABLES: + np.testing.assert_allclose(a[v], b[v], atol=0.01, err_msg=f"{v}: {units}") + + +@st.composite +def capped_families(draw): + """Working-age claimants with no income, four or five children and a high + council rent, so the benefit cap always bites; at least one child has + earnings well over the earnings exception's threshold.""" + claimants = [ + dict(age=draw(st.integers(25, 60))) for _ in range(draw(st.integers(1, 2))) + ] + children = [ + dict(age=draw(st.integers(0, 15)), employment_income=draw(amount)) + for _ in range(draw(st.integers(3, 4))) + ] + children.append( + dict( + age=draw(st.integers(0, 15)), + employment_income=draw(st.floats(15_000, 30_000)), + ) + ) + return dict( + claimants=claimants, + dependants=children, + tenure="RENT_FROM_COUNCIL", + rent=draw(st.floats(15_000, 30_000)), + region=draw(st.sampled_from(REGIONS)), + savings=0.0, + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(capped_families(), min_size=1, max_size=6), + year=st.sampled_from(YEARS), +) +def test_dependants_earnings_do_not_lift_the_benefit_cap(units, year): + with_income = Simulation(situation=situation(units, year)) + without_income = Simulation(situation=situation(units, year, zero_dependants=True)) + capped = np.asarray(without_income.calculate("benefit_cap_reduction", year)) + # Every generated family is over the cap, so the property is not vacuous. + assert np.all(capped > 0), units + for v in ["benefit_cap_reduction", "universal_credit"]: + np.testing.assert_allclose( + np.asarray(with_income.calculate(v, year)), + np.asarray(without_income.calculate(v, year)), + atol=0.01, + err_msg=f"{v}: {units}", + ) + assert not np.any( + np.asarray(with_income.calculate("is_benefit_cap_exempt_earnings", year)) + ), units + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_assessed_claimants_are_the_claimant_and_partner(units, year): + sim = Simulation(situation=situation(units, year)) + np.testing.assert_array_equal( + np.asarray(sim.calculate("is_uc_assessed_claimant", year)), + built_claimants(units), + err_msg=str(units), + ) + + +# Reg. 22(2) table and reg. 72(1), 2026-27, a month. +HIGHER_WORK_ALLOWANCE = 710 +LOWER_WORK_ALLOWANCE = 427 +TARIFF_INCOME_PER_STEP = 4.35 +TARIFF_STEP = 250 +TARIFF_LOWER_LIMIT = 6_000 +# Below the 2026-27 personal allowance and primary threshold (£12,570), so +# nothing is deducted from the claimant's earnings (reg. 55(5)). +untaxed_earnings = st.one_of(st.just(0.0), st.floats(0, 12_000)) + + +@st.composite +def untaxed_families(draw): + """Working-age claimants whose earnings carry no tax or NI, with capital + under the £16,000 limit (reg. 18).""" + claimants = [ + dict( + age=draw(st.integers(25, 60)), + employment_income=draw(untaxed_earnings), + savings_interest_income=draw(st.one_of(st.just(0.0), st.floats(0, 500))), + private_pension_income=draw(st.one_of(st.just(0.0), st.floats(0, 500))), + ) + for _ in range(draw(st.integers(1, 2))) + ] + return dict( + claimants=claimants, + dependants=draw(dependants()), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.one_of(st.just(0.0), st.floats(1_000, 12_000))), + region=draw(st.sampled_from(REGIONS)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 15_900))), + ) + + +@PROPERTY_SETTINGS +@given(units=st.lists(untaxed_families(), min_size=1, max_size=6)) +def test_income_matches_regulations_22_and_72(units): + year = 2026 + sim = Simulation(situation=situation(units, year)) + earned = np.asarray(sim.calculate("uc_earned_income", year)) + unearned = np.asarray(sim.calculate("uc_unearned_income", year)) + # The model says only whether the award has a housing costs element and + # whether a dependant is a child or qualifying young person. + housing = np.asarray(sim.calculate("uc_housing_costs_element", year)) > 0 + qualifying = np.asarray( + sim.calculate("is_child_or_qualifying_young_person_for_universal_credit", year) + ) + person = 0 + for i, unit in enumerate(units): + n_claimants, n_dependants = len(unit["claimants"]), len(unit["dependants"]) + responsible = qualifying[ + person + n_claimants : person + n_claimants + n_dependants + ].any() + person += n_claimants + n_dependants + work_allowance = 0 + if responsible: + monthly = LOWER_WORK_ALLOWANCE if housing[i] else HIGHER_WORK_ALLOWANCE + work_allowance = monthly * 12 + earnings = sum(c["employment_income"] for c in unit["claimants"]) + expected_earned = max(0, earnings - work_allowance) + pensions = sum(c["private_pension_income"] for c in unit["claimants"]) + interest = sum(c["savings_interest_income"] for c in unit["claimants"]) + excess = unit["savings"] - TARIFF_LOWER_LIMIT + if excess > 0: + # Tariff income replaces the capital's actual yield (reg. 72(3)). + steps = np.ceil(excess / TARIFF_STEP) + expected_unearned = pensions + steps * TARIFF_INCOME_PER_STEP * 12 + else: + expected_unearned = pensions + interest + np.testing.assert_allclose( + earned[i], expected_earned, atol=0.5, err_msg=str(unit) + ) + np.testing.assert_allclose( + unearned[i], expected_unearned, atol=0.5, err_msg=str(unit) + ) + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS), flag_all=st.booleans()) +def test_at_most_two_assessed_claimants_drawn_from_flagged_claimants( + units, year, flag_all +): + sim = Simulation(situation=situation(units, year, flag_all=flag_all)) + claimant = np.asarray(sim.calculate("is_uc_claimant", year)) + assessed = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) + flagged = np.asarray(sim.map_result(claimant.astype(float), "person", "benunit")) + counted = np.asarray(sim.map_result(assessed.astype(float), "person", "benunit")) + assert np.all(counted <= 2), units + assert np.all(claimant[assessed]), units + np.testing.assert_array_equal(counted, np.minimum(flagged, 2), err_msg=str(units)) + + +def test_three_flagged_claimants_leave_two_assessed(): + # A deterministic case of the property above: a couple and their child, + # all flagged, in each of three years. + units = [ + dict( + claimants=[dict(age=40), dict(age=38)], + dependants=[dict(age=12)], + tenure="OWNED_OUTRIGHT", + rent=0.0, + region="NORTH_EAST", + savings=0.0, + ) + ] + for year in YEARS: + sim = Simulation(situation=situation(units, year, flag_all=True)) + assert list(sim.calculate("is_uc_claimant", year)) == [True, True, True] + assert list(sim.calculate("is_uc_assessed_claimant", year)) == [ + True, + True, + False, + ] + + +@PROPERTY_SETTINGS +@given(units=populations, year=st.sampled_from(YEARS)) +def test_member_order_does_not_matter(units, year): + forward_data = situation(units, year) + backward_data = situation(units, year, reverse=True) + forward = Simulation(situation=forward_data) + backward = Simulation(situation=backward_data) + a = benunit_values(forward, year) + b = benunit_values(backward, year) + for v in BENUNIT_VARIABLES: + np.testing.assert_allclose(a[v], b[v], atol=0.01, err_msg=f"{v}: {units}") + + # The assessed claimants are the same people, by name. + def assessed(sim, data): + flags = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) + return {name for name, flag in zip(data["people"], flags) if flag} + + assert assessed(forward, forward_data) == assessed(backward, backward_data), units diff --git a/policyengine_uk/utils/benefit_unit.py b/policyengine_uk/utils/benefit_unit.py new file mode 100644 index 000000000..853726105 --- /dev/null +++ b/policyengine_uk/utils/benefit_unit.py @@ -0,0 +1,21 @@ +"""Benefit-unit aggregation helpers for means tests.""" + + +def add_for_members(benunit, period, variables, members): + """Sum variables over the benefit-unit members a means test counts. + + ``members`` is a person-level boolean array. Person-level variables are + summed over those members only; benefit-unit variables are added as they + are. Means tests pass the claimant and partner plus the programme's own + children or young persons, so a member who is none of these (for example + an 18-year-old who is neither a partner nor a qualifying young person) + does not count. + """ + person = benunit.members + total = 0 + for name in variables: + if person.entity.get_variable(name).entity.is_person: + total = total + benunit.sum(person(name, period) * members) + else: + total = total + benunit(name, period) + return total diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py index bb0fddc2a..7d021f0b6 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py @@ -22,7 +22,9 @@ def formula(benunit, period, parameters): "esa_contrib", "sda", ] - return max_( - add(benunit, period, CAPPED_BENEFITS) - benunit("benefit_cap", period), - 0, - ) + # The cap applies to "the welfare benefits to which a single person or + # couple is entitled" (WRA 2012 s. 96(1); UC Regs 2013 reg. 80(1)), so + # a dependant's own contributory benefit is not part of the total. + claimants = benunit.members("is_uc_assessed_claimant", period) + total = add_for_members(benunit, period, CAPPED_BENEFITS, claimants) + return max_(total - benunit("benefit_cap", period), 0) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py index c27e13c39..715fa26ae 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py @@ -23,11 +23,18 @@ def formula(benunit, period, parameters): # Earnings exemption for UC (£846/month = £10,152/year) # Note: Only check earned income, not UC amount itself to avoid circular dependency + # UC Regs 2013 reg. 82(1)(a) tests "the claimant's earned income or, + # if the claimant is a member of a couple, the couple's combined + # earned income": a dependant's earnings do not count. + claimant = person("is_uc_assessed_claimant", period) uc_earned = benunit.sum( - benunit.members("employment_income", period) - + benunit.members("self_employment_income", period) - - benunit.members("income_tax", period) - - benunit.members("national_insurance", period) + ( + person("employment_income", period) + + person("self_employment_income", period) + - person("income_tax", period) + - person("national_insurance", period) + ) + * claimant ) earnings_threshold = 10_152 meets_earnings_test = uc_earned >= earnings_threshold diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py index b1a0765b1..d54594976 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/childcare_element/uc_childcare_work_condition.py @@ -5,14 +5,19 @@ class uc_childcare_work_condition(Variable): value_type = bool entity = BenUnit label = "Meets Universal Credit childcare work condition" + documentation = ( + "Tests work for the claimant and partner, excluding dependants. " + "The in_work proxy means positive hours or earnings; offers of work, " + "the partner exceptions in regulation 32(1)(b), and the treated-as-working " + "rules in regulation 32(2) are not modelled." + ) definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/32/2020-04-06" + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/32" def formula(benunit, period, parameters): + # Reg. 32(1): "the claimant is in paid work" and, in a couple, "the + # other member". Nobody else's work counts or is required. person = benunit.members - adult = person("is_adult", period) + claimant = person("is_uc_assessed_claimant", period) in_work = person("in_work", period) - adults_in_work = adult & in_work - # Benefit unit must not have any adults not in work. - all_adults_in_work = benunit.all(in_work | ~adult) - return benunit.any(adults_in_work) & all_adults_in_work + return benunit.any(claimant & in_work) & benunit.all(in_work | ~claimant) 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 d4ddb8a5c..ee94ce504 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 @@ -16,7 +16,7 @@ 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) + claimant = person("is_uc_assessed_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 diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py index 1880148d3..9a69a049a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income.py @@ -5,17 +5,34 @@ class uc_earned_income(Variable): value_type = float entity = BenUnit label = "Universal Credit earned income (after deductions and work allowance)" + documentation = ( + "The claimant's earned income, or joint claimants' combined earned " + "income, less the work allowance. Earnings of a child, a qualifying " + "young person or anyone else in the benefit unit who is not a " + "claimant do not count." + ) definition_period = YEAR unit = GBP - reference = dict( - title="Universal Credit Regulations 2013 reg. 22(1)(b)", - href="https://www.legislation.gov.uk/uksi/2013/376/regulation/22", - ) + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 22(1)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/22", + ), + dict( + title="Welfare Reform Act 2012 s. 8(3) and (4)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/8", + ), + ] def formula(benunit, period, parameters): - # Each person's earned income is net of their own deductions - # (reg. 55(5), reg. 57(2)); the work allowance then comes off the - # combined earned income before the taper (reg. 22(1)(b)). - earned_income = add(benunit, period, ["uc_individual_earned_income"]) + # Reg. 22(1)(b) deducts "the claimant's earned income (or, in the case + # of joint claimants, their combined earned income)". Each claimant's + # earned income is net of their own deductions (reg. 55(5), reg. + # 57(2)); the work allowance then comes off the combined earned + # income before the taper. + claimants = benunit.members("is_uc_assessed_claimant", period) + earned_income = add_for_members( + benunit, period, ["uc_individual_earned_income"], claimants + ) work_allowance = benunit("uc_work_allowance", period) return max_(0, earned_income - work_allowance) 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 ba90cccf7..4157fc2e1 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 @@ -42,13 +42,8 @@ def formula(person, period, parameters): # 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 - ) + # at most two claimants (is_uc_assessed_claimant). + claimant = person("is_uc_assessed_claimant", period) claimant_earned_income = earned_income * claimant partner_earned_income = ( person.benunit.sum(claimant_earned_income) - claimant_earned_income diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py index 8dc07ee66..99011293b 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py @@ -5,13 +5,39 @@ class uc_unearned_income(Variable): value_type = float entity = BenUnit label = "Universal Credit unearned income" + documentation = ( + "The claimant's unearned income, or joint claimants' combined " + "unearned income. Income of a child, a qualifying young person or " + "anyone else in the benefit unit who is not a claimant does not count." + ) definition_period = YEAR unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 22(1)(a)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/22", + ), + dict( + title="Welfare Reform Act 2012 s. 8(3) and (4)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/8", + ), + dict( + title="Universal Credit Regulations 2013 reg. 66(1)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/66", + ), + ] def formula(benunit, period, parameters): + # Reg. 22(1)(a) deducts "all of the claimant's unearned income (or in + # the case of joint claimants all of their combined unearned income)". + # Person-level sources count for claimants only; benefit-unit sources + # (tariff income from capital) are added as they are. p = parameters(period).gov.dwp.universal_credit.means_test + claimants = benunit.members("is_uc_assessed_claimant", period) household = benunit.household - total = add(benunit, period, p.income_definitions.unearned) + total = add_for_members( + benunit, period, p.income_definitions.unearned, claimants + ) tariff_income_applies = benunit("uc_tariff_income", period) > 0 reported_capital = benunit("uc_reported_capital", period) has_reported_capital = reported_capital >= 0 @@ -20,10 +46,10 @@ def formula(benunit, period, parameters): ) + household("non_residential_property_value", period) capital_derived_income = ( ((household("savings", period) > 0) | has_reported_capital) - * benunit("savings_interest_income", period) + * add_for_members(benunit, period, ["savings_interest_income"], claimants) + ((household("corporate_wealth", period) > 0) | has_reported_capital) - * benunit("dividend_income", period) + * add_for_members(benunit, period, ["dividend_income"], claimants) + ((property_capital > 0) | has_reported_capital) - * benunit("property_income", period) + * add_for_members(benunit, period, ["property_income"], claimants) ) return total - tariff_income_applies * capital_derived_income diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_assessed_claimant.py b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_assessed_claimant.py new file mode 100644 index 000000000..6ae7c6ec1 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/is_uc_assessed_claimant.py @@ -0,0 +1,50 @@ +from policyengine_uk.model_api import * + + +class is_uc_assessed_claimant(Variable): + value_type = bool + entity = Person + label = "Universal Credit claimant whose income is assessed" + documentation = ( + "The single claimant, or one of the two members of the couple, whose " + "income a Universal Credit award assesses: `is_uc_claimant`, limited " + "to the two eldest. Universal Credit deducts the income of the " + "claimant, or the combined income of joint claimants; where a member " + "of a couple claims as a single person, their partner's income counts " + "as if they were joint claimants. A child's or qualifying young " + "person's income is theirs, not the claimant's, and does not count; " + "nor does the income of anyone else in the benefit unit. Who the " + "claimants are comes from `is_uc_claimant`: a member it flags is " + "taken as a claimant, whatever their age." + ) + definition_period = YEAR + reference = [ + dict( + title="Welfare Reform Act 2012 s. 2(1)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/2", + ), + dict( + title="Welfare Reform Act 2012 s. 8(4)", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/8", + ), + dict( + title="Welfare Reform Act 2012 s. 40", + href="https://www.legislation.gov.uk/ukpga/2012/5/section/40", + ), + dict( + title="Universal Credit Regulations 2013 reg. 22(1) and (3)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/22", + ), + ] + + def formula(person, period, parameters): + # A claim is made by a single person or jointly by the two members of + # a couple (WRA 2012 s. 2(1)), and "claimant" means a single claimant + # or each of joint claimants (s. 40), so there are at most two. Where + # the data flag more (an adult child in the parents' benefit unit), + # the two eldest are the claimants; members of the same age rank in + # the order they are listed. + claimant = person("is_uc_claimant", period) + age = person("age", period) + rank = person.get_rank(person.benunit, -age, condition=claimant) + return claimant & (rank < 2)