diff --git a/changelog.d/uc-earned-income-consumers-1986.fixed.md b/changelog.d/uc-earned-income-consumers-1986.fixed.md new file mode 100644 index 0000000000..47d3070fa6 --- /dev/null +++ b/changelog.d/uc-earned-income-consumers-1986.fixed.md @@ -0,0 +1 @@ +Use Universal Credit earned income as each rule defines it (#1986): the benefit cap earnings exception now applies only to an award of Universal Credit (not to Housing Benefit) and sums each person's earnings net of their own tax, National Insurance and relievable pension contributions, without the minimum income floor, against 16 hours a week at the national living wage in whole pounds a month (UC Regs 2013 regs 82 and 6(1A)(za); £881 a month from April 2026) instead of a hard-coded £10,152 a year; the targeted childcare Universal Credit route and the Kingston upon Thames, Merton, Newham and Westminster council tax reduction schemes now count earned income before the work allowance. diff --git a/changelog.d/uc-earned-income-own-tax.fixed.md b/changelog.d/uc-earned-income-own-tax.fixed.md new file mode 100644 index 0000000000..0a800b871c --- /dev/null +++ b/changelog.d/uc-earned-income-own-tax.fixed.md @@ -0,0 +1 @@ +Deduct from each person's Universal Credit earnings only their own income tax and National Insurance on that employment or self-employment (UC Regs 2013 reg. 55(5)(b), reg. 57(2) step 3), not the whole benefit unit's tax on all income, so tax on pensions, State Pension, property, savings and dividends, the High Income Child Benefit Charge and voluntary Class 3 contributions no longer reduce earned income. diff --git a/docs/book/programs/gov/dfe/targeted-childcare-entitlement.ipynb b/docs/book/programs/gov/dfe/targeted-childcare-entitlement.ipynb index 3b41a8f8a5..0c1c2cb9d4 100644 --- a/docs/book/programs/gov/dfe/targeted-childcare-entitlement.ipynb +++ b/docs/book/programs/gov/dfe/targeted-childcare-entitlement.ipynb @@ -28,7 +28,7 @@ "\n", "#### Eligibility conditions\n", "Families may also qualify based on income thresholds if they receive:\n", - "* **Universal Credit** with household earned income of **£15,400 or less** per year\n", + "* **Universal Credit** with earned income of **£15,400 or less** per year (combined for a couple), as Universal Credit calculates it: earnings after the person's own tax, National Insurance and pension contributions, and before the work allowance (`uc_earned_income_before_work_allowance`)\n", "* **Tax Credits** (Working Tax Credit or Child Tax Credit) with household income of **£16,190 or less** per year\n", "\n", "#### Geographic scope\n", diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index 5e4ecbe443..db0208ec42 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -44,7 +44,12 @@ { "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 (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, 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**: 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. 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/`." + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "### Benefit cap earnings exception\n\nThe cap does not apply where the claimant's earned income, or a couple's combined earned income, reaches 16 hours a week at the national living wage, converted to a monthly amount by multiplying by 52 and dividing by 12, with any fraction of a pound disregarded (UC Regulations 2013 regs 82(1)(a) and 6(1A)(za)): £846 a month in 2025-26 and £881 in 2026-27 (`benefit_cap_earnings_threshold`, twelve times the monthly amount). Before 1 April 2017 it was £430 a month. Earned income here (`benefit_cap_earned_income`) is each person's earnings net of their own income tax, National Insurance and pension contributions, and leaves out income treated as earned under the minimum income floor (reg 82(4)). The nine-month grace period is not modelled." }, { "cell_type": "code", diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/README.md b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/README.md new file mode 100644 index 0000000000..e48bb342f4 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/README.md @@ -0,0 +1 @@ +# Benefit cap diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/README.md b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/README.md new file mode 100644 index 0000000000..1cd731ff55 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/README.md @@ -0,0 +1 @@ +# Earnings exception diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/monthly_amount.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/monthly_amount.yaml new file mode 100644 index 0000000000..5e4cfed49a --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/monthly_amount.yaml @@ -0,0 +1,13 @@ +description: Fixed monthly earned income at or above which a Universal Credit award is excepted from the benefit cap. The regulations set a sum of money until 1 April 2017 and a number of hours at the national living wage rate since, so this is zero from that date. +values: + 2013-04-29: 430 + 2017-04-01: 0 +metadata: + unit: currency-GBP + period: month + label: Universal Credit benefit cap earnings exception fixed monthly amount + reference: + - title: The Universal Credit Regulations 2013 reg. 82(1)(a), as made + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/82/made + - title: The Universal Credit (Benefit Cap Earnings Exception) Amendment Regulations 2017 reg. 2(3)(a) + href: https://www.legislation.gov.uk/uksi/2017/138/regulation/2/made diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/weekly_hours.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/weekly_hours.yaml new file mode 100644 index 0000000000..03dd32e203 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/benefit_cap/earnings_exception/weekly_hours.yaml @@ -0,0 +1,13 @@ +description: Hours a week at the national living wage rate whose pay, converted to a monthly amount, is the earned income at or above which a Universal Credit award is excepted from the benefit cap. Zero before 1 April 2017, when the regulations set a fixed sum instead. +values: + 2013-04-29: 0 + 2017-04-01: 16 +metadata: + unit: hour + period: week + label: Universal Credit benefit cap earnings exception weekly hours at the national living wage + reference: + - title: The Universal Credit Regulations 2013 reg. 82(1)(a) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/82 + - title: The Universal Credit (Benefit Cap Earnings Exception) Amendment Regulations 2017 reg. 2(3)(a) + href: https://www.legislation.gov.uk/uksi/2017/138/regulation/2/made 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 62a0fd3051..517c2ccda2 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 @@ -5,10 +5,11 @@ employment_income: 1_000 miscellaneous_income: 100 uc_work_allowance: 10 - # The claimant's own tax (UC reg 22 counts the claimant's earnings). - tax: 1 + # The claimant's own tax on these earnings. + uc_income_tax_on_earnings: 1 output: - uc_earned_income: 1100 - 11 + uc_individual_earned_income: 1_100 - 1 + uc_earned_income: 1_100 - 11 - name: Capped at 0 period: 2025 @@ -17,7 +18,46 @@ employment_income: 1_000 miscellaneous_income: 100 uc_work_allowance: 2_000 - # The claimant's own tax (UC reg 22 counts the claimant's earnings). - tax: 1 + # The claimant's own tax on these earnings. + uc_income_tax_on_earnings: 1 output: uc_earned_income: 0 + +- name: Each person's deductions come off only their own earnings + period: 2025 + absolute_error_margin: 0 + input: + people: + earner: + employment_income: 1_000 + uc_income_tax_on_earnings: 100 + uc_national_insurance_on_earnings: 10 + partner: + uc_income_tax_on_earnings: 500 + pension_contributions: 50 + benunits: + benunit: + members: [earner, partner] + uc_work_allowance: 0 + output: + uc_individual_earned_income: [890, 0] + uc_earned_income: 890 + +- name: Work allowance comes off combined earnings after each person's deductions + period: 2025 + absolute_error_margin: 0 + input: + people: + earner: + employment_income: 1_000 + uc_income_tax_on_earnings: 100 + partner: + employment_income: 500 + pension_contributions: 50 + benunits: + benunit: + members: [earner, partner] + uc_work_allowance: 300 + output: + uc_individual_earned_income: [900, 450] + uc_earned_income: 1_050 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml new file mode 100644 index 0000000000..2df9a8fbb6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_earnings_deductions.yaml @@ -0,0 +1,368 @@ +# Universal Credit deducts from a person's earnings only the income tax and +# National Insurance that person pays in respect of their own employment +# (UC Regs 2013 reg. 55(5)(b)) or trade (reg. 57(2) step 3), plus their own +# relievable pension contributions (reg. 55(5)(a)). Tax on pensions, State +# Pension, property, savings and dividends is not deducted, and one partner's +# tax never reduces the other's earnings. Earnings are taken as the lowest +# slice of the person's non-savings income, after their allowances; savings +# and dividends sit above all non-savings income (ITA 2007 s. 16). +# +# Hand calculations use the 2026-27 rates: +# - personal allowance 12,570; basic rate 20% on the first 37,700 of taxable +# income; dividend allowance 500, dividend ordinary rate 10.75%; property +# allowance 1,000; trading allowance 1,000 (only for a trade profit within +# it when gross receipts are not given, ITTOIA 2005 s. 783AI); +# - primary Class 1 NI 8% above the primary threshold of 241.73 a week +# (12,569.96 a year); Class 4 NI 6% above the lower profits limit of 12,570; +# Class 2 NI nil; +# - UC standard allowance 424.90 a month (single, 25 or over) or 666.97 +# (couple, one 25 or over); child element 303.94 a month; work allowance +# 427 a month with housing costs; taper 55%. + +- name: Single parent with earnings and property income, only tax on earnings is deducted + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 20_000 + property_income: 5_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + # Earnings take the personal allowance first: (20,000 - 12,570) x 20%. + # Property income is taxed on (5,000 - 1,000) x 20% = 800, not deducted. + income_tax: [2_286, 0] + uc_income_tax_on_earnings: [1_486, 0] + # (20,000 - 12,569.96) x 8%. + uc_national_insurance_on_earnings: [594.40, 0] + # 20,000 - 1,486 - 594.40. + uc_individual_earned_income: [17_919.60, 0] + # Less the work allowance of 427 x 12 = 5,124. + uc_earned_income: 12_795.60 + # Rent from property is not UC unearned income (reg. 66(1) is a closed + # list). + uc_unearned_income: 0 + # 424.90 x 12 + 303.94 x 12 + 9,600 = 5,098.80 + 3,647.28 + 9,600. + uc_maximum_amount: 18_346.08 + # 18,346.08 - 0.55 x 12,795.60. Deducting all of the tax, as before this + # fix, gave 11,748.50: 0.55 x 800 = 440 too much. + universal_credit: 11_308.50 + +- name: Mixed-age couple, the pensioner's tax on State Pension does not reduce the partner's earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 16_000 + partner: + age: 45 + employment_income: 13_000 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 20_000 + output: + # Pensioner: (16,000 - 12,570) x 20% = 686, on State Pension. + # Partner: (13,000 - 12,570) x 20% = 86 and (13,000 - 12,569.96) x 8% + # = 34.40. Both are above the personal allowance, so there is no + # Marriage Allowance. + income_tax: [686, 86] + uc_income_tax_on_earnings: [0, 86] + uc_national_insurance_on_earnings: [0, 34.40] + # 13,000 - 86 - 34.40. + uc_earned_income: 12_879.60 + uc_unearned_income: 16_000 + # 666.97 x 12 + 20,000. + uc_maximum_amount: 28_003.64 + # 28,003.64 - (0.55 x 12,879.60 + 16,000). Deducting the whole benefit + # unit's tax, as before this fix, gave 5,297.16: 0.55 x 686 = 377.30 + # too much. + universal_credit: 4_919.86 + +- name: Self-employed claimant with pension and dividend income, only tax on the trade is deducted + period: 2026 + absolute_error_margin: 0.01 + input: + age: 55 + self_employment_income: 18_000 + # In a start-up period, so the minimum income floor does not apply. + uc_is_in_startup_period: true + private_pension_income: 4_000 + dividend_income: 3_000 + tenure_type: RENT_FROM_COUNCIL + rent: 12_000 + output: + # Trade: a profit of 18,000 is above the trading allowance, so no + # allowance applies without gross receipts. It takes the personal + # allowance first: (18,000 - 12,570) x 20% = 1,086. Pension: 4,000 x 20% + # = 800. Dividends: (3,000 - 500) x 10.75% = 268.75. Neither is deducted. + income_tax: 2_154.75 + uc_income_tax_on_earnings: 1_086 + # Class 4: (18,000 - 12,570) x 6%. + uc_national_insurance_on_earnings: 325.80 + # 18,000 - 1,086 - 325.80, with no work allowance. + uc_earned_income: 16_588.20 + # The pension counts; dividends are not UC unearned income. + uc_unearned_income: 4_000 + # 424.90 x 12 + 12,000. + uc_maximum_amount: 17_098.80 + # 17,098.80 - (0.55 x 16,588.20 + 4,000). Deducting all of the tax and + # NI, as before this fix, gave 4,563.10: 0.55 x 1,068.75 = 587.81 too + # much. + universal_credit: 3_975.29 + +- name: Scottish taxpayer, earnings sit below pension income in the Scottish bands + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 20_000 + private_pension_income: 10_000 + region: SCOTLAND + output: + # 2026-27 Scottish bands above the personal allowance: starter 19% on the + # first 3,967, basic 20% to 16,956, intermediate 21% to 31,092. + # Earnings slice 7,430: 3,967 x 19% + 3,463 x 20% = 753.73 + 692.60. + uc_income_tax_on_earnings: 1_446.33 + # Pension slice 7,430 to 17,430: 9,526 x 20% + 474 x 21% = 2,004.74. + income_tax: 3_451.07 + +- name: Couple with Marriage Allowance, the recipient's reduction lowers the tax on their earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + recipient: + age: 40 + employment_income: 15_000 + dividend_income: 5_000 + transferor: + age: 38 + employment_income: 10_000 + benunits: + benunit: + members: [recipient, transferor] + households: + household: + members: [recipient, transferor] + output: + # The transferor's unused allowance of 2,570 exceeds the 1,257 cap, which + # rounds up to 1,260. Tax on the recipient's earnings: + # (15,000 - 12,570 - 1,260) x 20% = 234, the same as 486 less the + # 20% x 1,260 = 252 reduction. Dividends: (5,000 - 500) x 10.75% = 483.75, + # not deducted. + marriage_allowance: [1_260, 0] + income_tax: [717.75, 0] + uc_income_tax_on_earnings: [234, 0] + # (15,000 - 12,569.96) x 8%. + uc_national_insurance_on_earnings: [194.40, 0] + uc_individual_earned_income: [14_571.60, 10_000] + uc_earned_income: 24_571.60 + +- name: Voluntary Class 3 NI is not deducted + period: 2026 + absolute_error_margin: 0.01 + input: + age: 40 + employment_income: 13_000 + ni_class_3: 900 + output: + national_insurance: 934.40 + uc_national_insurance_on_earnings: 34.40 + # 13,000 - 86 - 34.40. + uc_earned_income: 12_879.60 + +- name: Pension contributions come off only the contributor's own earnings + period: 2026 + absolute_error_margin: 0.01 + input: + people: + earner: + age: 40 + employment_income: 20_000 + employee_pension_contributions: 1_000 + would_claim_marriage_allowance: false + partner: + age: 38 + personal_pension_contributions: 1_000 + would_claim_marriage_allowance: false + benunits: + benunit: + members: [earner, partner] + households: + household: + members: [earner, partner] + output: + # Relief on the earner's contributions: (20,000 - 12,570 - 1,000) x 20%. + uc_income_tax_on_earnings: [1_286, 0] + # 20,000 - 1,000 - 1,286 - 594.40. The partner has no earnings, so their + # own contributions reduce nothing. + uc_individual_earned_income: [17_119.60, 0] + uc_earned_income: 17_119.60 + +- name: High Income Child Benefit Charge is not deducted + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + employment_income: 70_000 + child: + age: 3 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + # Child Benefit 27.05 x 52 = 1,406.60; the charge takes + # (70,000 - 60,000) / 200 = 50% of it, 703.30. + CB_HITC: [703.30, 0] + income_tax: [16_135.30, 0] + # 37,700 x 20% + (57,430 - 37,700) x 40%. + uc_income_tax_on_earnings: [15_432, 0] + +- name: From 2027 property income has its own rates, and its tax is still not deducted + period: 2027 + absolute_error_margin: 0.01 + input: + age: 35 + employment_income: 20_000 + property_income: 5_000 + output: + # Property: (5,000 - 1,000) x 22% = 880. + property_income_tax: 880 + income_tax: 2_366 + uc_income_tax_on_earnings: 1_486 + +- name: A tax reduction comes off the tax on earnings first + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 92 + employment_income: 20_000 + married_couples_allowance: 11_000 + would_claim_marriage_allowance: false + partner: + age: 45 + would_claim_marriage_allowance: false + benunits: + benunit: + members: [older, partner] + households: + household: + members: [older, partner] + output: + # Married couple's allowance reduction: 10% x 11,000 = 1,100. + capped_mcad: [1_100, 0] + # (20,000 - 12,570) x 20% = 1,486, less 1,100. + income_tax: [386, 0] + uc_income_tax_on_earnings: [386, 0] + +- name: Pension income does not change the tax on earnings when a reduction applies + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 92 + employment_income: 20_000 + private_pension_income: 5_000 + married_couples_allowance: 11_000 + would_claim_marriage_allowance: false + partner: + age: 45 + would_claim_marriage_allowance: false + benunits: + benunit: + members: [older, partner] + households: + household: + members: [older, partner] + output: + # 1,486 on earnings + 5,000 x 20% on the pension - 1,100. + income_tax: [1_386, 0] + # The reduction still comes off the earnings slice: 1,486 - 1,100. + uc_income_tax_on_earnings: [386, 0] + +- name: The pension annual allowance charge is not deducted, even with a tax reduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 92 + employment_income: 20_000 + employer_pension_contributions: 65_000 + married_couples_allowance: 11_000 + would_claim_marriage_allowance: false + partner: + age: 45 + would_claim_marriage_allowance: false + benunits: + benunit: + members: [older, partner] + households: + household: + members: [older, partner] + output: + # Contributions of 65,000 exceed the 60,000 annual allowance by 5,000, + # charged at the basic rate: 1,000. + personal_pension_contributions_tax: [1_000, 0] + income_tax: [1_386, 0] + uc_income_tax_on_earnings: [386, 0] + +- name: Pension contributions paid after 75 are not relievable, so are not deducted + period: 2026 + absolute_error_margin: 0.01 + input: + age: 76 + employment_income: 15_000 + employee_pension_contributions: 1_000 + output: + # No relief after 75 (FA 2004 s. 188(3)(a)): (15,000 - 12,570) x 20%. + uc_income_tax_on_earnings: 486 + # Over State Pension age, so no Class 1 NI. + uc_national_insurance_on_earnings: 0 + # 15,000 - 486, with the 1,000 of contributions not deducted. + uc_individual_earned_income: 14_514 + +- name: A reform exempting pensions from income tax leaves the tax on earnings unchanged + period: 2026 + absolute_error_margin: 0.01 + input: + gov.hmrc.income_tax.earned_taxable_income_exclusions: + - taxable_savings_interest_income + - taxable_dividend_income + - taxable_property_income + - received_allowances_earned_income + - marriage_allowance + - taxable_pension_income + age: 40 + employment_income: 20_000 + private_pension_income: 10_000 + output: + # The pension is outside the taxed base, so it is not above earnings + # either: (20,000 - 12,570) x 20%. + income_tax: 1_486 + uc_income_tax_on_earnings: 1_486 diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.yaml new file mode 100644 index 0000000000..568e6240d7 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.yaml @@ -0,0 +1,243 @@ +# Targeted (two-year-old) childcare, Universal Credit route. Local Authority +# (Duty to Secure Early Years Provision Free of Charge) Regulations 2014 +# reg. 1(2), definition of "eligible child" (a)(vii), and reg. 1(3)-(4): +# https://www.legislation.gov.uk/uksi/2014/2147/regulation/1 +# +# The parent must be entitled to universal credit with earned income not +# exceeding the applicable amount. Reg. 1(3)(a): "earned income" means income +# for the purposes of Chapter 2 of Part 6 of the UC Regulations 2013, which +# is net of the person's own tax, NI and pension contributions and comes +# before the work allowance (reg. 22, Part 3). Reg. 1(3)(c) reads the limit +# against a couple's combined income. The limit here is 15,400 a year. +# In 2026-27 the personal allowance and the primary threshold are 12,570. + +- name: "#1986 counterexample: earnings of 17,000 exceed the limit" + # Tax (17,000 - 12,570) x 20% = 886; NI (17,000 - 12,570) x 8% = 354.40; + # earned income 15,759.60 > 15,400. After the 5,124 work allowance it + # would be 10,635.60, which is how the model wrongly passed it before. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 17_000 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_earned_income_before_work_allowance: 15_759.60 + uc_earned_income: 10_635.60 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + +- name: Earned income just under the limit meets the criterion + # Tax (16,500 - 12,570) x 20% = 786; NI 3,930 x 8% = 314.40; earned income + # 15,399.60 <= 15,400. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 16_500 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_earned_income_before_work_allowance: 15_399.60 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: true + +- name: Earned income just over the limit does not + # Tax 3,931 x 20% = 786.20; NI 3,931 x 8% = 314.48; earned income + # 15,400.32 > 15,400. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 16_501 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_earned_income_before_work_allowance: 15_400.32 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + +- name: A couple's combined earned income is tested + # Reg. 1(3)(c): 8,000 + 8,000 = 16,000 > 15,400, although each partner is + # under the limit alone. + period: 2026 + input: + people: + parent1: + age: 30 + employment_income: 8_000 + parent2: + age: 30 + employment_income: 8_000 + child: + age: 2 + benunits: + benunit: + members: [parent1, parent2, child] + would_claim_uc: true + households: + household: + members: [parent1, parent2, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_earned_income_before_work_allowance: 16_000 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + +- name: Combined earned income exactly at the limit meets the criterion + # "not exceeding": 7,700 + 7,700 = 15,400 <= 15,400. Each is under the + # personal allowance and primary threshold, so no tax or NI. + period: 2026 + input: + people: + parent1: + age: 30 + employment_income: 7_700 + parent2: + age: 30 + employment_income: 7_700 + child: + age: 2 + benunits: + benunit: + members: [parent1, parent2, child] + would_claim_uc: true + households: + household: + members: [parent1, parent2, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_earned_income_before_work_allowance: 15_400 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: true + +- name: Income treated as earned under the minimum income floor counts + # Reg. 1(3)(a) uses UC Part 6 Chapter 2, which includes reg. 62. The + # partner who is not the child's carer has profits of 3,000 and is subject + # to the floor (35 hours at the national living wage), so the couple's + # earned income is far above 15,400, although actual earnings are 3,000. + period: 2026 + input: + people: + self_employed_partner: + age: 30 + self_employment_income: 3_000 + uc_is_in_startup_period: false + carer: + age: 30 + child: + age: 2 + benunits: + benunit: + members: [self_employed_partner, carer, child] + would_claim_uc: true + households: + household: + members: [self_employed_partner, carer, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_mif_applies: [true, false, false] + benefit_cap_earned_income: 3_000 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + +- name: Without pension contributions, earnings of 16,800 exceed the limit + # Tax 4,230 x 20% = 846; NI 4,230 x 8% = 338.40; earned income 15,615.60. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 16_800 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + uc_earned_income_before_work_allowance: 15_615.60 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + +- name: Pension contributions come off earned income for the limit + # Reg. 55(5)(a): the 1,000 contribution brings earned income to at most + # 16,800 - 1,000 - 338.40 - 646 = 14,815.60 (with tax relief at source), + # under 15,400. + period: 2026 + input: + people: + parent: + age: 30 + employment_income: 16_800 + employee_pension_contributions: 1_000 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + meets_universal_credit_criteria_for_targeted_childcare_entitlement: true + +- name: No Universal Credit entitlement, no Universal Credit route + period: 2026 + input: + people: + parent: + age: 30 + employment_income: 5_000 + child: + age: 2 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + output: + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_earnings_exception.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_earnings_exception.yaml new file mode 100644 index 0000000000..cbe571a5c1 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/benefit_cap_earnings_exception.yaml @@ -0,0 +1,384 @@ +# Universal Credit benefit cap earnings exception, UC Regs 2013 reg. 82. +# https://www.legislation.gov.uk/uksi/2013/376/regulation/82 +# +# (1)(a): the cap does not apply where "the claimant's earned income or, if +# the claimant is a member of a couple, the couple's combined earned income" +# is at least the pay for 16 hours a week at the reg. 4 National Minimum +# Wage Regulations rate (the national living wage), times 52 and divided by +# 12. Reg. 6(1A)(za) disregards a fraction of a pound in that amount. +# (4): earned income excludes income treated as earned under reg. 62 (the +# minimum income floor). +# The exception applies "to an award of universal credit": the income test is +# uc_benefit_cap_earnings_threshold_met, and is_benefit_cap_exempt_earnings +# also needs a UC award. Cases without one test the income test only. +# Earned income is net of the person's own income tax, NI and relievable +# pension contributions on their earnings (regs. 55(5), 57(2)); Class 3 is not +# on earnings. In 2025-26 and 2026-27 the personal allowance and the primary +# threshold are both 12,570 a year. + +# --- Threshold: 16 x national living wage x 52 / 12, whole pounds --- + +- name: Before 1 April 2017 the threshold was 430 a month + # Reg. 82(1)(a) as made; SI 2017/138 substituted the hours formula. + period: 2016 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 5_160 # 430 x 12 + +- name: 2017-18 threshold from the 7.50 national living wage + # 7.50 x 16 x 52 / 12 = 520.00. + period: 2017 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 6_240 # 520 x 12 + +- name: 2020-21 threshold rounds 604.59 down to 604 a month + # 8.72 x 16 x 52 / 12 = 604.59. + period: 2020 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 7_248 # 604 x 12 + +- name: 2022-23 threshold rounds 658.67 down to 658 a month + # 9.50 x 16 x 52 / 12 = 658.67. + period: 2022 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 7_896 # 658 x 12 + +- name: 2024-25 threshold rounds 793.17 down to 793 a month + # 11.44 x 16 x 52 / 12 = 793.17. + period: 2024 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 9_516 # 793 x 12 + +- name: 2025-26 threshold is 846 a month + # 12.21 x 16 x 52 / 12 = 846.56; GOV.UK gives 846 to 31 March 2026. + period: 2025 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 10_152 # 846 x 12 + +- name: 2026-27 threshold is 881 a month + # 12.71 x 16 x 52 / 12 = 881.23; GOV.UK gives 881 from 1 April 2026. + period: 2026 + input: + people: + person: + age: 30 + output: + benefit_cap_earnings_threshold: 10_572 # 881 x 12 + +- name: The threshold uses the national living wage whatever the claimant's age + # Reg. 82(1)(a) refers to the reg. 4 rate, not the claimant's own rate. + period: 2026 + input: + people: + person: + age: 19 + output: + benefit_cap_earnings_threshold: 10_572 + +# --- Earned income at the threshold --- + +- name: Earnings of 10,573 meet the 2026-27 threshold after rounding + # 10,573 >= 10,572. Without the whole-pound rounding the threshold would be + # 10,574.72 and the exception would not apply. + period: 2026 + input: + people: + person: + age: 30 + employment_income: 10_573 + output: + benefit_cap_earned_income: 10_573 + uc_benefit_cap_earnings_threshold_met: true + +- name: Earnings exactly at the 2026-27 threshold meet it + # "equal to or exceeds": 10,572 >= 10,572. + period: 2026 + input: + people: + person: + age: 30 + employment_income: 10_572 + output: + benefit_cap_earned_income: 10_572 + uc_benefit_cap_earnings_threshold_met: true + +- name: Earnings of 10,571 fall short of the 2026-27 threshold + period: 2026 + input: + people: + person: + age: 30 + employment_income: 10_571 + output: + benefit_cap_earned_income: 10_571 + uc_benefit_cap_earnings_threshold_met: false + +- name: Earned income is net of the person's income tax and NI on the earnings + # Tax (14,000 - 12,570) x 20% = 286; Class 1 (14,000 - 12,570) x 8% = + # 114.40; 14,000 - 286 - 114.40 = 13,599.60. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + employment_income: 14_000 + output: + benefit_cap_earned_income: 13_599.60 + uc_benefit_cap_earnings_threshold_met: true + +- name: Pension contributions are deducted from earned income + # Reg. 55(5)(a): 11,500 - 1,000 = 10,500 < 10,572. + period: 2026 + input: + people: + person: + age: 30 + employment_income: 11_500 + employee_pension_contributions: 1_000 + output: + benefit_cap_earned_income: 10_500 + uc_benefit_cap_earnings_threshold_met: false + +- name: Pension contributions paid at 74 are deducted + # Under 75 the contribution is relievable (Finance Act 2004 s. 188(3)(a)): + # 11,000 - 1,000 = 10,000. No NI is due over State Pension age. + period: 2026 + input: + people: + person: + age: 74 + employment_income: 11_000 + employee_pension_contributions: 1_000 + output: + benefit_cap_earned_income: 10_000 + +- name: Pension contributions paid at 75 are not deducted + # Finance Act 2004 s. 188(3)(a): contributions paid once the person has + # reached 75 are not relievable, + # so reg. 55(5)(a) does not deduct them. 11,000 is under the personal + # allowance and no NI is due over State Pension age, so earned income is + # 11,000. + period: 2026 + input: + people: + person: + age: 75 + employment_income: 11_000 + employee_pension_contributions: 1_000 + output: + benefit_cap_earned_income: 11_000 + +- name: Voluntary Class 3 contributions are not deducted + period: 2026 + input: + people: + person: + age: 30 + employment_income: 10_600 + ni_class_3: 900 + output: + benefit_cap_earned_income: 10_600 + uc_benefit_cap_earnings_threshold_met: true + +- name: Tax on the person's property income is not deducted from their earnings + # Earnings are the lowest slice of income and sit inside the personal + # allowance, so the tax on earnings is nil. + period: 2026 + input: + people: + person: + age: 30 + employment_income: 10_600 + property_income: 40_000 + output: + benefit_cap_earned_income: 10_600 + uc_benefit_cap_earnings_threshold_met: true + +- name: A partner's tax on property income does not reduce the couple's earned income + period: 2026 + input: + people: + earner: + age: 30 + employment_income: 10_600 + partner: + age: 30 + property_income: 40_000 + benunits: + benunit: + members: [earner, partner] + output: + benefit_cap_earned_income: 10_600 + uc_benefit_cap_earnings_threshold_met: true + +- name: A couple's earned income is combined + # Reg. 82(1)(a): 6,000 + 6,000 = 12,000 >= 10,572. + period: 2026 + input: + people: + person1: + age: 30 + employment_income: 6_000 + person2: + age: 30 + employment_income: 6_000 + benunits: + benunit: + members: [person1, person2] + output: + benefit_cap_earned_income: 12_000 + uc_benefit_cap_earnings_threshold_met: true + +- name: Income treated as earned under the minimum income floor does not count + # Reg. 82(4). Profits of 3,000 are below the Class 4 lower profits limit, so + # earned income is 3,000; the floor (35 x 12.71 x 52 = 23,132.20 gross) + # counts for the award but not for the exception. + period: 2026 + input: + people: + person: + age: 30 + self_employment_income: 3_000 + uc_is_in_startup_period: false + output: + uc_mif_applies: true + benefit_cap_earned_income: 3_000 + uc_benefit_cap_earnings_threshold_met: false + +- name: Earnings of 5,200 in 2016-17 meet the 430 a month threshold + period: 2016 + input: + people: + person: + age: 30 + employment_income: 5_200 + output: + uc_benefit_cap_earnings_threshold_met: true + +- name: Earnings of 9,520 in 2024-25 meet the 793 a month threshold + period: 2024 + input: + people: + person: + age: 30 + employment_income: 9_520 + output: + uc_benefit_cap_earnings_threshold_met: true + +- name: Earnings of 9,500 in 2024-25 fall short of the 793 a month threshold + period: 2024 + input: + people: + person: + age: 30 + employment_income: 9_500 + output: + uc_benefit_cap_earnings_threshold_met: false + +# --- #1986 counterexample --- + +- name: A partner's voluntary Class 3 does not remove the couple's exception + # London couple, four children, private rent 36,000, 2025-26. One partner + # earns 10,300; the other has no earnings and pays 923 of Class 3. Earned + # income is 10,300 >= 846 x 12 = 10,152, so the cap does not apply. Before + # #1986 the model deducted the partner's Class 3 and capped the award. + period: 2025 + input: + people: + earner: + age: 35 + employment_income: 10_300 + partner: + age: 35 + ni_class_3: 923 + child1: + age: 3 + child2: + age: 5 + child3: + age: 8 + child4: + age: 10 + benunits: + benunit: + members: [earner, partner, child1, child2, child3, child4] + would_claim_uc: true + households: + household: + members: [earner, partner, child1, child2, child3, child4] + region: LONDON + brma: INNER_NORTH_LONDON + tenure_type: RENT_PRIVATELY + rent: 36_000 + output: + benefit_cap_earned_income: 10_300 + benefit_cap_earnings_threshold: 10_152 + is_benefit_cap_exempt_earnings: true + benefit_cap_reduction: 0 + uc_benefit_cap_earnings_threshold_met: true + +# --- The exception is for Universal Credit only --- + +- name: Earnings over the UC threshold do not except a Housing Benefit award + # Reg. 82 applies "to an award of universal credit". A Housing Benefit + # claimant with no UC and no Working Tax Credit has no earnings exception + # (HB Regs 2006 reg. 75E needs entitlement to WTC). 2024-25: 9,600 is over + # the UC threshold of 793 x 12 = 9,516, but the family cap still applies. + period: 2024 + input: + people: + parent: + age: 40 + employment_income: 9_600 + hours_worked: 416 + housing_benefit_reported: 100 + child1: + age: 8 + child2: + age: 6 + child3: + age: 4 + child4: + age: 2 + benunits: + benunit: + members: [parent, child1, child2, child3, child4] + would_claim_uc: false + would_claim_housing_benefit: true + working_tax_credit: 0 + households: + household: + members: [parent, child1, child2, child3, child4] + region: LONDON + tenure_type: RENT_FROM_COUNCIL + rent: 36_000 + output: + uc_benefit_cap_earnings_threshold_met: true + universal_credit_pre_benefit_cap: 0 + is_benefit_cap_exempt_earnings: false + is_benefit_cap_exempt: false + benefit_cap: 25_323 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml index daaec21e26..6bffa6850d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction.yaml @@ -215,7 +215,7 @@ claims_all_entitled_benefits: true uc_maximum_amount: 5_000 uc_income_reduction: 2_500 - uc_earned_income: 6_000 + uc_earned_income_before_work_allowance: 6_000 uc_unearned_income: 0 households: household: @@ -228,6 +228,43 @@ universal_credit: 2_500 council_tax_reduction: 1_100 +- name: "Merton UC claimant's income counts earnings before the work allowance" + # #1986. Single parent, child aged 6, earning 12,000 (under the personal + # allowance and primary threshold, so earned income is 12,000), council rent, + # council tax 1,800. The award has a housing costs element, so the work + # allowance is 427 x 12 = 5,124 and UC = maximum - 55% x (12,000 - 5,124) = + # maximum - 3,781.80. Income (scheme para. 37) is earned income plus the + # award: 12,000 + maximum - 3,781.80, which exceeds the applicable amount + # (the maximum, para. 28) by 8,218.20. CTR = 1,800 - 20% x 8,218.20 = + # 156.36. Deducting the work allowance from income would have given 1,181.16. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 12_000 + child: + age: 6 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + claims_all_entitled_benefits: true + households: + household: + members: [parent, child] + country: ENGLAND + region: LONDON + local_authority: MERTON + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + council_tax: 1_800 + savings: 0 + output: + uc_earned_income_before_work_allowance: 12_000 + council_tax_reduction: 156.36 + - name: Merton UC claimant uses UC-reported capital period: 2026 absolute_error_margin: 0.01 @@ -371,7 +408,7 @@ claims_all_entitled_benefits: true uc_maximum_amount: 5_000 uc_income_reduction: 2_500 - uc_earned_income: 6_000 + uc_earned_income_before_work_allowance: 6_000 uc_unearned_income: 0 households: household: @@ -384,6 +421,43 @@ universal_credit: 2_500 council_tax_reduction: 1_100 +- name: "Kingston upon Thames UC claimant's income counts earnings before the work allowance" + # #1986. Single parent, child aged 6, earning 12,000 (under the personal + # allowance and primary threshold, so earned income is 12,000), council rent, + # council tax 1,800. The award has a housing costs element, so the work + # allowance is 427 x 12 = 5,124 and UC = maximum - 55% x (12,000 - 5,124) = + # maximum - 3,781.80. Income (scheme para. 37) is earned income plus the + # award: 12,000 + maximum - 3,781.80, which exceeds the applicable amount + # (the maximum, para. 28) by 8,218.20. CTR = 1,800 - 20% x 8,218.20 = + # 156.36. Deducting the work allowance from income would have given 1,181.16. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 12_000 + child: + age: 6 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + claims_all_entitled_benefits: true + households: + household: + members: [parent, child] + country: ENGLAND + region: LONDON + local_authority: KINGSTON_UPON_THAMES + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + council_tax: 1_800 + savings: 0 + output: + uc_earned_income_before_work_allowance: 12_000 + council_tax_reduction: 156.36 + - name: Kingston upon Thames UC claimant uses UC-reported capital period: 2026 absolute_error_margin: 0.01 @@ -542,6 +616,41 @@ output: council_tax_reduction: 867.92 +- name: Newham UC claimant's income counts earnings before the work allowance + # #1986. Single parent, child aged 6, earning 4,000, council rent, council + # tax 1,800. Earnings are under the 5,124 work allowance, so UC is the + # maximum. Income is 4,000 + maximum, 4,000 over the applicable amount, so + # CTR = 70% x 1,800 - 25% x 4,000 = 260. Deducting the work allowance from + # income would have left no excess and given 1,260. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 4_000 + child: + age: 6 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + claims_all_entitled_benefits: true + households: + household: + members: [parent, child] + country: ENGLAND + region: LONDON + local_authority: NEWHAM + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + council_tax: 1_800 + savings: 0 + output: + uc_earned_income_before_work_allowance: 4_000 + uc_earned_income: 0 + council_tax_reduction: 260 + - name: Newham UC claimant uses UC-reported capital against the local limit period: 2026 absolute_error_margin: 0.01 @@ -686,7 +795,7 @@ claims_all_entitled_benefits: true uc_maximum_amount: 5_000 uc_income_reduction: 2_500 - uc_earned_income: 6_000 + uc_earned_income_before_work_allowance: 6_000 uc_unearned_income: 0 households: household: @@ -699,6 +808,43 @@ universal_credit: 2_500 council_tax_reduction: 1_100 +- name: "Westminster UC claimant's income counts earnings before the work allowance" + # #1986. Single parent, child aged 6, earning 12,000 (under the personal + # allowance and primary threshold, so earned income is 12,000), council rent, + # council tax 1,800. The award has a housing costs element, so the work + # allowance is 427 x 12 = 5,124 and UC = maximum - 55% x (12,000 - 5,124) = + # maximum - 3,781.80. Income (scheme para. 37) is earned income plus the + # award: 12,000 + maximum - 3,781.80, which exceeds the applicable amount + # (the maximum, para. 28) by 8,218.20. CTR = 1,800 - 20% x 8,218.20 = + # 156.36. Deducting the work allowance from income would have given 1,181.16. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 12_000 + child: + age: 6 + benunits: + benunit: + members: [parent, child] + would_claim_uc: true + claims_all_entitled_benefits: true + households: + household: + members: [parent, child] + country: ENGLAND + region: LONDON + local_authority: WESTMINSTER + tenure_type: RENT_FROM_COUNCIL + rent: 9_600 + council_tax: 1_800 + savings: 0 + output: + uc_earned_income_before_work_allowance: 12_000 + council_tax_reduction: 156.36 + - name: Westminster UC claimant uses UC-reported capital period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/test_uc_earned_income_consumers_properties.py b/policyengine_uk/tests/test_uc_earned_income_consumers_properties.py new file mode 100644 index 0000000000..287560c46e --- /dev/null +++ b/policyengine_uk/tests/test_uc_earned_income_consumers_properties.py @@ -0,0 +1,482 @@ +"""Property-based tests for rules that read Universal Credit earned income. + +Three rules outside the UC award test a UC claimant's earned income: + +- the benefit cap earnings exception (UC Regs 2013 reg. 82(1)(a)): earned + income, without the minimum income floor (reg. 82(4)), against 16 hours a + week at the national living wage, in whole pounds a month (reg. 6(1A)(za)); +- the targeted childcare route for UC families (SI 2014/2147 reg. 1(3)): + earned income as in UC Regs Part 6 Chapter 2, before the work allowance, + against 15,400 a year; +- the Kingston upon Thames, Merton, Newham and Westminster council tax + reduction schemes (Default Scheme para. 37): the Secretary of State's + calculation of income, before the work allowance, plus the award. + +Invariants, for any generated population of single people and couples with +earnings, self-employment, pension contributions, voluntary Class 3 and +taxable unearned income: + +1. The benefit cap exception never depends on income tax or NI that is not + on earnings: adding Class 3 or any unearned income to any adult leaves + benefit_cap_earned_income and the income test unchanged, and the + exception too wherever there is a UC award before and after. +2. Differential against the UC means test: benefit cap earned income equals + the UC earned income before the work allowance when no one's minimum + income floor applies, and never exceeds it; and switching every floor off + never changes it (reg. 82(4)). The exception is the income test plus a + UC award (reg. 82(1)), so it never applies to Housing Benefit alone. +3. The threshold is 12 x floor(12.71 x 16 x 52 / 12) in 2026-27 and the same + formula on the national living wage parameter in every year, for every + family. +4. Monotone in earnings: the benefit cap income test is non-decreasing, and + the childcare criterion non-increasing, in any adult's employment income. + A fixed example crosses the childcare limit with a UC award throughout. +5. Differential against the formulas #1986 replaced: the childcare criterion + now implies the old criterion (earned income before the work allowance is + never below earned income after it), and local CTR is never higher. +6. Differential against a reference: for a UC award in the four boroughs, + council_tax_reduction equals the scheme formula recomputed from the + model's own UC maximum amount, award and earned income. + +Invariant 1 holds only while unearned income leaves the person's allowances +alone, as in test_uc_earnings_deductions_properties.py: adjusted net income +stays below the personal allowance taper, no one is old enough for the +married couple's allowance, and no one claims Marriage Allowance. +Self-employment profits are never negative, because a trading loss is netted +against employment income in uc_individual_earned_income but not in the +benefit cap measure. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import * +from policyengine_uk.utils.scenario import Scenario + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +YEARS = [2020, 2024, 2025, 2026] +TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] +LOCAL_AUTHORITIES = ["MERTON", "KINGSTON_UPON_THAMES", "NEWHAM", "WESTMINSTER"] +UNEARNED = [ + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", +] +earnings = st.one_of(st.just(0.0), st.floats(0, 30_000)) +self_employment = st.one_of(st.just(0.0), st.floats(0, 20_000)) +pension = st.one_of(st.just(0.0), st.floats(0, 2_000)) +unearned = st.one_of(st.just(0.0), st.floats(0, 7_000)) +bumps = st.floats(1, 9_000) + + +@st.composite +def families(draw): + adults = [] + for _ in range(draw(st.integers(1, 2))): + adult = dict( + age=draw(st.integers(18, 60)), + employment_income=draw(earnings), + self_employment_income=draw(self_employment), + employee_pension_contributions=draw(pension), + ni_class_3=draw(st.one_of(st.just(0.0), st.floats(0, 950))), + uc_is_in_startup_period=draw(st.booleans()), + ) + for variable in UNEARNED: + adult[variable] = draw(unearned) + adults.append(adult) + return dict( + adults=adults, + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 3)))], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.floats(0, 15_000)), + local_authority=draw(st.sampled_from(LOCAL_AUTHORITIES)), + council_tax=draw(st.floats(500, 3_000)), + ) + + +def situation(units, year, bump=None, earnings_only=False, no_floor=False): + """One simulation holding every family. + + ``bump`` is (family index, adult index, variable, amount) to add, or a + list of them. + ``earnings_only`` drops Class 3 and every kind of unearned income. + ``no_floor`` puts every adult in a start-up period, so no minimum income + floor applies. + """ + bumps = [] if bump is None else [bump] if isinstance(bump, tuple) else bump + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, adult in enumerate(unit["adults"]): + name = f"p{i}_{j}" + # A family has at most two adults: the claimant and partner. + person = { + "would_claim_marriage_allowance": {year: False}, + "is_claimant_or_partner": {year: True}, + } + for variable, value in adult.items(): + if earnings_only and (variable in UNEARNED or variable == "ni_class_3"): + continue + person[variable] = {year: value} + if no_floor: + person["uc_is_in_startup_period"] = {year: True} + for b in bumps: + if b[:2] == (i, j): + variable, amount = b[2], b[3] + person[variable] = {year: adult.get(variable, 0.0) + amount} + people[name] = person + names.append(name) + for k, age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {year: age}} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "would_claim_uc": {year: True}, + "claims_all_entitled_benefits": {year: True}, + } + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "country": {year: "ENGLAND"}, + "region": {year: "LONDON"}, + "local_authority": {year: unit["local_authority"]}, + "council_tax": {year: unit["council_tax"]}, + "savings": {year: 0.0}, + } + return {"people": people, "benunits": benunits, "households": households} + + +BENUNIT_VARIABLES = [ + "benefit_cap_earned_income", + "benefit_cap_earnings_threshold", + "is_benefit_cap_exempt_earnings", + "uc_benefit_cap_earnings_threshold_met", + "universal_credit_pre_benefit_cap", + "uc_earned_income_before_work_allowance", + "uc_earned_income", + "uc_unearned_income", + "uc_maximum_amount", + "universal_credit", + "meets_universal_credit_criteria_for_targeted_childcare_entitlement", +] +# Each family is one benefit unit in its own household, so household values +# line up with benefit unit values. +HOUSEHOLD_VARIABLES = ["council_tax_reduction", "council_tax"] + + +def calculate(units, year, scenario=None, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs), scenario=scenario) + values = {} + for v in BENUNIT_VARIABLES + HOUSEHOLD_VARIABLES: + values[v] = np.asarray(sim.calculate(v, year)) + values["floor_applies"] = np.asarray( + sim.map_result( + sim.calculate("uc_mif_applies", year), "person", "benunit", "any" + ) + ) + return values + + +@st.composite +def bumped(draw, variables): + units = draw(st.lists(families(), min_size=1, max_size=8)) + i = draw(st.integers(0, len(units) - 1)) + j = draw(st.integers(0, len(units[i]["adults"]) - 1)) + return units, (i, j, draw(st.sampled_from(variables)), draw(bumps)) + + +# Annual benefit cap earnings thresholds (12 x the monthly amount). +THRESHOLDS = {2020: 7_248, 2024: 9_516, 2025: 10_152, 2026: 10_572} + + +@st.composite +def near_threshold_families(draw, year): + """A family whose one earner is just either side of the threshold, so + that any deduction wrongly taken from earned income moves the + exception.""" + earner = dict( + age=draw(st.integers(18, 60)), + employment_income=THRESHOLDS[year] + draw(st.floats(-300, 900)), + ) + adults = [earner] + if draw(st.booleans()): + adults.append(dict(age=draw(st.integers(18, 60)))) + return dict( + adults=adults, + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 4)))], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.floats(0, 15_000)), + local_authority=draw(st.sampled_from(LOCAL_AUTHORITIES)), + council_tax=draw(st.floats(500, 3_000)), + ) + + +@PROPERTY_SETTINGS +@given(year=st.sampled_from(YEARS), data=st.data()) +def test_benefit_cap_exception_ignores_tax_not_on_earnings(year, data): + units = data.draw( + st.lists( + st.one_of(families(), near_threshold_families(year)), + min_size=1, + max_size=8, + ) + ) + # One adult in every family gets more Class 3 (up to a year's worth) or + # unearned income large enough to be taxed, so a wrongly deducted charge + # would move an earner near the threshold across it. + bump = [] + for i, unit in enumerate(units): + j = data.draw(st.integers(0, len(unit["adults"]) - 1)) + variable = data.draw(st.sampled_from(UNEARNED + ["ni_class_3"])) + if variable == "ni_class_3": + amount = data.draw(st.floats(500, 950)) + else: + amount = data.draw(st.floats(2_000, 9_000)) + bump.append((i, j, variable, amount)) + low = calculate(units, year) + high = calculate(units, year, bump=bump) + np.testing.assert_allclose( + high["benefit_cap_earned_income"], + low["benefit_cap_earned_income"], + atol=0.01, + err_msg=f"{bump} {units}", + ) + np.testing.assert_array_equal( + high["uc_benefit_cap_earnings_threshold_met"], + low["uc_benefit_cap_earnings_threshold_met"], + err_msg=f"{bump} {units}", + ) + # Unearned income can end the UC award, and with it the exception; where + # there is an award either way, the exception does not move. + on_uc = (low["universal_credit_pre_benefit_cap"] > 0) & ( + high["universal_credit_pre_benefit_cap"] > 0 + ) + np.testing.assert_array_equal( + high["is_benefit_cap_exempt_earnings"][on_uc], + low["is_benefit_cap_exempt_earnings"][on_uc], + err_msg=f"{bump} {units}", + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=8), + year=st.sampled_from(YEARS), +) +def test_benefit_cap_earned_income_matches_uc_without_the_floor(units, year): + v = calculate(units, year) + cap = v["benefit_cap_earned_income"] + uc = v["uc_earned_income_before_work_allowance"] + no_floor = ~v["floor_applies"] + np.testing.assert_allclose(cap[no_floor], uc[no_floor], atol=0.01, err_msg=units) + # The exception is the income test plus an award of UC (reg. 82(1)). + np.testing.assert_array_equal( + v["is_benefit_cap_exempt_earnings"], + v["uc_benefit_cap_earnings_threshold_met"] + & (v["universal_credit_pre_benefit_cap"] > 0), + err_msg=str(units), + ) + assert np.all(cap <= uc + 0.01), units + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=8), + year=st.sampled_from(YEARS), +) +def test_minimum_income_floor_never_counts_for_the_benefit_cap(units, year): + with_floor = calculate(units, year) + without_floor = calculate(units, year, no_floor=True) + np.testing.assert_allclose( + with_floor["benefit_cap_earned_income"], + without_floor["benefit_cap_earned_income"], + atol=0.01, + err_msg=str(units), + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=8), + year=st.sampled_from(YEARS), +) +def test_benefit_cap_threshold_is_16_hours_at_the_living_wage(units, year): + v = calculate(units, year) + nlw = Simulation(situation={"people": {"p": {"age": {year: 30}}}}).calculate( + "minimum_wage", year + )[0] + expected = 12 * np.floor(nlw * 16 * 52 / 12) + np.testing.assert_allclose(v["benefit_cap_earnings_threshold"], expected) + if year == 2026: + np.testing.assert_allclose(v["benefit_cap_earnings_threshold"], 10_572) + + +CHILDCARE_LIMIT_CROSSING = ( + [ + dict( + adults=[dict(age=30, employment_income=16_500.0)], + children=[2], + tenure="RENT_FROM_COUNCIL", + rent=9_600.0, + local_authority="MERTON", + council_tax=1_800.0, + ) + ], + (0, 0, "employment_income", 1.0), +) + + +@PROPERTY_SETTINGS +@given(case=bumped(["employment_income"]), year=st.sampled_from(YEARS)) +# Earned income goes from 15,399.60 to 15,400.32, across the 15,400 limit, +# with a UC award throughout. +@example(case=CHILDCARE_LIMIT_CROSSING, year=2026) +def test_monotone_in_earnings(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump) + # More earnings never take away the benefit cap income test... + assert np.all( + high["uc_benefit_cap_earnings_threshold_met"] + >= low["uc_benefit_cap_earnings_threshold_met"] + ), (bump, units) + # ...and never bring a family within the childcare earnings limit. Where + # the minimum income floor applies, the floor stands in for gross + # earnings while the person's actual tax and NI are still deducted, so + # more employment income can lower earned income; #1973 nets the floor + # instead. Those families are left out here. + criterion = "meets_universal_credit_criteria_for_targeted_childcare_entitlement" + no_floor = ~low["floor_applies"] & ~high["floor_applies"] + assert np.all(high[criterion][no_floor] <= low[criterion][no_floor]), ( + bump, + units, + ) + + +def _use_formulas_before_1986(simulation): + class meets_universal_credit_criteria_for_targeted_childcare_entitlement(Variable): + value_type = bool + entity = BenUnit + label = "UC childcare criterion as computed before #1986" + definition_period = YEAR + + def formula(benunit, period, parameters): + p = parameters(period).gov.dfe.targeted_childcare_entitlement + uc = benunit("universal_credit", period) + earned_income = benunit("uc_earned_income", period) + return (uc > 0) & (earned_income <= p.income_limit.universal_credit) + + class uc_earned_income_before_work_allowance(Variable): + # Only the local CTR schemes read this variable apart from + # uc_earned_income and the childcare criterion, so swapping it for the + # after-work-allowance amount reproduces their old income figure. + value_type = float + entity = BenUnit + label = "UC earned income as local CTR read it before #1986" + definition_period = YEAR + unit = GBP + + def formula(benunit, period, parameters): + earned_income = add(benunit, period, ["uc_individual_earned_income"]) + work_allowance = benunit("uc_work_allowance", period) + return max_(0, earned_income - work_allowance) + + class uc_earned_income(Variable): + value_type = float + entity = BenUnit + label = "UC earned income (after deductions and work allowance)" + definition_period = YEAR + unit = GBP + + def formula(benunit, period, parameters): + earned_income = add(benunit, period, ["uc_individual_earned_income"]) + work_allowance = benunit("uc_work_allowance", period) + return max_(0, earned_income - work_allowance) + + system = simulation.tax_benefit_system + system.update_variable( + meets_universal_credit_criteria_for_targeted_childcare_entitlement + ) + system.update_variable(uc_earned_income) + system.update_variable(uc_earned_income_before_work_allowance) + + +BEFORE_1986 = Scenario(simulation_modifier=_use_formulas_before_1986) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=8), + year=st.sampled_from([2025, 2026]), +) +def test_fix_only_tightens_childcare_and_ctr(units, year): + after = calculate(units, year) + before = calculate(units, year, scenario=BEFORE_1986) + # The UC award itself is unchanged. + np.testing.assert_allclose( + after["universal_credit"], before["universal_credit"], atol=0.01 + ) + assert np.all( + after["uc_earned_income_before_work_allowance"] + >= after["uc_earned_income"] - 0.01 + ), units + criterion = "meets_universal_credit_criteria_for_targeted_childcare_entitlement" + assert np.all(after[criterion] <= before[criterion]), units + assert np.all( + after["council_tax_reduction"] <= before["council_tax_reduction"] + 0.01 + ), units + + +# Scheme parameters: maximum support rate and taper (withdrawal rate). +SCHEMES = { + "MERTON": "merton", + "KINGSTON_UPON_THAMES": "kingston_upon_thames", + "NEWHAM": "newham", + "WESTMINSTER": "westminster", +} + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=8), + year=st.sampled_from([2025, 2026]), +) +def test_local_ctr_matches_the_scheme_formula(units, year): + # Single-adult, no non-dependant households: no non-dependant + # deductions, so CTR for a UC award is + # max(0, liability x maximum support rate + # - taper x max(0, earned income before the work allowance + # + unearned income + UC - UC maximum amount)). + v = calculate(units, year) + system = Simulation(situation={"people": {"p": {"age": {year: 30}}}}) + parameters = system.tax_benefit_system.parameters(year).gov.local_authorities + for k, unit in enumerate(units): + if v["universal_credit"][k] <= 0: + continue + ctr = getattr( + parameters, SCHEMES[unit["local_authority"]] + ).council_tax_reduction + income = ( + v["uc_earned_income_before_work_allowance"][k] + + v["uc_unearned_income"][k] + + v["universal_credit"][k] + ) + excess = max(0.0, income - v["uc_maximum_amount"][k]) + expected = max( + 0.0, + v["council_tax"][k] * ctr.maximum_support_rate + - excess * ctr.means_test.withdrawal_rate, + ) + np.testing.assert_allclose( + v["council_tax_reduction"][k], expected, atol=0.01, err_msg=str(unit) + ) diff --git a/policyengine_uk/tests/test_uc_earnings_deductions_properties.py b/policyengine_uk/tests/test_uc_earnings_deductions_properties.py new file mode 100644 index 0000000000..5dae910538 --- /dev/null +++ b/policyengine_uk/tests/test_uc_earnings_deductions_properties.py @@ -0,0 +1,311 @@ +"""Property-based tests for the Universal Credit deductions from earnings. + +UC Regs 2013 reg. 55(5) and reg. 57(2) deduct from a person's earnings their +own relievable pension contributions, the income tax and National Insurance +they pay in respect of their employment or trade, and payroll giving (reg. +55(5)(c), which the model does not have). The model takes +earnings as the lowest slice of the person's non-savings income, after their +allowances, with other income above it (ITA 2007 s. 16 for savings and +dividends). + +Invariants, for any generated population of single people and couples with +earnings, self-employment and every kind of taxable unearned income: + +1. Unearned income never changes earned income: giving any adult more State + Pension, private pension, property, savings or dividend income leaves + every person's uc_individual_earned_income, and so uc_earned_income, + unchanged. +2. Monotone: the UC award, before and after the benefit cap, is + non-increasing in each kind of unearned income for each adult. +3. Differential against the tax engine: the income tax deducted equals the + income tax the same person pays when their earnings are their only + income, and the NI deducted equals their NI. +4. Differential against the formula this replaced: earned income is never + lower, and UC never higher, than when the whole benefit unit's tax was + deducted, because only deductions were removed. + +Invariants 1-3 hold only while unearned income leaves the person's +allowances alone, so the generated incomes keep adjusted net income below +the personal allowance taper (100,000) and no one claims Marriage Allowance +(see test_uc_state_pension_properties.py for the Marriage Allowance +deviation). Tax reductions are covered: adults born before 6 April 1935 can +have a married couple's allowance, which comes off the tax on earnings +first. Invariant 4 runs with Marriage Allowance claimed. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import * +from policyengine_uk.utils.scenario import Scenario + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2020 has the temporary UC uplift; 2026 current rates; 2027 the new property +# and savings rates (Finance Act 2026). +YEARS = [2020, 2026, 2027] +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", +] +REGIONS = ["LONDON", "NORTH_EAST", "WALES", "SCOTLAND"] +UNEARNED = [ + "state_pension", + "private_pension_income", + "property_income", + "savings_interest_income", + "dividend_income", +] +# The married couple's allowance needs a birth before 6 April 1935, which +# every adult aged 93 or over meets in each simulated year. +MCA_BIRTH_YEAR = 1934 +PENSION_AGE = st.integers(67, 96) +MCA_AGE = st.integers(93, 96) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single": [WORKING_AGE], + "couple": [WORKING_AGE, WORKING_AGE], + "mixed_age": [PENSION_AGE, WORKING_AGE], + # An older partner who works and has a married couple's allowance, so a + # tax reduction applies to someone with earnings. + "mixed_age_with_tax_reduction": [MCA_AGE, WORKING_AGE], +} +# Per adult: earnings up to 50,000 and unearned income up to 5 x 7,000, +# plus a bump up to 9,000, keeps adjusted net income under 100,000. +earnings = st.one_of(st.just(0.0), st.floats(0, 30_000)) +self_employment = st.one_of(st.just(0.0), st.floats(0, 20_000)) +unearned = st.one_of(st.just(0.0), st.floats(0, 7_000)) +bumps = st.floats(1, 9_000) +UC_VARIABLES = [ + "universal_credit", + "universal_credit_pre_benefit_cap", + "uc_earned_income", +] +PERSON_VARIABLES = [ + "uc_individual_earned_income", + "uc_income_tax_on_earnings", + "uc_national_insurance_on_earnings", + "income_tax", + "national_insurance", +] + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(list(SHAPES))) + adults = [] + for age in [draw(age) for age in SHAPES[shape]]: + has_tax_reduction = shape == "mixed_age_with_tax_reduction" and age >= 93 + adult = dict( + age=age, + employment_income=( + draw(st.floats(15_000, 30_000)) if has_tax_reduction else draw(earnings) + ), + self_employment_income=draw(self_employment), + # Some self-employed are in a start-up period, so the minimum + # income floor does not apply. + uc_is_in_startup_period=draw(st.booleans()), + ) + for variable in UNEARNED: + if variable == "state_pension" and age < 67: + continue + adult[variable] = draw(unearned) + # Used only where the adult is old enough in the simulated year. + adult["married_couples_allowance"] = ( + draw(st.floats(3_000, 12_000)) + if has_tax_reduction + else draw(st.one_of(st.just(0.0), st.floats(0, 12_000))) + ) + adults.append(adult) + return dict( + adults=adults, + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.floats(0, 15_000)), + region=draw(st.sampled_from(REGIONS)), + ) + + +def situation(units, year, bump=None, earnings_only=False, marriage_allowance=False): + """One simulation holding every family. + + ``bump`` is (family index, adult index, variable, amount) to add. + ``earnings_only`` drops every kind of unearned income. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, adult in enumerate(unit["adults"]): + name = f"p{i}_{j}" + # The generated adults are the claimant and partner; say so, so the + # claimant-or-partner presumption (a member under 20 and much + # younger is the head's child) does not apply. Children get False. + person = { + "state_pension": {year: 0.0}, + "is_claimant_or_partner": {year: True}, + } + for variable, value in adult.items(): + if earnings_only and variable in UNEARNED: + continue + if variable == "married_couples_allowance" and ( + year - adult["age"] > MCA_BIRTH_YEAR + ): + continue + person[variable] = {year: value} + if bump is not None and bump[:2] == (i, j) and not earnings_only: + variable, amount = bump[2], bump[3] + person[variable] = {year: adult.get(variable, 0.0) + amount} + if not marriage_allowance: + person["would_claim_marriage_allowance"] = {year: False} + people[name] = person + names.append(name) + for k, age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "region": {year: unit["region"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, scenario=None, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs), scenario=scenario) + values = {v: np.asarray(sim.calculate(v, year)) for v in UC_VARIABLES} + for v in PERSON_VARIABLES: + values[v] = np.asarray(sim.calculate(v, year)) + values["is_adult_person"] = np.asarray(sim.calculate("age", year)) >= 18 + return values + + +@st.composite +def bumped(draw): + units = draw(st.lists(families(), min_size=1, max_size=10)) + i = draw(st.integers(0, len(units) - 1)) + j = draw(st.integers(0, len(units[i]["adults"]) - 1)) + variables = UNEARNED if units[i]["adults"][j]["age"] >= 67 else UNEARNED[1:] + return units, (i, j, draw(st.sampled_from(variables)), draw(bumps)) + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +@example( + # An earner just above the personal allowance gains a pound of pension. + # Taking earnings as the top slice instead of the bottom one would tax + # that pound against earnings. + case=( + [ + dict( + adults=[ + dict( + age=18, + employment_income=12_571.0, + self_employment_income=0.0, + uc_is_in_startup_period=False, + married_couples_allowance=0.0, + ) + ], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=0.0, + region="LONDON", + ) + ], + (0, 0, "private_pension_income", 1.0), + ), + year=2026, +) +def test_unearned_income_never_changes_earned_income(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump) + for variable in ["uc_individual_earned_income", "uc_earned_income"]: + np.testing.assert_allclose( + high[variable], low[variable], atol=0.01, err_msg=f"{bump} {units}" + ) + + +@PROPERTY_SETTINGS +@given(case=bumped(), year=st.sampled_from(YEARS)) +def test_uc_is_non_increasing_in_unearned_income(case, year): + units, bump = case + low = calculate(units, year) + high = calculate(units, year, bump=bump) + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(high[variable] <= low[variable] + 0.01), (variable, bump, units) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=10), + year=st.sampled_from(YEARS), +) +def test_deductions_equal_tax_and_ni_on_earnings_alone(units, year): + full = calculate(units, year) + alone = calculate(units, year, earnings_only=True) + adult = full["is_adult_person"] + np.testing.assert_allclose( + full["uc_income_tax_on_earnings"][adult], + alone["income_tax"][adult], + atol=0.01, + err_msg=str(units), + ) + np.testing.assert_allclose( + full["uc_national_insurance_on_earnings"][adult], + alone["national_insurance"][adult], + atol=0.01, + err_msg=str(units), + ) + # Never more than the person's own tax and NI. + assert np.all(full["uc_income_tax_on_earnings"] <= full["income_tax"] + 0.01) + assert np.all( + full["uc_national_insurance_on_earnings"] <= full["national_insurance"] + 0.01 + ) + + +def _use_formula_before_1942(simulation): + class uc_earned_income(Variable): + value_type = float + entity = BenUnit + label = "UC earned income as computed before #1942" + definition_period = YEAR + unit = GBP + + def formula(benunit, period, parameters): + gross = add(benunit, period, ["uc_mif_capped_earned_income"]) + disregards = add( + benunit, + period, + ["uc_work_allowance", "benunit_tax", "pension_contributions"], + ) + return max_(0, gross - disregards) + + simulation.tax_benefit_system.update_variable(uc_earned_income) + + +BEFORE_1942 = Scenario(simulation_modifier=_use_formula_before_1942) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=10), + year=st.sampled_from(YEARS), +) +def test_fix_only_removes_deductions(units, year): + after = calculate(units, year, marriage_allowance=True) + before = calculate(units, year, scenario=BEFORE_1942, marriage_allowance=True) + assert np.all(after["uc_earned_income"] >= before["uc_earned_income"] - 0.01), units + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(after[variable] <= before[variable] + 0.01), (variable, units) diff --git a/policyengine_uk/tests/test_uc_income_from_capital_properties.py b/policyengine_uk/tests/test_uc_income_from_capital_properties.py index 0bc207f5aa..8b532df799 100644 --- a/policyengine_uk/tests/test_uc_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_uc_income_from_capital_properties.py @@ -17,18 +17,21 @@ and rent the family receives. So capital yield enters once, as tariff income. None of those three is on the list in any year. 2. Invariance: interest, dividends and rent change nothing in unearned income, - tariff income, eligibility or the maximum amount. For families without - earnings they change nothing in the award either. + tariff income, eligibility, the maximum amount or the award, whatever the + family earns, because tax on them never comes off earnings (reg. 55(5)(b), + reg. 57(2) step 3). 3. Monotone in capital: adding capital to any countable source never raises the award, before or after the benefit cap, and never lowers tariff income. -Invariant 2's award clause is restricted to families without earnings because -the model deducts the whole benefit unit's income tax from its earnings -(PolicyEngine/policyengine-uk#1942), so tax on dividends raises the award of a -working family. Reg. 55(5)(b) and reg. 57 step 3 allow only tax paid in respect -of the employment or trade. The strict xfail below pins that case and will -flip when #1942 is fixed; widen invariant 2 to all families then. +Invariant 2's award clause runs with Marriage Allowance switched off. The +model books it on the recipient as the transferor's unused personal allowance +(PolicyEngine/policyengine-uk#1947), so capital income that uses up one +partner's allowance raises the other partner's tax on earnings. The clause +also compares only families whose personal allowances the income leaves +unchanged: above 100,000 of adjusted net income the allowance tapers (ITA 2007 +s. 35), which raises the tax a person pays on their earnings and so changes +the deduction from them (see test_uc_earnings_deductions_properties.py). """ import numpy as np @@ -83,8 +86,8 @@ @st.composite -def families(draw, with_earnings=True): - earnings = draw(st.one_of(st.just(0.0), money)) if with_earnings else 0.0 +def families(draw): + earnings = draw(st.one_of(st.just(0.0), money)) return dict( ages=[draw(WORKING_AGE) for _ in range(draw(st.integers(1, 2)))], children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], @@ -103,19 +106,27 @@ def families(draw, with_earnings=True): ) -def situation(units, year, income_scale=1.0, capital_bump=None): +def situation( + units, year, income_scale=1.0, capital_bump=None, marriage_allowance=True +): """Build one simulation holding every family. ``income_scale`` multiplies each family's interest, dividends and rent; ``capital_bump`` is an optional (source, amount) added to every family's - capital. + capital. With ``marriage_allowance=False`` no one claims Marriage + Allowance. """ people, benunits, households = {}, {}, {} for i, unit in enumerate(units): names = [] for j, age in enumerate(unit["ages"]): name = f"p{i}_{j}" - person = {"age": {year: age}} + # The generated adults are the claimant and partner; say so, so the + # claimant-or-partner presumption (a member under 20 and much + # younger is the head's child) does not apply. Children get False. + person = {"age": {year: age}, "is_claimant_or_partner": {year: True}} + if not marriage_allowance: + person["would_claim_marriage_allowance"] = {year: False} if j == 0: person["employment_income"] = {year: unit["earnings"]} person["private_pension_income"] = { @@ -128,7 +139,7 @@ def situation(units, year, income_scale=1.0, capital_bump=None): names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunit = {"members": names} reported = unit["reported_capital"] @@ -160,6 +171,9 @@ def listed_sources(sim, year): def calculate(units, year, **kwargs): sim = Simulation(situation=situation(units, year, **kwargs)) values = {v: np.asarray(sim.calculate(v, year)) for v in UC_VARIABLES} + values["personal_allowances"] = np.asarray( + sim.calculate("personal_allowance", year, map_to="benunit") + ) sources = listed_sources(sim, year) # No actual income from capital is on the list in any year. assert not set(CAPITAL_INCOME) & set(sources), sources @@ -221,16 +235,24 @@ def test_interest_dividends_and_rent_do_not_enter_the_means_test(units, scale, y @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), scale=st.sampled_from([0.0, 0.5, 3.0]), year=st.sampled_from(YEARS), ) def test_interest_dividends_and_rent_leave_the_award_unchanged(units, scale, year): - base = calculate(units, year) - scaled = calculate(units, year, income_scale=scale) + base = calculate(units, year, marriage_allowance=False) + scaled = calculate(units, year, income_scale=scale, marriage_allowance=False) + # Only one adult receives the capital income, so the unit's summed + # personal allowances are unchanged exactly when that adult's is. + unchanged = np.isclose( + scaled["personal_allowances"], base["personal_allowances"], atol=0.01 + ) for variable in UC_VARIABLES: np.testing.assert_allclose( - scaled[variable], base[variable], atol=0.01, err_msg=f"{variable}: {units}" + scaled[variable][unchanged], + base[variable][unchanged], + atol=0.01, + err_msg=f"{variable}: {units}", ) @@ -259,14 +281,6 @@ def test_uc_is_non_increasing_in_capital(units, source, bump, year): ), units -@pytest.mark.xfail( - strict=True, - raises=AssertionError, - reason=( - "PolicyEngine/policyengine-uk#1942: uc_earned_income deducts the benefit " - "unit's whole income tax, including tax on dividends, from earnings" - ), -) def test_tax_on_dividends_does_not_raise_a_working_familys_award(): # 2026: single claimant aged 30 earning 10,000, under the personal # allowance and the NI primary threshold, so no tax or NI is paid in diff --git a/policyengine_uk/tests/test_uc_state_pension_properties.py b/policyengine_uk/tests/test_uc_state_pension_properties.py index 70ee6af404..0afad1e6ff 100644 --- a/policyengine_uk/tests/test_uc_state_pension_properties.py +++ b/policyengine_uk/tests/test_uc_state_pension_properties.py @@ -12,23 +12,25 @@ 1. Monotone: the UC award, before and after the benefit cap, is non-increasing in State Pension, and more State Pension adds exactly that much to unearned income without changing the maximum amount. -2. Pound for pound: with no earnings in the family, raising State Pension by - d lowers the award before the benefit cap by exactly min(d, award). +2. Pound for pound: raising State Pension by d lowers the award before the + benefit cap by exactly min(d, award), whatever the partner earns, because + tax on State Pension never comes off earnings (reg. 55(5)(b), reg. 57(2) + step 3). 3. Equivalence: UC with State Pension x equals UC with the same x of private pension income received by the same person instead (both are retirement pension income, taxed the same way). 4. Property income is outside reg. 66(1)'s list of unearned income, at any - capital level. With no earnings, replacing State Pension x with ordinary - property income leaves the award equal to the no-income case, while - State Pension reduces the pre-cap award by min(x, award). Any tariff + capital level. Replacing State Pension x with ordinary property income + leaves the award equal to the no-income case, while State Pension reduces + the pre-cap award by min(x, award), whatever the partner earns. Any tariff income from capital remains the same in all three cases. -Invariants 2 and 4 are restricted to families without earnings because the -model deducts the whole benefit unit's income tax from its earnings -(PolicyEngine/policyengine-uk#1942), so tax on State Pension reduces earned -income. Reg. 55(5)(b) and reg. 57 step 3 allow only tax paid in respect of -the employment or trade. The strict xfail below pins that case and will flip -when #1942 is fixed; widen invariants 2 and 4 to all families then. +Marriage Allowance is switched off for invariants 2, 3 and 4. The model gives +the recipient min(partner's unused personal allowance, 10% of the personal +allowance) instead of the fixed transferable amount in ITA 2007 s. 55B(4)-(6), +so State Pension or property income that uses up the pensioner's unused +allowance raises the earning partner's tax on earnings. The strict xfail at +the end pins that deviation (PolicyEngine/policyengine-uk#1947). """ import numpy as np @@ -74,12 +76,12 @@ @st.composite -def families(draw, with_earnings=True): +def families(draw): # Weight towards mixed-age couples, the only shape UC can reach. shape = draw( st.sampled_from(["mixed_age", "mixed_age", "single_pension", "couple_pension"]) ) - earnings = draw(st.one_of(st.just(0.0), money)) if with_earnings else 0.0 + earnings = draw(st.one_of(st.just(0.0), money)) return dict( ages=[draw(age) for age in SHAPES[shape]], children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], @@ -91,21 +93,38 @@ def families(draw, with_earnings=True): ) -def situation(units, year, income_variable="state_pension", pension_bump=0.0): +def situation( + units, + year, + income_variable="state_pension", + pension_bump=0.0, + marriage_allowance=True, +): """Build one simulation holding every family. The eldest adult receives the family's State Pension (plus ``pension_bump``) under ``income_variable``; when that is not ``state_pension``, their State Pension is set to zero so the same amount arrives as the other income instead. A working-age partner receives the - family's earnings. + family's earnings. With ``marriage_allowance=False`` no one claims + Marriage Allowance. """ people, benunits, households = {}, {}, {} for i, unit in enumerate(units): names = [] for j, age in enumerate(unit["ages"]): name = f"p{i}_{j}" - person = {"age": {year: age}, "state_pension": {year: 0.0}} + # The generated adults are the claimant and partner; say so, so the + # claimant-or-partner presumption (a member under 20 and much + # younger is the head's child) does not turn a 67-and-18 couple + # into a parent and child. Children get False below. + person = { + "age": {year: age}, + "state_pension": {year: 0.0}, + "is_claimant_or_partner": {year: True}, + } + if not marriage_allowance: + person["would_claim_marriage_allowance"] = {year: False} if j == 0: amount = unit["state_pension"] + pension_bump person[income_variable] = {year: amount} @@ -115,7 +134,7 @@ def situation(units, year, income_variable="state_pension", pension_bump=0.0): names.append(name) for k, age in enumerate(unit["children"]): name = f"c{i}_{k}" - people[name] = {"age": {year: age}} + people[name] = {"age": {year: age}, "is_claimant_or_partner": {year: False}} names.append(name) benunits[f"b{i}"] = {"members": names} households[f"h{i}"] = { @@ -168,13 +187,34 @@ def test_uc_is_non_increasing_in_state_pension(units, bump, year): @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), year=st.sampled_from(YEARS), ) +@example( + # A partner just above the personal allowance and a pensioner just above + # it too, so a pound of State Pension is taxed while the partner's + # earnings reduce UC. Deducting the pensioner's tax from the partner's + # earnings (the formula before #1942) breaks pound for pound here. + units=[ + dict( + ages=[67, 18], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=12_000.0, + savings=0.0, + earnings=12_571.0, + state_pension=12_571.0, + ) + ], + bump=1.0, + year=2026, +) def test_uc_falls_pound_for_pound_in_state_pension(units, bump, year): - low = calculate(units, year) - high = calculate(units, year, pension_bump=bump) + # Earnings in the family change nothing: tax on State Pension is never + # deducted from anyone's earnings (reg. 55(5)(b), reg. 57(2) step 3). + low = calculate(units, year, marriage_allowance=False) + high = calculate(units, year, pension_bump=bump, marriage_allowance=False) award = low["universal_credit_pre_benefit_cap"] np.testing.assert_allclose( high["universal_credit_pre_benefit_cap"], @@ -199,7 +239,7 @@ def test_state_pension_counts_like_private_pension(units, year): @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), year=st.sampled_from(YEARS), ) @example( @@ -217,9 +257,16 @@ def test_state_pension_counts_like_private_pension(units, year): year=2026, ) def test_property_income_is_excluded_while_state_pension_counts(units, year): - pension = calculate(units, year) - property_income = calculate(units, year, income_variable="property_income") - without_income = calculate([{**unit, "state_pension": 0.0} for unit in units], year) + # Neither income's tax comes off the partner's earnings (reg. 55(5)(b)), + # so this holds with earnings in the family too. + kwargs = dict(marriage_allowance=False) + pension = calculate(units, year, **kwargs) + property_income = calculate( + units, year, income_variable="property_income", **kwargs + ) + without_income = calculate( + [{**unit, "state_pension": 0.0} for unit in units], year, **kwargs + ) assert_same(property_income, without_income, str(units)) pensions = np.array([unit["state_pension"] for unit in units]) np.testing.assert_allclose( @@ -236,14 +283,6 @@ def test_property_income_is_excluded_while_state_pension_counts(units, year): ) -@pytest.mark.xfail( - strict=True, - reason=( - "PolicyEngine/policyengine-uk#1942: uc_earned_income deducts the whole " - "benefit unit's income tax, including the pensioner's tax on State " - "Pension, from the partner's earnings" - ), -) def test_tax_on_state_pension_does_not_reduce_partners_earned_income(): # 2026: pensioner aged 70 with State Pension 16,000 pays income tax of # (16,000 - 12,570) x 20% = 686. The partner aged 45 earns 13,000 and pays @@ -264,3 +303,35 @@ def test_tax_on_state_pension_does_not_reduce_partners_earned_income(): values = calculate([unit], 2026) assert values["uc_earned_income"][0] == pytest.approx(12_879.60, abs=0.01) assert values["universal_credit"][0] == pytest.approx(4_919.86, abs=0.01) + + +@pytest.mark.xfail( + strict=True, + reason=( + "PolicyEngine/policyengine-uk#1947: Marriage Allowance is booked on " + "the recipient as the transferor's unused personal allowance, so the " + "transferor's State Pension raises the recipient's tax on earnings" + ), +) +def test_state_pension_does_not_change_partners_marriage_allowance(): + # 2026: pensioner aged 70, partner aged 45 earning 20,000, council rent + # 20,000. Once the pensioner elects, ITA 2007 s. 55B gives the partner a + # fixed reduction of 20% x 1,260 = 252 and cuts the pensioner's own + # allowance by 1,260 (s. 55B(6)); the pensioner's State Pension of 12,000 + # keeps them within the basic rate, so they can elect (s. 55C(1)(c)). + # Partner's tax on earnings = (20,000 - 12,570) x 20% - 252 = 1,234 and + # NI (20,000 - 12,569.96) x 8% = 594.40, so earned income = 18,171.60 + # at any State Pension up to the pensioner's election limit. + # UC = 8,003.64 + 20,000 - (0.55 x 18,171.60 + 12,000) = 6,009.26. + unit = dict( + ages=[70, 45], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=20_000.0, + savings=0.0, + earnings=20_000.0, + state_pension=12_000.0, + ) + values = calculate([unit], 2026) + assert values["uc_earned_income"][0] == pytest.approx(18_171.60, abs=0.01) + assert values["universal_credit"][0] == pytest.approx(6_009.26, abs=0.01) diff --git a/policyengine_uk/tests/test_uc_unearned_benefits_properties.py b/policyengine_uk/tests/test_uc_unearned_benefits_properties.py index e67cde0319..78a59e33a0 100644 --- a/policyengine_uk/tests/test_uc_unearned_benefits_properties.py +++ b/policyengine_uk/tests/test_uc_unearned_benefits_properties.py @@ -17,23 +17,20 @@ families that already received the benefit; starting to receive contributory ESA or industrial injuries benefit can lift the cap, because receipt exempts the family from it. -2. Pound for pound: with no earnings in the family, raising one of the - benefits by d lowers the award before the benefit cap by exactly - min(counted increase, award). -3. Equivalence: with no earnings in the family, UC before the benefit cap - with x of the benefit equals UC with the counted amount of private pension - received by the same person instead. For carer support payment both - families keep the carer's caring hours, so both get the carer element. - -Invariants 2 and 3, and invariant 1 for carer support payment, are -restricted to families without earnings because the model deducts the whole -benefit unit's income tax from its earnings -(PolicyEngine/policyengine-uk#1942), so tax on a taxable benefit or pension -reduces earned income. Carer support payment is taxable, so above the cap, -where more of it adds nothing to unearned income, the carer's extra tax -lowers the partner's earned income and raises the award. The strict xfail -below pins that case and will flip when #1942 is fixed; widen these -invariants to all families then. They compare the award before the benefit cap because +2. Pound for pound: raising one of the benefits by d lowers the award before + the benefit cap by exactly min(counted increase, award), whatever the + partner earns, because tax on a benefit never comes off anyone's earnings + (reg. 55(5)(b), reg. 57(2) step 3). +3. Equivalence: UC before the benefit cap with x of the benefit equals UC with + the counted amount of private pension received by the same person instead. + For carer support payment both families keep the carer's caring hours, so + both get the carer element. + +Marriage Allowance is switched off for invariants 2 and 3, and for invariant +1 for carer support payment. The model books it on the recipient as the +transferor's unused personal allowance (PolicyEngine/policyengine-uk#1947), +so a taxable benefit that uses up the first adult's allowance raises the +earning partner's tax on earnings. Invariants 2 and 3 compare the award before the benefit cap because contributory ESA and industrial injuries benefit trigger benefit cap exemptions and ESA counts towards the cap, which private pension does not. """ @@ -90,8 +87,8 @@ @st.composite -def families(draw, with_earnings=True): - earnings = draw(st.one_of(st.just(0.0), money)) if with_earnings else 0.0 +def families(draw): + earnings = draw(st.one_of(st.just(0.0), money)) return dict( ages=[draw(WORKING_AGE) for _ in range(draw(st.integers(1, 2)))], children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], @@ -103,14 +100,17 @@ def families(draw, with_earnings=True): ) -def situation(units, year, income_variable, bump=0.0, carer=False): +def situation( + units, year, income_variable, bump=0.0, carer=False, marriage_allowance=True +): """Build one simulation holding every family. The first adult receives the family's ``amount`` (plus ``bump``) under ``income_variable``; a partner receives the family's earnings. With ``carer``, the first adult cares for 40 hours a week in Scotland and has carer support payment set explicitly (zero unless it is the income - variable), so the carer element applies whatever the income. + variable), so the carer element applies whatever the income. With + ``marriage_allowance=False`` no one claims Marriage Allowance. """ people, benunits, households = {}, {}, {} for i, unit in enumerate(units): @@ -122,6 +122,8 @@ def situation(units, year, income_variable, bump=0.0, carer=False): # head's child) does not apply. Setting it for anyone makes it an # input for everyone, so children get False below. person = {"age": {year: age}, "is_claimant_or_partner": {year: True}} + if not marriage_allowance: + person["would_claim_marriage_allowance"] = {year: False} if j == 0: if carer: person["care_hours"] = {year: 40} @@ -207,13 +209,14 @@ def test_uc_is_non_increasing_in_each_benefit(units, benefit, bump, year): @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), year=st.sampled_from(CSP_YEARS), ) def test_uc_is_non_increasing_in_carer_support_payment(units, bump, year): - low = calculate(units, year, "carer_support_payment", carer=True) - high = calculate(units, year, "carer_support_payment", bump=bump, carer=True) + kwargs = dict(carer=True, marriage_allowance=False) + low = calculate(units, year, "carer_support_payment", **kwargs) + high = calculate(units, year, "carer_support_payment", bump=bump, **kwargs) increase = high["counted_csp"] - low["counted_csp"] assert np.all(increase >= -0.01), units assert_monotone(low, high, increase, units) @@ -221,14 +224,14 @@ def test_uc_is_non_increasing_in_carer_support_payment(units, bump, year): @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), benefit=st.sampled_from(BENEFITS), bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), year=st.sampled_from(YEARS), ) def test_uc_falls_pound_for_pound_in_each_benefit(units, benefit, bump, year): - low = calculate(units, year, benefit) - high = calculate(units, year, benefit, bump=bump) + low = calculate(units, year, benefit, marriage_allowance=False) + high = calculate(units, year, benefit, bump=bump, marriage_allowance=False) award = low["universal_credit_pre_benefit_cap"] np.testing.assert_allclose( high["universal_credit_pre_benefit_cap"], @@ -240,13 +243,14 @@ def test_uc_falls_pound_for_pound_in_each_benefit(units, benefit, bump, year): @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), year=st.sampled_from(CSP_YEARS), ) def test_uc_falls_pound_for_pound_in_counted_carer_support_payment(units, bump, year): - low = calculate(units, year, "carer_support_payment", carer=True) - high = calculate(units, year, "carer_support_payment", bump=bump, carer=True) + kwargs = dict(carer=True, marriage_allowance=False) + low = calculate(units, year, "carer_support_payment", **kwargs) + high = calculate(units, year, "carer_support_payment", bump=bump, **kwargs) award = low["universal_credit_pre_benefit_cap"] increase = high["counted_csp"] - low["counted_csp"] np.testing.assert_allclose( @@ -259,14 +263,14 @@ def test_uc_falls_pound_for_pound_in_counted_carer_support_payment(units, bump, @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), benefit=st.sampled_from(BENEFITS), year=st.sampled_from(YEARS), ) def test_each_benefit_counts_like_private_pension(units, benefit, year): assert_same( - calculate(units, year, benefit), - calculate(units, year, "private_pension_income"), + calculate(units, year, benefit, marriage_allowance=False), + calculate(units, year, "private_pension_income", marriage_allowance=False), [v for v in UC_VARIABLES if v != "universal_credit"], str(units), ) @@ -274,15 +278,16 @@ def test_each_benefit_counts_like_private_pension(units, benefit, year): @PROPERTY_SETTINGS @given( - units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + units=st.lists(families(), min_size=1, max_size=20), year=st.sampled_from(CSP_YEARS), ) def test_carer_support_payment_counts_like_capped_private_pension(units, year): - with_csp = calculate(units, year, "carer_support_payment", carer=True) + kwargs = dict(carer=True, marriage_allowance=False) + with_csp = calculate(units, year, "carer_support_payment", **kwargs) # The same family with the counted amount of Carer Support Payment # received as private pension instead. counted = [dict(unit, amount=c) for unit, c in zip(units, with_csp["counted_csp"])] - with_pension = calculate(counted, year, "private_pension_income", carer=True) + with_pension = calculate(counted, year, "private_pension_income", **kwargs) assert_same( with_csp, with_pension, @@ -291,14 +296,6 @@ def test_carer_support_payment_counts_like_capped_private_pension(units, year): ) -@pytest.mark.xfail( - strict=True, - reason=( - "PolicyEngine/policyengine-uk#1942: uc_earned_income deducts the whole " - "benefit unit's income tax, including the carer's tax on Carer Support " - "Payment, from the partner's earnings" - ), -) def test_carer_support_payment_above_the_cap_does_not_change_uc(): # 2026, Scotland: a carer aged 40 caring 40 hours a week with private # pension of 9,000 and a partner aged 38 earning 4,000 (under the personal diff --git a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.py b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.py index 52a75c07be..b8e4c7f41e 100644 --- a/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.py +++ b/policyengine_uk/variables/gov/dfe/targeted_childcare_entitlement/meets_universal_credit_criteria_for_targeted_childcare_entitlement.py @@ -5,7 +5,19 @@ class meets_universal_credit_criteria_for_targeted_childcare_entitlement(Variabl value_type = bool entity = BenUnit label = "meets Universal Credit criteria for targeted childcare entitlement" + documentation = ( + "Whether the parents are entitled to Universal Credit with earned " + "income, as Universal Credit calculates it and before the work " + "allowance, not exceeding the limit." + ) definition_period = YEAR + reference = dict( + title=( + "Local Authority (Duty to Secure Early Years Provision Free of " + "Charge) Regulations 2014 reg. 1(2), (3) and (4)" + ), + href="https://www.legislation.gov.uk/uksi/2014/2147/regulation/1", + ) def formula(benunit, period, parameters): p = parameters(period).gov.dfe.targeted_childcare_entitlement @@ -13,11 +25,13 @@ def formula(benunit, period, parameters): # Check if receiving Universal Credit uc = benunit("universal_credit", period) - # For Universal Credit recipients, we must use the definition of "earned income" - # The Local Authority (Duty to Secure Early Years Provision Free of Charge) (Amendment) Regulations 2018 - part 2.c - # https://www.legislation.gov.uk/uksi/2018/146/made - - # UC-specific earned income as specified by 2(c)(3)(a) of the Regulations - earned_income = benunit("uc_earned_income", period) + # Reg. 1(3)(a): "earned income" means income for the purposes of + # Chapter 2 of Part 6 of the Universal Credit Regulations 2013, and + # reg. 1(3)(c) reads the limit against a couple's combined income. + # Chapter 2 is each person's earnings after their own tax, National + # Insurance and pension contributions, with the minimum income floor + # (reg. 62). The work allowance is in reg. 22 (Part 3, the award), so + # it is not deducted here. + earned_income = benunit("uc_earned_income_before_work_allowance", period) return (uc > 0) & (earned_income <= p.income_limit.universal_credit) diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_earned_income.py b/policyengine_uk/variables/gov/dwp/benefit_cap_earned_income.py new file mode 100644 index 0000000000..cf577c6eef --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_earned_income.py @@ -0,0 +1,42 @@ +from policyengine_uk.model_api import * + + +class benefit_cap_earned_income(Variable): + value_type = float + entity = BenUnit + label = "Earned income for the benefit cap earnings exception" + documentation = ( + "The earned income the Universal Credit benefit cap earnings " + "exception tests: each person's earnings less their own pension " + "contributions, income tax and National Insurance, summed over the " + "people whose earnings the UC award counts. It leaves out any income the minimum income floor " + "treats a self-employed person as having, and the work allowance is " + "not deducted." + ) + definition_period = YEAR + unit = GBP + reference = dict( + title="Universal Credit Regulations 2013 reg. 82(1)(a) and (4)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + ) + + def formula(benunit, period, parameters): + # 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". Reg. + # 82(4) leaves out income a person is treated as having under reg. 62 + # (minimum income floor), so this sums each person's actual earned income. + # It counts the same people and the same earnings as the UC award + # (uc_earned_income_before_work_allowance): the claimant and partner + # and, as the award does for now, the programme's own children or + # young persons. Earnings include miscellaneous_income: in the FRS that + # is mainly odd-job pay (reg. 52(a)(iii) "any other paid work"), though + # it also holds some income reg. 66(1)(m) treats as unearned, such as + # royalties. Splitting those out will move the award and this test + # together. + person = benunit.members + members = person("is_claimant_or_partner", period) | person( + "is_child_or_qualifying_young_person_for_universal_credit", period + ) + return add_for_members( + benunit, period, ["uc_individual_earned_income_before_mif"], members + ) diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_earnings_threshold.py b/policyengine_uk/variables/gov/dwp/benefit_cap_earnings_threshold.py new file mode 100644 index 0000000000..ba4505b946 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_earnings_threshold.py @@ -0,0 +1,49 @@ +from policyengine_uk.model_api import * + + +class benefit_cap_earnings_threshold(Variable): + value_type = float + entity = BenUnit + label = "Benefit cap earnings exception threshold" + documentation = ( + "The earned income, over a year, at or above which a Universal Credit " + "award is excepted from the benefit cap: twelve times the monthly " + "amount in the regulations. Since 1 April 2017 the monthly amount is " + "16 hours a week at the national living wage rate, times 52 and " + "divided by 12, with any fraction of a pound disregarded. Before " + "then it was 430 a month." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 82(1)(a)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + ), + dict( + title="Universal Credit Regulations 2013 reg. 6(1A)(za)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/6", + ), + dict( + title="National Minimum Wage Regulations 2015 reg. 4", + href="https://www.legislation.gov.uk/uksi/2015/621/regulation/4", + ), + ] + + def formula(benunit, period, parameters): + p = parameters(period).gov + exception = p.dwp.universal_credit.benefit_cap.earnings_exception + # Reg. 82(1)(a) uses "the hourly rate set out in regulation 4 of the + # National Minimum Wage Regulations": the single national living wage + # rate, whatever the claimant's age. It is the top age band of the + # model's schedule. + hourly_rate = p.hmrc.minimum_wage.non_apprentice.amounts[-1] + monthly_pay = ( + hourly_rate * exception.weekly_hours * WEEKS_IN_YEAR / MONTHS_IN_YEAR + ) + # Reg. 6(1A)(za) disregards a fraction of a pound in the amount + # calculated for reg. 82(1)(a): 12.21 x 16 x 52 / 12 = 846.56 is 846. + monthly_amount = exception.monthly_amount + np.floor(monthly_pay) + # Each monthly assessment period is tested against the monthly + # amount; over a year of level earnings that is twelve times it. + return np.ones(benunit.count) * monthly_amount * MONTHS_IN_YEAR 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 c27e13c39d..a32df8fe92 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 @@ -4,57 +4,33 @@ class is_benefit_cap_exempt_earnings(Variable): value_type = bool entity = BenUnit - label = "Whether exempt from the benefits cap for non-health/disability reasons" + label = "Whether excepted from the benefit cap because of earnings" + documentation = ( + "The Universal Credit earnings exception: the benefit cap does not " + "apply to an award of Universal Credit where the claimant's earned " + "income, or a couple's combined earned income, reaches the pay for " + "16 hours a week at the national living wage. It does not apply to " + "Housing Benefit, whose cap has its own exceptions (HB Regs 2006 " + "regs. 75E and 75F; entitlement to Working Tax Credit is in " + "is_benefit_cap_exempt_health_disability). The nine-month grace " + "period is not modelled." + ) definition_period = YEAR - reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 82(1)(a)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + ), + dict( + title="Housing Benefit Regulations 2006 reg. 75E", + href="https://www.legislation.gov.uk/uksi/2006/213/regulation/75E", + ), + ] def formula(benunit, period, parameters): - # Check if anyone in benefit unit is over state pension age - person = benunit.members - over_pension_age = person("is_SP_age", period) - has_pensioner = benunit.any(over_pension_age) - - # UC-specific exemptions - # Limited capability for work and work-related activity - has_lcwra = benunit.any(person("uc_limited_capability_for_WRA", period)) - - # Carer element in UC indicates caring for someone with disability - gets_uc_carer_element = benunit("uc_carer_element", period) > 0 - - # Earnings exemption for UC (£846/month = £10,152/year) - # Note: Only check earned income, not UC amount itself to avoid circular dependency - 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) - ) - earnings_threshold = 10_152 - meets_earnings_test = uc_earned >= earnings_threshold - - # Disability and carer benefits that exempt from cap - QUAL_PERSONAL_BENEFITS = [ - "attendance_allowance", - "carers_allowance", - "dla", # Disability Living Allowance (includes components) - "pip_dl", # PIP daily living component - "pip_m", # PIP mobility component - "iidb", # Industrial injuries disability benefit - ] - - # ESA and Working Tax Credit - QUAL_BENUNIT_BENEFITS = [ - "esa_income", # Income-based ESA - "working_tax_credit", # If getting WTC, likely working enough - ] - - qualifying_personal_benefits = add(benunit, period, QUAL_PERSONAL_BENEFITS) - qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) - - # Check for Armed Forces Compensation Scheme payments - afcs = benunit("afcs", period) > 0 - - # ESA contribution-based with support component - esa_support_component = benunit("esa_contrib", period) > 0 - - return meets_earnings_test + # Reg. 82(1): "The benefit cap does not apply to an award of + # universal credit ...". The award before the cap is read, since the + # award after it depends on this exception. + has_uc_award = benunit("universal_credit_pre_benefit_cap", period) > 0 + threshold_met = benunit("uc_benefit_cap_earnings_threshold_met", period) + return has_uc_award & threshold_met diff --git a/policyengine_uk/variables/gov/dwp/uc_benefit_cap_earnings_threshold_met.py b/policyengine_uk/variables/gov/dwp/uc_benefit_cap_earnings_threshold_met.py new file mode 100644 index 0000000000..1536e3f4f5 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/uc_benefit_cap_earnings_threshold_met.py @@ -0,0 +1,23 @@ +from policyengine_uk.model_api import * + + +class uc_benefit_cap_earnings_threshold_met(Variable): + value_type = bool + entity = BenUnit + label = "Earned income reaches the Universal Credit benefit cap earnings threshold" + documentation = ( + "Whether the claimant's earned income, or a couple's combined earned " + "income, is at least the pay for 16 hours a week at the national " + "living wage (UC Regs 2013 reg. 82(1)(a)). The benefit cap earnings " + "exception also needs an award of Universal Credit." + ) + definition_period = YEAR + reference = dict( + title="Universal Credit Regulations 2013 reg. 82(1)(a)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + ) + + def formula(benunit, period, parameters): + earned_income = benunit("benefit_cap_earned_income", period) + threshold = benunit("benefit_cap_earnings_threshold", period) + return earned_income >= threshold 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 6e496b8681..b7d900d2cd 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 @@ -4,28 +4,20 @@ class uc_earned_income(Variable): value_type = float entity = BenUnit - label = "Universal Credit earned income (after disregards and tax)" + label = "Universal Credit earned income (after deductions and work allowance)" definition_period = YEAR unit = GBP - - reference = "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", + ) 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 - ) - personal_gross_earned_income = add_for_members( - benunit, period, ["uc_mif_capped_earned_income"], members - ) - disregards = add_for_members( - benunit, - period, - ["uc_work_allowance", "tax", "pension_contributions"], - members, - ) - return max_(0, personal_gross_earned_income - disregards) + # Each person's earned income is net of their own deductions + # (reg. 55(5), reg. 57(2)), summed over the members whose income + # counts (uc_earned_income_before_work_allowance); the work allowance + # then comes off the combined earned income before the taper + # (reg. 22(1)(b)). + earned_income = benunit("uc_earned_income_before_work_allowance", period) + 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_earned_income_before_work_allowance.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income_before_work_allowance.py new file mode 100644 index 0000000000..30ea9b4586 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_earned_income_before_work_allowance.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class uc_earned_income_before_work_allowance(Variable): + value_type = float + entity = BenUnit + label = "Universal Credit earned income before the work allowance" + documentation = ( + "The benefit unit's earned income as Universal Credit calculates it " + "(UC Regs 2013 Part 6 Chapter 2): each person's earnings less their " + "own pension contributions, income tax and National Insurance, or " + "the amount the minimum income floor treats them as having. The work " + "allowance, which belongs to the award calculation in reg. 22, is " + "not deducted. Rules that refer to the Universal Credit calculation " + "of earned income use this amount." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 52", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/52", + ), + dict( + title="Universal Credit Regulations 2013 reg. 22(1)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/22", + ), + ] + + 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 + ) + return add_for_members( + benunit, period, ["uc_individual_earned_income"], members + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py new file mode 100644 index 0000000000..49dff530cc --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_income_tax_on_earnings.py @@ -0,0 +1,96 @@ +from policyengine_uk.model_api import * + + +class uc_income_tax_on_earnings(Variable): + value_type = float + entity = Person + label = "Universal Credit deduction for income tax on the person's earnings" + documentation = ( + "Income tax the person pays in respect of their own employment and " + "self-employment, which Universal Credit deducts from their earned " + "income. Tax on pensions, State Pension, property, savings and " + "dividends is not deducted, nor is the High Income Child Benefit " + "Charge or the pension annual allowance charge." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2), step 3", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + dict( + title="Income Tax Act 2007 s. 16", + href="https://www.legislation.gov.uk/ukpga/2007/3/section/16", + ), + ] + + def formula(person, period, parameters): + # Reg. 55(5)(b) deducts income tax paid by the person "in respect of + # the employment" and reg. 57(2) step 3 income tax paid "in respect + # of any trade". Neither says how to split a person's tax when they + # also have other income. Earnings are taken as the lowest slice of + # their non-savings income, after the allowances they actually have: + # savings and dividends sit above earnings and other non-savings + # income (as in ITA 2007 s. 16), property income above the rest of + # it (as in s. 16A from 2027-28), and other non-savings income + # (private pensions, State Pension, taxable benefits) above + # earnings. So tax on other income never comes off earnings. Under RTI, DWP deducts the PAYE + # actually taken on the job, which can include tax on a State + # Pension coded against it; that is not modelled. + p = parameters(period) + income_tax = p.gov.hmrc.income_tax + # earned_taxable_income is adjusted net income less the incomes and + # allowances on the exclusions list. Read that list, so a reform + # that changes it (for example one that exempts pensions) is + # followed here. + exclusions = list(income_tax.earned_taxable_income_exclusions) + excluded_incomes = [ + variable + for variable in exclusions + if variable in income_tax.adjusted_net_income_components + ] + earnings_components = [ + "taxable_employment_income", + "taxable_self_employment_income", + "taxable_miscellaneous_income", + ] + # Match the gross earnings in uc_mif_capped_earned_income. + bi = p.gov.contrib.ubi_center.basic_income.interactions + if bi.include_in_means_tests and bi.include_in_taxable_income: + earnings_components.append("basic_income") + earnings = add( + person, + period, + [ + variable + for variable in earnings_components + if variable not in exclusions + ], + ) + # The income left in earned_taxable_income before allowances. The + # part of it above earnings is the person's other non-savings income. + income_in_base = person("adjusted_net_income", period) - add( + person, period, excluded_incomes + ) + other_income = max_(0, income_in_base - earnings) + taxable_earnings = max_( + 0, person("earned_taxable_income", period) - other_income + ) + rates = income_tax.rates + tax = where( + person("pays_scottish_income_tax", period), + rates.scotland.rates.calc(taxable_earnings), + rates.uk.calc(taxable_earnings), + ) + # Tax reductions (for example the married couple's allowance) come + # off the tax on earnings first, as allowances do. So the deduction + # never exceeds the income tax the person pays, never includes a + # charge such as the High Income Child Benefit Charge, and does not + # move when other income absorbs more or less of a reduction. + reductions = add(person, period, income_tax.income_tax_subtractions) + return max_(0, tax - reductions) 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 new file mode 100644 index 0000000000..201eedbb24 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income.py @@ -0,0 +1,54 @@ +from policyengine_uk.model_api import * + + +class uc_individual_earned_income(Variable): + value_type = float + entity = Person + label = "Universal Credit earned income of the person" + documentation = ( + "The person's earned income for Universal Credit, after the " + "deductions for their own relievable pension contributions and their " + "own income tax and National Insurance in respect of their employment " + "and self-employment, before the work allowance." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + dict( + title="Finance Act 2004 s. 188 (relievable pension contributions)", + href="https://www.legislation.gov.uk/ukpga/2004/12/section/188", + ), + ] + + def formula(person, period, parameters): + # The deductions are the person's own and come off only their own + # earnings, so one partner's tax, NI or pension contributions never + # reduce the other partner's earned income. + gross_earnings = person("uc_mif_capped_earned_income", period) + # Contributions paid after the person reaches 75 are not relievable + # (Finance Act 2004 s. 188(3)(a)). + age_limit = parameters( + period + ).gov.hmrc.pensions.pension_contributions_relief_age_limit + relievable_pension_contributions = person("pension_contributions", period) * ( + person("age", period) < age_limit + ) + tax_and_national_insurance = add( + person, + period, + ["uc_income_tax_on_earnings", "uc_national_insurance_on_earnings"], + ) + return max_( + 0, + gross_earnings + - relievable_pension_contributions + - tax_and_national_insurance, + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py new file mode 100644 index 0000000000..73a93702a1 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_individual_earned_income_before_mif.py @@ -0,0 +1,62 @@ +from policyengine_uk.model_api import * + + +class uc_individual_earned_income_before_mif(Variable): + value_type = float + entity = Person + label = ( + "Universal Credit earned income of the person, before the minimum income floor" + ) + documentation = ( + "The person's actual earned income for Universal Credit: their " + "earnings less their own relievable pension contributions and their " + "own income tax and National Insurance in respect of their employment " + "and self-employment. The minimum income floor compares this with " + "the net threshold." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + dict( + title="Finance Act 2004 s. 188 (relievable pension contributions)", + href="https://www.legislation.gov.uk/ukpga/2004/12/section/188", + ), + ] + + def formula(person, period, parameters): + # The deductions are the person's own and come off only their own + # earnings, so one partner's tax, NI or pension contributions never + # reduce the other partner's earned income. + earnings_components = ["employment_income", "miscellaneous_income"] + bi = parameters(period).gov.contrib.ubi_center.basic_income + if bi.interactions.include_in_means_tests: + earnings_components.append("basic_income") + # A trading loss makes self-employed earnings nil (reg. 57(2)); it + # is not set against employed earnings. + self_employed = max_(0, person("self_employment_income", period)) + earnings = add(person, period, earnings_components) + self_employed + # Contributions paid after the person reaches 75 are not relievable + # (Finance Act 2004 s. 188(3)(a)), so reg. 55(5)(a) does not deduct + # them. + age_limit = parameters( + period + ).gov.hmrc.pensions.pension_contributions_relief_age_limit + relievable_pension_contributions = person("pension_contributions", period) * ( + person("age", period) < age_limit + ) + tax_and_national_insurance = add( + person, + period, + ["uc_income_tax_on_earnings", "uc_national_insurance_on_earnings"], + ) + return max_( + 0, earnings - relievable_pension_contributions - tax_and_national_insurance + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_national_insurance_on_earnings.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_national_insurance_on_earnings.py new file mode 100644 index 0000000000..93ca6e8d71 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_national_insurance_on_earnings.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * + + +class uc_national_insurance_on_earnings(Variable): + value_type = float + entity = Person + label = "Universal Credit deduction for National Insurance on the person's earnings" + documentation = ( + "Primary Class 1 contributions on the person's employment and Class 2 " + "and Class 4 contributions on their trade, which Universal Credit " + "deducts from their earned income. Voluntary Class 3 contributions " + "are not in respect of an employment or trade, so are not deducted." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Universal Credit Regulations 2013 reg. 55(5)(b)", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + ), + dict( + title="Universal Credit Regulations 2013 reg. 57(2), step 3", + href="https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + ), + ] + adds = [ + "ni_class_1_employee", + "ni_class_2", + "ni_class_4", + ] diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index 54883e9b4b..3577552f73 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -25,8 +25,18 @@ def legacy_council_tax_reduction( universal_credit = benunit("universal_credit", period) has_uc_award = universal_credit > 0 uc_applicable_amount = benunit("uc_maximum_amount", period) + # The schemes use "the calculation or estimate of the amount of the + # income of the applicant ... made by the Secretary of State for the + # purpose of determining the award of universal credit", plus the award + # (Default Scheme 2013 para. 37(1) and (3)(a); Kingston upon Thames and + # Merton reproduce it, Newham refers to the same calculation, and + # Westminster's scheme is based on the Default Scheme). That income is + # earned income after tax, National Insurance and pension contributions + # and unearned income. The work allowance and taper are steps in the + # award (UC Regs 2013 reg. 22), not in the income, so the work allowance + # is not deducted here. uc_applicable_income = ( - benunit("uc_earned_income", period) + benunit("uc_earned_income_before_work_allowance", period) + benunit("uc_unearned_income", period) + universal_credit )