diff --git a/changelog.d/uc-state-pension-unearned-income.fixed.md b/changelog.d/uc-state-pension-unearned-income.fixed.md new file mode 100644 index 0000000000..caa0e29697 --- /dev/null +++ b/changelog.d/uc-state-pension-unearned-income.fixed.md @@ -0,0 +1 @@ +Count State Pension as Universal Credit unearned income (retirement pension income, UC Regs 2013 regs 66(1)(a) and 67). Mixed-age couples on UC were having their State Pension ignored. diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index 5581288a93..9717bfba21 100644 --- a/docs/book/programs/gov/dwp/universal-credit.ipynb +++ b/docs/book/programs/gov/dwp/universal-credit.ipynb @@ -44,7 +44,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: the maximum entitlement is reduced by the benunit's countable earnings above its work allowance (`uc_work_allowance`) at the published taper rate (the `gov.dwp.universal_credit.means_test.reduction_rate` parameter, currently 55%), plus its unearned income. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." + "source": "## How PolicyEngine computes Universal Credit\n\nPolicyEngine computes Universal Credit at the benefit-unit level in three stages:\n\n1. **Maximum entitlement** (`uc_maximum_amount`): sum the elements the benunit qualifies for — `uc_standard_allowance`, `uc_child_element`, `uc_disability_elements`, `uc_carer_element`, `uc_housing_costs_element`, and `uc_childcare_element`.\n2. **Means test**: the maximum entitlement is reduced by the benunit's countable earnings above its work allowance (`uc_work_allowance`) at the published taper rate (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, savings, dividend and property income, and the tariff income deemed from capital (reg 72), which replaces the actual yield of the capital it is charged on. Asset rules apply on top of this — capital above the lower threshold deems a tariff income, and capital above the upper threshold disqualifies the benunit entirely.\n3. **Benefit cap**: the post-means-test award (`universal_credit_pre_benefit_cap`) is finally reduced by `benefit_cap_reduction` to produce `universal_credit`. The benefit cap only applies to benunits without a benefit-cap exemption (working enough hours, having a qualifying disability benefit, etc.).\n\nThe take-up step is handled by the input variable `would_claim_uc`, which is populated stochastically when the dataset is built so that PolicyEngine's caseload aggregates match published DWP claimant numbers rather than the full eligible population.\n\nParameters live in `policyengine_uk/parameters/gov/dwp/universal_credit/` and the per-element formulas in `policyengine_uk/variables/gov/dwp/universal_credit/`." }, { "cell_type": "code", diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml index 0d7a4331b1..25db4373d0 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/unearned.yaml @@ -8,6 +8,10 @@ metadata: reference: - title: The Universal Credit Regulations 2013 reg. 66 href: https://www.legislation.gov.uk/uksi/2013/376/regulation/66 + - title: The Universal Credit Regulations 2013 reg. 67 (retirement pension income) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/67 + - title: State Pension Credit Act 2002 s. 16(1)(za), (a) (State Pension counts as retirement pension income) + href: https://www.legislation.gov.uk/ukpga/2002/16/section/16 - title: The Universal Credit Regulations 2013 reg. 66(1)(b)(ii), (viii) and (ix) (contributory ESA, maternity allowance, industrial injuries benefit) href: https://www.legislation.gov.uk/uksi/2013/376/regulation/66 - title: The Universal Credit Regulations 2013 reg. 2 (employment and support allowance and jobseeker's allowance mean the contributory allowances; industrial injuries benefit means benefit under SSCBA 1992 Part 5) @@ -28,6 +32,7 @@ values: - maternity_allowance - iidb - private_pension_income + - state_pension - uc_tariff_income - savings_interest_income - dividend_income @@ -40,6 +45,7 @@ values: - maternity_allowance - iidb - private_pension_income + - state_pension - uc_tariff_income - savings_interest_income - dividend_income diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml new file mode 100644 index 0000000000..c88a124cb8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_state_pension.yaml @@ -0,0 +1,187 @@ +# State Pension is retirement pension income, which counts in full as +# Universal Credit unearned income: UC Regs 2013 reg. 66(1)(a), with the +# meaning in reg. 67(1) taken from State Pension Credit Act 2002 s. 16(1)(za) +# (new State Pension) and (a) (Category A or B retirement pension). Only a +# mixed-age couple can have both a UC award and a State Pension, because UC +# needs a member under State Pension age. +# +# Hand calculations use the 2026-27 rates read in April 2026: +# - couple standard allowance (one over 25): 666.97 a month = 8,003.64 a year; +# - earned income reduction rate: 55%; +# - council rent of 6,240 a year is met in full by the housing costs element. +# So the maximum amount is 8,003.64 + 6,240 = 14,243.64. + +- name: State Pension counts as unearned income + period: 2026 + absolute_error_margin: 0.01 + input: + state_pension: 9_000 + output: + uc_unearned_income: 9_000 + +- name: State Pension adds to private pension income + period: 2026 + absolute_error_margin: 0.01 + input: + state_pension: 9_000 + private_pension_income: 1_000 + output: + uc_unearned_income: 10_000 + +- name: Reported new State Pension flows through to unearned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension_type: NEW + # 9,000 / 52 = 173.08 a week, under the full new State Pension, so + # all of it is new_state_pension and none is a protected payment. + state_pension_reported: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + output: + state_pension: [9_000, 0] + uc_unearned_income: 9_000 + +- name: Mixed-age couple renting from the council, State Pension reduces UC pound for pound + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + council_tax: 1_500 + output: + uc_standard_allowance: 8_003.64 + uc_housing_costs_element: 6_240 + uc_maximum_amount: 14_243.64 + uc_unearned_income: 9_000 + uc_income_reduction: 9_000 + # 14,243.64 - 9,000. Before this fix the award was 14,243.64. + universal_credit: 5_243.64 + # The pension-age CTR income is State Pension plus UC: + # 9,000 + 5,243.64 = 14,243.64, below the applicable amount of + # 383.35 x 52 = 19,934.20, so council tax is met in full. Before this fix + # the inflated UC gave an income of 23,243.64 and CTR of + # 1,500 - 20% x (23,243.64 - 19,934.20) = 838.11. + council_tax_benefit: 1_500 + +- name: Same couple with the same amount of private pension instead gets the same UC + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + council_tax: 1_500 + output: + uc_unearned_income: 9_000 + universal_credit: 5_243.64 + +- name: State Pension above the maximum amount removes UC entirely + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 20_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + uc_unearned_income: 20_000 + # The reduction is capped at the maximum amount. + uc_income_reduction: 14_243.64 + universal_credit: 0 + +- name: State Pension and the younger partner's earnings both reduce UC + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + partner: + age: 60 + # Below the personal allowance and the NI primary threshold, so net + # earnings equal gross earnings. + employment_income: 6_000 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + uc_earned_income: 6_000 + # No children and no limited capability for work, so no work allowance: + # 55% x 6,000 = 3,300 of earnings plus 9,000 of State Pension. + uc_income_reduction: 12_300 + # 14,243.64 - 12,300. + universal_credit: 1_943.64 + +- name: State Pension also counts in 2020-21 + period: 2020 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + households: + household: + members: [pensioner, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + # Couple standard allowance 596.58 a month (with the 2020 uplift): + # 7,158.96 + 6,240 = 13,398.96; less 9,000 of State Pension. + uc_maximum_amount: 13_398.96 + uc_unearned_income: 9_000 + universal_credit: 4_398.96 diff --git a/policyengine_uk/tests/test_uc_state_pension_properties.py b/policyengine_uk/tests/test_uc_state_pension_properties.py new file mode 100644 index 0000000000..9b68022746 --- /dev/null +++ b/policyengine_uk/tests/test_uc_state_pension_properties.py @@ -0,0 +1,237 @@ +"""Property-based tests for State Pension in the Universal Credit means test. + +UC Regs 2013 reg. 66(1)(a) counts retirement pension income as unearned +income, and reg. 67(1) gives that phrase its State Pension Credit Act 2002 +s. 16 meaning, which covers State Pension (s. 16(1)(za) and (a)) and private +pensions (s. 16(1)(f)) alike. Reg. 22(1)(a) deducts all unearned income from +the maximum amount. Only a mixed-age couple can hold both a UC award and a +State Pension, because UC needs a member under State Pension age. + +Invariants, for any generated population of families: + +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). +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). With no earnings in the family, it + also equals UC with x of property income (held without property capital, + so it is not treated as capital yield under reg. 72). + +Invariants 2 and 3 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 3 to all families then. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 2020 has the temporary standard allowance uplift, 2026 the current rates. +YEARS = [2020, 2026] +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", + "OWNED_WITH_MORTGAGE", +] +# State Pension age is 66 until 2026-27 finishes phasing up; 67 and over is +# unambiguously pension age and 60 and under unambiguously working age. +PENSION_AGE = st.integers(67, 100) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "mixed_age": [PENSION_AGE, WORKING_AGE], + "single_pension": [PENSION_AGE], + "couple_pension": [PENSION_AGE, PENSION_AGE], +} +money = st.floats(0, 30_000, allow_nan=False, allow_infinity=False) +UC_VARIABLES = [ + "universal_credit", + "universal_credit_pre_benefit_cap", + "uc_earned_income", + "uc_unearned_income", + "uc_income_reduction", + "uc_maximum_amount", +] + + +@st.composite +def families(draw, with_earnings=True): + # 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 + return dict( + ages=[draw(age) for age in SHAPES[shape]], + children=[draw(st.integers(0, 15)) for _ in range(draw(st.integers(0, 2)))], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(money), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + earnings=earnings, + state_pension=draw(money), + ) + + +def situation(units, year, income_variable="state_pension", pension_bump=0.0): + """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. + """ + 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}} + if j == 0: + amount = unit["state_pension"] + pension_bump + person[income_variable] = {year: amount} + elif age < 67: + person["employment_income"] = {year: unit["earnings"]} + people[name] = person + names.append(name) + for k, age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {year: age}} + names.append(name) + benunits[f"b{i}"] = {"members": names} + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "savings": {year: unit["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs)) + return {v: np.asarray(sim.calculate(v, year)) for v in UC_VARIABLES} + + +def assert_same(a, b, message): + for variable in UC_VARIABLES: + np.testing.assert_allclose( + a[variable], b[variable], atol=0.01, err_msg=f"{variable}: {message}" + ) + + +@PROPERTY_SETTINGS +@given( + 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), +) +def test_uc_is_non_increasing_in_state_pension(units, bump, year): + low = calculate(units, year) + high = calculate(units, year, pension_bump=bump) + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(high[variable] <= low[variable] + 0.01), (variable, units) + # State Pension changes nothing in the maximum amount, and all of it is + # unearned income. + np.testing.assert_allclose( + high["uc_maximum_amount"], low["uc_maximum_amount"], atol=0.01 + ) + np.testing.assert_allclose( + high["uc_unearned_income"] - low["uc_unearned_income"], + bump, + atol=0.01, + err_msg=str(units), + ) + # Families with no working-age adult never get UC. + no_working_age_adult = np.array([min(unit["ages"]) >= 67 for unit in units]) + assert np.all(high["universal_credit"][no_working_age_adult] == 0), units + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + 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_state_pension(units, bump, year): + low = calculate(units, year) + high = calculate(units, year, pension_bump=bump) + award = low["universal_credit_pre_benefit_cap"] + np.testing.assert_allclose( + high["universal_credit_pre_benefit_cap"], + award - np.minimum(bump, award), + atol=0.01, + err_msg=str(units), + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + year=st.sampled_from(YEARS), +) +def test_state_pension_counts_like_private_pension(units, year): + assert_same( + calculate(units, year), + calculate(units, year, income_variable="private_pension_income"), + str(units), + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(with_earnings=False), min_size=1, max_size=20), + year=st.sampled_from(YEARS), +) +def test_state_pension_counts_like_property_income(units, year): + assert_same( + calculate(units, year), + calculate(units, year, income_variable="property_income"), + str(units), + ) + + +@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 + # income tax of 86 and NI of 34.40 (above the personal allowance, so no + # marriage allowance). Council rent 20,000. + # Earned income = 13,000 - 86 - 34.40 = 12,879.60 (reg. 55(5)(b)). + # Maximum amount = 12 x 666.97 + 20,000 = 28,003.64. + # UC = 28,003.64 - (0.55 x 12,879.60 + 16,000) = 4,919.86. + unit = dict( + ages=[70, 45], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=20_000.0, + savings=0.0, + earnings=13_000.0, + state_pension=16_000.0, + ) + 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)