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 0000000000..278c5e9931 --- /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 when inputs flag more). 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 0000000000..def11bef16 --- /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); WRA 2012 s. 8(4)). `uc_earned_income` and `uc_unearned_income` also counted the claimant's children and qualifying young people, so a dependant's wages, profits or pension reduced the parents' award. The benefit cap earnings exception (reg. 82(1)(a)), the capped benefit total (WRA 2012 s. 96(1)) and the childcare work condition (reg. 32(1)) now read the claimants only, so a dependant's job neither lifts the cap nor meets or blocks the work condition. diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index d06c75ce64..c587a5fa5e 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**: 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 maximum entitlement is then reduced by 55% (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter) of the benunit's combined earnings above its work allowance (`uc_work_allowance`), and by 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, 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 (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**: 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 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 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/childcare_element/uc_childcare_work_condition_reg_32.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml index 0c43d5ad79..9058ab33ab 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/childcare_element/uc_childcare_work_condition_reg_32.yaml @@ -511,11 +511,11 @@ members: [first, second, third] output: # A couple has two members, so three flagged claimants are a data error, - # not a legal claim. The formula generalises reg. 32(1) as: some claimant - # meets limb (a) and every other claimant meets limb (b). + # not a legal claim. The two eldest are the claimants + # (is_uc_assessed_claimant): one meets limb (a) and the other limb (b). uc_childcare_work_condition: true -- name: Reg. 32(1) - three flagged claimants, one of whom neither works nor meets an exception +- name: Reg. 32(1) - three flagged claimants; the youngest neither works nor meets an exception period: 2025 absolute_error_margin: 0 input: @@ -538,6 +538,37 @@ household: members: [first, second, third] output: + # Three flagged claimants are a data error. The two eldest are the + # claimants (is_uc_assessed_claimant), as for their income, so the + # youngest's work is not tested: one claimant works and the other has + # limited capability for work. + is_uc_assessed_claimant: [true, true, false] + uc_childcare_work_condition: true + +- name: Reg. 32(1) - three flagged claimants; the second eldest neither works nor meets an exception + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_claimant_or_partner: true + employment_income: 15_000 + second: + age: 39 + is_claimant_or_partner: true + third: + age: 38 + is_claimant_or_partner: true + uc_limited_capability_for_WRA: true + benunits: + benunit: + members: [first, second, third] + households: + household: + members: [first, second, third] + output: + is_uc_assessed_claimant: [true, true, false] uc_childcare_work_condition: false - name: Reg. 32(1)(a) - self-employment is paid work 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 0000000000..19bfeb3f28 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/dependants_income.yaml @@ -0,0 +1,213 @@ +# 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 by the data 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: + is_claimant_or_partner: true + age: 50 + employment_income: 15_000 + parent_2: + is_claimant_or_partner: true + age: 48 + employment_income: 15_000 + adult_child: + age: 25 + is_claimant_or_partner: true + 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 517c2ccda2..23c44124de 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 @@ -61,3 +61,119 @@ 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 by the data 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: + is_claimant_or_partner: true + age: 50 + employment_income: 10_000 + parent_2: + is_claimant_or_partner: true + age: 48 + adult_child: + age: 25 + is_claimant_or_partner: true + 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 1ae608c9dc..f193617754 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 @@ -313,3 +313,100 @@ uc_assessable_capital: 6_001 uc_tariff_income: 4.35 * 12 uc_unearned_income: 4.35 * 12 + +# 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 + private_pension_income: 400 + carers_allowance: 500 + benunits: + benunit: + members: [parent, child] + output: + uc_unearned_income: 1_000 + +- name: A dependant's contributory JSA or ESA supplied by the data does not count either + # Not a lawful case: a person receiving ESA or JSA is not a qualifying + # young person (reg. 5(5)), so they would not be the claimant's + # dependant. Data can still supply them (the Enhanced FRS gives some + # synthetic children contributory ESA); their income is still not the + # claimant's. + 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 + jsa_contrib: 300 + esa_contrib: 200 + benunits: + benunit: + members: [parent, child] + output: + uc_unearned_income: 1_000 + +- name: A dependant's pension 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 + private_pension_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: Tariff income from the household's capital still counts with a dependant present + # The household's £6,001 is £1 over the £6,000 lower limit: one £4.35 + # monthly step (reg. 72). The child's pension is not the claimant's. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + is_parent: true + child: + age: 10 + private_pension_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 0000000000..e916f4bf05 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/is_uc_assessed_claimant.yaml @@ -0,0 +1,127 @@ +# 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 is not a 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, false] + is_uc_assessed_claimant: [true, true, false] + +- name: An adult child flagged as a claimant by the data is not a third claimant + period: 2026 + absolute_error_margin: 0 + input: + people: + parent_1: + is_claimant_or_partner: true + age: 50 + parent_2: + is_claimant_or_partner: true + age: 48 + adult_child: + age: 25 + is_claimant_or_partner: true + 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 0000000000..b03ea77dd3 --- /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 (reg. 66(1)(a)) plus, with capital over + £6,000, the reg. 72 tariff income of £4.35 a month for each £250 or part. + Savings interest is not reg. 66(1) income for anyone, claimant or not. + 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. +""" + +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) + # Retirement pension income is unearned income (reg. 66(1)(a)); + # savings interest is in no reg. 66(1) description, so capital counts + # only through its tariff income (reg. 72(1)). + pensions = sum(c["private_pension_income"] for c in unit["claimants"]) + excess = unit["savings"] - TARIFF_LOWER_LIMIT + tariff = 0.0 + if excess > 0: + tariff = np.ceil(excess / TARIFF_STEP) * TARIFF_INCOME_PER_STEP * 12 + expected_unearned = pensions + tariff + 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/variables/gov/dwp/benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/benefit_cap_reduction.py index bb0fddc2a4..7d021f0b6f 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 6746386523..1bdb797d6a 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 @@ -20,11 +20,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 86ef872a7e..d6e16123e6 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 @@ -34,7 +34,9 @@ class uc_childcare_work_condition(Variable): def formula(benunit, period, parameters): person = benunit.members - claimant = person("is_uc_claimant", period) + # The claimant and partner (at most two members), never a dependant + # or a further member flagged as a claimant. + claimant = person("is_uc_assessed_claimant", period) in_paid_work = person("in_work", period) | person( "uc_childcare_treated_as_in_paid_work", period ) 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..ee94ce5043 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 91a978566f..9a69a049ac 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,27 +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): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (UC Regs 2013 reg 22); dropping dependants' - # own income is a follow-up. Anyone else in the benefit unit does not count. - person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_qualifying_young_person_for_universal_credit", period - ) - # 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)). + # 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"], 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 ac7fb31548..82ae0f66cb 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 @@ -43,12 +43,9 @@ def formula(person, period, parameters): # 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, and is_uc_claimant flags at most two; where - # inputs flag more, 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 - ) + # inputs flag more, the two eldest are the couple + # (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 25f13198c8..8c9c47126a 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 @@ -6,23 +6,38 @@ class uc_unearned_income(Variable): entity = BenUnit label = "Universal Credit unearned income" documentation = ( - "Income falling within the descriptions in regulation 66(1) of the " - "Universal Credit Regulations 2013. Capital counts only through its " - "assumed yield (uc_tariff_income, regulation 72(1)); actual interest, " - "dividends and rent are not unearned income at any capital level." + "The claimant's unearned income, or joint claimants' combined " + "unearned income: income falling within the descriptions in " + "regulation 66(1) of the Universal Credit Regulations 2013. Capital " + "counts only through its assumed yield (uc_tariff_income, regulation " + "72(1)); actual interest, dividends and rent are not unearned income " + "at any capital level. 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 = "https://www.legislation.gov.uk/uksi/2013/376/regulation/66" + 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): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (UC Regs 2013 reg 22); dropping dependants' - # own income is a follow-up. Anyone else in the benefit unit does not count. - person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_qualifying_young_person_for_universal_credit", period - ) + # 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 - return add_for_members(benunit, period, p.income_definitions.unearned, members) + claimants = benunit.members("is_uc_assessed_claimant", period) + return add_for_members( + benunit, period, p.income_definitions.unearned, claimants + ) 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 0000000000..14cce046f2 --- /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. + # is_uc_claimant flags at most two unless inputs flag more; then the + # two eldest are the claimants, and members of the same age rank in + # the order the people are entered. + claimant = person("is_uc_claimant", period) + age = person("age", period) + rank = person.get_rank(person.benunit, -age, condition=claimant) + return claimant & (rank < 2)