diff --git a/changelog.d/uc-capital-derived-income.fixed.md b/changelog.d/uc-capital-derived-income.fixed.md new file mode 100644 index 000000000..6294868b5 --- /dev/null +++ b/changelog.d/uc-capital-derived-income.fixed.md @@ -0,0 +1 @@ +Universal Credit no longer counts actual savings interest, dividends or rent as unearned income: regulation 66(1) of the Universal Credit Regulations 2013 lists no description that covers them, so capital counts only through its assumed yield (tariff income). diff --git a/changelog.d/uc-capital-derived-income.removed.md b/changelog.d/uc-capital-derived-income.removed.md new file mode 100644 index 000000000..4119c877c --- /dev/null +++ b/changelog.d/uc-capital-derived-income.removed.md @@ -0,0 +1 @@ +Removed the `gov.dwp.universal_credit.means_test.income_definitions.capital_derived` parameter, which gated the removal of interest, dividends and rent from Universal Credit unearned income. diff --git a/docs/book/programs/gov/dwp/universal-credit.ipynb b/docs/book/programs/gov/dwp/universal-credit.ipynb index 9717bfba2..5e4ecbe44 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 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/`." + "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/`." }, { "cell_type": "code", diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/capital_derived.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/capital_derived.yaml deleted file mode 100644 index 211724065..000000000 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/means_test/income_definitions/capital_derived.yaml +++ /dev/null @@ -1,30 +0,0 @@ -description: > - Model income variables treated as capital-derived income for Universal Credit - when tariff income applies. Regulation 72(3) covers actual income derived - from capital, for example rental, interest or dividends. In the current - model, those map to `property_income` when countable property capital is - present, `savings_interest_income` when savings are present, and - `dividend_income` when corporate wealth is present. Those linked income - streams are removed from `uc_unearned_income` once `uc_tariff_income` - applies to avoid double counting the same capital. The model does not add - annual income flows back into `uc_assessable_capital`, because the dataset - does not identify whether those receipts are retained as capital within the - assessment year. When benunit-level `uc_reported_capital` is provided, the - model treats that as sufficient evidence of countable capital for all listed - capital-derived income variables, so `savings_interest_income`, - `dividend_income`, and `property_income` are all suppressed once tariff - income applies. -metadata: - economy: false - label: Universal Credit capital-derived unearned income sources - propagate_metadata_to_children: true - period: year - unit: list - reference: - - title: The Universal Credit Regulations 2013 reg. 72(3) - href: https://www.legislation.gov.uk/uksi/2013/376/regulation/72/3 -values: - 2013-04-29: - - savings_interest_income - - dividend_income - - property_income 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 25db4373d..ff0d28245 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 @@ -1,4 +1,25 @@ -description: Unearned income sources for Universal Credit. +description: > + Unearned income sources for Universal Credit. Regulation 66(1) defines + unearned income as income "falling within the following descriptions" and + lists them in sub-paragraphs (a) to (m); income outside that list is not + taken into account (DWP Advice for Decision Making H5002 and H5112). Actual + interest, dividends and rent fall in none of them: sub-paragraph (m) reaches + only income taxable under Part 5 of the Income Tax (Trading and Other Income) + Act 2005, whereas interest and dividends are charged under Part 4 and + property income under Part 3. Capital counts instead through its assumed + yield under regulation 72(1) (`uc_tariff_income`, sub-paragraph (k)). Where + capital is treated as yielding income, regulation 72(3) makes the actual + income derived from it, "for example rental, interest or dividends", part of + the person's capital. So `savings_interest_income`, `dividend_income` and + `property_income` are not on this list at any capital level. Income from a + trust or an annuity (sub-paragraphs (i) and (j)) and estate income (ITTOIA + Part 5 Chapter 6, sub-paragraph (m)) still count, but none of them is in + those three variables. Letting that amounts to a trade, such as a bed and + breakfast, gives self-employed earnings under regulation 57, which the model + reads from `self_employment_income`. Whether other letting can be a trade is + unsettled: DWP staff guidance treats a self-employed landlord's rent as + self-employed income, while letting is not a trade for tax, and the model + does not identify such landlords. metadata: economy: false label: Universal Credit unearned income sources @@ -6,7 +27,7 @@ metadata: period: year unit: list reference: - - title: The Universal Credit Regulations 2013 reg. 66 + - title: The Universal Credit Regulations 2013 reg. 66(1) 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 @@ -22,6 +43,14 @@ metadata: href: https://www.legislation.gov.uk/uksi/2026/246/article/25 - title: The Universal Credit Regulations 2013 reg. 72 href: https://www.legislation.gov.uk/uksi/2013/376/regulation/72 + - title: The Universal Credit Regulations 2013 reg. 57 + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/57 + - title: The Universal Credit Regulations (Northern Ireland) 2016 reg. 66(1) + href: https://www.legislation.gov.uk/nisr/2016/216/regulation/66 + - title: The Universal Credit Regulations (Northern Ireland) 2016 reg. 72 + href: https://www.legislation.gov.uk/nisr/2016/216/regulation/72 + - title: DWP Advice for Decision Making, Chapter H5 (unearned income), H5002, H5093-H5094 and H5112 + href: https://www.gov.uk/government/publications/advice-for-decision-making-staff-guide values: @@ -34,9 +63,6 @@ values: - private_pension_income - state_pension - uc_tariff_income - - savings_interest_income - - dividend_income - - property_income 2023-11-19: - carers_allowance - uc_unearned_carer_support_payment @@ -47,6 +73,3 @@ values: - private_pension_income - state_pension - uc_tariff_income - - savings_interest_income - - dividend_income - - property_income diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_income_from_capital.yaml new file mode 100644 index 000000000..446765898 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_income_from_capital.yaml @@ -0,0 +1,193 @@ +# Universal Credit awards with interest, dividends and rent, hand-computed from +# the UC Regs 2013 (and the UC Regs (NI) 2016, which are the same here). +# +# - Reg. 66(1): unearned income is income "falling within the following +# descriptions"; none covers actual interest, dividends or rent. Reg. +# 66(1)(m) reaches only ITTOIA 2005 Part 5 income (interest and dividends +# are Part 4, property income Part 3). DWP ADM H5002: "If a type of income +# is not listed above, it does not affect the claimant's award." +# - Reg. 72(1): capital above £6,000 yields £4.35 a month for each £250 or +# part of £250 (reg. 66(1)(k)). Reg. 72(3): actual income derived from that +# capital, "for example rental, interest or dividends", is capital. +# - Reg. 22(1): award = maximum amount - 55% of earned income - all unearned +# income. +# +# 2026 rates (gov.dwp.universal_credit.standard_allowance, 2026-27): single +# claimant 25 or over £424.90 a month, so 12 x 424.90 = £5,098.80 a year; +# joint claimants, one or both 25 or over, £666.97 a month, so £8,003.64. +# Every case below has income under the personal allowance, so no income tax +# or National Insurance, and no housing, child or disability element. +# +# Property values are taken as UC capital as entered. Reg. 49(1) would first +# deduct 10% for sale costs and any secured debt from a property's market +# value; the model does not yet (#1960). So "£10,000 of let property" below +# means £10,000 of assessable capital, for example a property with a market +# value of £11,111.11 and no mortgage. + +- name: Savings interest, dividends and rent below £6,000 of capital leave the award unchanged + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + savings_interest_income: 200 + dividend_income: 300 + property_income: 1_000 + benunits: + benunit: + members: person + households: + household: + members: person + savings: 5_000 + output: + # No tariff income (capital is not above £6,000) and no unearned income. + uc_tariff_income: 0 + uc_unearned_income: 0 + uc_maximum_amount: 5_098.80 + universal_credit: 5_098.80 + +- name: Rent and interest are not counted on top of tariff income from a let property + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + savings_interest_income: 500 + property_income: 4_800 + benunits: + benunit: + members: person + households: + household: + members: person + savings: 0 + other_residential_property_value: 10_000 + output: + # Tariff income on £10,000 of capital: (10,000 - 6,000) / 250 = 16 steps + # x £4.35 = £69.60 a month, £835.20 a year. UC = 5,098.80 - 835.20 = + # £4,263.60. + uc_tariff_income: 835.20 + uc_unearned_income: 835.20 + universal_credit: 4_263.60 + +- name: Northern Ireland treats rent and interest from capital the same way + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + savings_interest_income: 500 + property_income: 4_800 + benunits: + benunit: + members: person + households: + household: + members: person + region: NORTHERN_IRELAND + savings: 0 + other_residential_property_value: 10_000 + output: + # UC Regs (NI) 2016 regs. 66(1) and 72 match the GB text on these points. + uc_tariff_income: 835.20 + uc_unearned_income: 835.20 + universal_credit: 4_263.60 + +- name: Rent from a lodger in the home does not reduce the award + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + property_income: 3_000 + benunits: + benunit: + members: person + households: + household: + members: person + tenure_type: OWNED_OUTRIGHT + main_residence_value: 200_000 + output: + # The home is disregarded capital (Sch. 10 para. 1) and rent from a + # sub-tenant or boarder is not income (ADM H5112). + uc_tariff_income: 0 + uc_unearned_income: 0 + universal_credit: 5_098.80 + +- name: A partner's dividends do not reduce a working couple's award + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + employment_income: 10_000 + partner: + age: 32 + dividend_income: 2_000 + benunits: + benunit: + members: [person, partner] + households: + household: + members: [person, partner] + savings: 3_000 + output: + # No work allowance (no child, no limited capability for work), so the + # earnings reduction is 55% x 10,000 = 5,500. UC = 8,003.64 - 5,500 = + # £2,503.64. The dividends do not appear anywhere in the calculation. + uc_earned_income: 10_000 + uc_unearned_income: 0 + uc_maximum_amount: 8_003.64 + universal_credit: 2_503.64 + +# The two cases below pin monotonicity at the tariff threshold. Before this +# fix, £1 more savings switched interest out of unearned income and raised the +# award from £2,098.80 to £5,046.60. +- name: Interest does not reduce the award with savings of £6,000 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + savings_interest_income: 3_000 + benunits: + benunit: + members: person + households: + household: + members: person + savings: 6_000 + output: + uc_tariff_income: 0 + uc_unearned_income: 0 + universal_credit: 5_098.80 + +- name: One more pound of savings lowers the award by the tariff income only + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + savings_interest_income: 3_000 + benunits: + benunit: + members: person + households: + household: + members: person + savings: 6_001 + output: + # £1 over £6,000 is an incomplete £250 step: £4.35 x 12 = £52.20. + # UC = 5,098.80 - 52.20 = £5,046.60. + uc_tariff_income: 52.20 + uc_unearned_income: 52.20 + universal_credit: 5_046.60 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml index b1db7dcd7..1ae608c9d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income/uc_unearned_income.yaml @@ -1,4 +1,14 @@ -- name: All sources summed +# UC Regs 2013 reg. 66(1) lists the descriptions of unearned income, and income +# outside them is not taken into account (DWP ADM H5002, H5112). Actual +# interest, dividends and rent fall in none of them: reg. 66(1)(m) reaches only +# ITTOIA 2005 Part 5 income, while interest and dividends are Part 4 and +# property income is Part 3. Capital counts through its assumed yield instead +# (reg. 72(1), reg. 66(1)(k)): £4.35 a month for each £250, or part of £250, +# above £6,000. Where capital is treated as yielding income, reg. 72(3) treats +# the actual income derived from it, "for example rental, interest or +# dividends", as capital. + +- name: Listed sources are summed and interest and dividends are not period: 2020 absolute_error_margin: 0 input: @@ -8,7 +18,8 @@ savings_interest_income: 10 dividend_income: 1 output: - uc_unearned_income: 11_111 + # Reg. 66(1)(b)(iii) carer's allowance + (b)(i) JSA + (a) private pension. + uc_unearned_income: 11_100 - name: Tariff income is exposed in unearned income period: 2025 @@ -27,7 +38,7 @@ output: uc_unearned_income: 4.35 * 12 -- name: Savings interest is folded into capital when tariff income applies +- name: Savings interest is not unearned income when tariff income applies period: 2025 absolute_error_margin: 0.01 input: @@ -46,7 +57,7 @@ uc_tariff_income: 4.35 * 12 uc_unearned_income: 4.35 * 12 -- name: Dividend income is folded into capital when tariff income applies +- name: Dividend income is not unearned income when tariff income applies period: 2025 absolute_error_margin: 0.01 input: @@ -66,7 +77,7 @@ uc_tariff_income: 4.35 * 12 uc_unearned_income: 4.35 * 12 -- name: Property income is folded into capital when tariff income applies +- name: Rent from residential property is not unearned income when tariff income applies period: 2025 absolute_error_margin: 0.01 input: @@ -86,7 +97,7 @@ uc_tariff_income: 4.35 * 12 uc_unearned_income: 4.35 * 12 -- name: Non-residential property income is folded into capital when tariff income applies +- name: Rent from non-residential property is not unearned income when tariff income applies period: 2025 absolute_error_margin: 0.01 input: @@ -106,7 +117,7 @@ uc_tariff_income: 4.35 * 12 uc_unearned_income: 4.35 * 12 -- name: Dividend income stays in unearned income without corporate wealth +- name: Dividend income is not unearned income without share capital in the data period: 2025 absolute_error_margin: 0.01 input: @@ -122,10 +133,12 @@ members: person savings: 6_001 output: + # No reg. 66(1) description covers dividends, whether or not the data + # records the shares that pay them. uc_tariff_income: 4.35 * 12 - uc_unearned_income: 120 + 4.35 * 12 + uc_unearned_income: 4.35 * 12 -- name: Property income stays in unearned income without countable property capital +- name: Rent is not unearned income without property capital in the data period: 2025 absolute_error_margin: 0.01 input: @@ -142,9 +155,9 @@ savings: 6_001 output: uc_tariff_income: 4.35 * 12 - uc_unearned_income: 120 + 4.35 * 12 + uc_unearned_income: 4.35 * 12 -- name: Main residence value alone does not suppress property income +- name: Rent from letting part of the home is not unearned income period: 2025 absolute_error_margin: 0.01 input: @@ -161,8 +174,58 @@ savings: 6_001 main_residence_value: 1_000_000 output: + # The home is disregarded capital (Sch. 10 para. 1), so it yields no + # tariff income, and rent from a sub-tenant or boarder is not income + # either (ADM H5112, example of a room let at £60 a week). uc_tariff_income: 4.35 * 12 - uc_unearned_income: 120 + 4.35 * 12 + uc_unearned_income: 4.35 * 12 + +- name: Interest, dividends and rent are not unearned income below the tariff threshold + period: 2025 + absolute_error_margin: 0 + input: + people: + person: + age: 30 + savings_interest_income: 200 + dividend_income: 300 + property_income: 1_000 + benunits: + benunit: + members: person + households: + household: + members: person + savings: 5_000 + output: + # Capital of £6,000 or less yields nothing (reg. 72(1); ADM H1764), and + # the actual income it pays is outside reg. 66(1). + uc_tariff_income: 0 + uc_unearned_income: 0 + +- name: Interest is not added to tariff income when the data records no savings + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 30 + savings_interest_income: 500 + property_income: 4_800 + benunits: + benunit: + members: person + households: + household: + members: person + savings: 0 + other_residential_property_value: 10_000 + output: + # Tariff income on £10,000 of capital in let property (taken as entered; + # reg. 49 valuation is #1960): (10,000 - 6,000) / 250 = 16 steps x £4.35 + # x 12 = £835.20. Neither the rent nor the interest is added on top of it. + uc_tariff_income: 835.20 + uc_unearned_income: 835.20 - name: Reported capital drives tariff income for multi-benunit households period: 2025 @@ -188,7 +251,7 @@ uc_tariff_income: [52.2, 0] uc_unearned_income: [52.2, 0] -- name: Reported capital suppresses dividend income without household asset detail +- name: Dividend income is not unearned income with reported capital period: 2025 absolute_error_margin: 0.01 input: @@ -208,7 +271,7 @@ uc_tariff_income: 52.2 uc_unearned_income: 52.2 -- name: Reported capital suppresses property income without household asset detail +- name: Rent is not unearned income with reported capital period: 2025 absolute_error_margin: 0.01 input: @@ -228,7 +291,7 @@ uc_tariff_income: 52.2 uc_unearned_income: 52.2 -- name: Dividend income stays unearned when only disregarded pension wealth is held +- name: Dividend income is not unearned income when only disregarded pension wealth is held period: 2025 absolute_error_margin: 0.01 input: @@ -246,8 +309,7 @@ private_pension_wealth: 150_000 output: # Pension wealth is disregarded capital (UC Regs 2013 Sch 10 para 10), so - # the dividend gate keys on corporate_wealth alone: with no share-like - # holdings the dividends are not folded into capital and stay unearned. + # it adds no tariff income, and the dividends are outside reg. 66(1). uc_assessable_capital: 6_001 uc_tariff_income: 4.35 * 12 - uc_unearned_income: 4.35 * 12 + 120 + uc_unearned_income: 4.35 * 12 diff --git a/policyengine_uk/tests/test_uc_income_from_capital_properties.py b/policyengine_uk/tests/test_uc_income_from_capital_properties.py new file mode 100644 index 000000000..0bc207f5a --- /dev/null +++ b/policyengine_uk/tests/test_uc_income_from_capital_properties.py @@ -0,0 +1,295 @@ +"""Property-based tests for income from capital in the Universal Credit means test. + +UC Regs 2013 reg. 66(1) makes unearned income only income "falling within the +following descriptions", and no description covers actual interest, dividends +or rent (reg. 66(1)(m) reaches only ITTOIA 2005 Part 5 income; interest and +dividends are Part 4, property income Part 3). Capital counts through its +assumed yield under reg. 72(1), which reg. 66(1)(k) brings into unearned +income. Where capital is treated as yielding income, reg. 72(3) treats the +actual income derived from it as capital. +The UC Regs (NI) 2016 are the same on these points. + +Invariants, for any generated population of families: + +1. Single count (differential): unearned income equals the tariff income + computed independently from reg. 72(1) on assessable capital, plus the + other sources on the model's reg. 66(1) list, whatever interest, dividends + 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. +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. +""" + +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. +# The capital rules have not changed since 29 April 2013. +YEARS = [2020, 2026] +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", + "OWNED_WITH_MORTGAGE", +] +REGIONS = ["LONDON", "NORTH_WEST", "WALES", "SCOTLAND", "NORTHERN_IRELAND"] +# Countable capital sources (gov.dwp.universal_credit.means_test.capital.sources). +CAPITAL_SOURCES = [ + "savings", + "other_residential_property_value", + "non_residential_property_value", + "corporate_wealth", +] +CAPITAL_INCOME = ["savings_interest_income", "dividend_income", "property_income"] +# Reg. 72(1): £4.35 a month for each £250, or part of £250, above £6,000. +TARIFF_THRESHOLD, TARIFF_STEP, TARIFF_MONTHLY = 6_000, 250, 4.35 + +WORKING_AGE = st.integers(18, 60) +money = st.floats(0, 30_000, allow_nan=False, allow_infinity=False) +capital = st.one_of(st.just(0.0), st.floats(0, 20_000, allow_nan=False)) +UC_VARIABLES = [ + "universal_credit", + "universal_credit_pre_benefit_cap", + "uc_earned_income", + "uc_unearned_income", + "uc_tariff_income", + "uc_income_reduction", + "uc_maximum_amount", + "uc_assessable_capital", + "is_uc_eligible", +] + + +@st.composite +def families(draw, with_earnings=True): + earnings = draw(st.one_of(st.just(0.0), money)) if with_earnings else 0.0 + 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)))], + tenure=draw(st.sampled_from(TENURES)), + region=draw(st.sampled_from(REGIONS)), + rent=draw(money), + capital={source: draw(capital) for source in CAPITAL_SOURCES}, + main_residence_value=draw(st.one_of(st.just(0.0), st.floats(0, 400_000))), + # Reported capital overrides the household proxy (-1 means unreported). + reported_capital=draw(st.one_of(st.just(-1.0), capital)), + earnings=earnings, + private_pension_income=draw(st.one_of(st.just(0.0), money)), + capital_income={variable: draw(money) for variable in CAPITAL_INCOME}, + # Which adult receives the capital income. + recipient=draw(st.integers(0, 1)), + ) + + +def situation(units, year, income_scale=1.0, capital_bump=None): + """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. + """ + 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}} + if j == 0: + person["employment_income"] = {year: unit["earnings"]} + person["private_pension_income"] = { + year: unit["private_pension_income"] + } + if j == min(unit["recipient"], len(unit["ages"]) - 1): + for variable, amount in unit["capital_income"].items(): + person[variable] = {year: amount * income_scale} + 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) + benunit = {"members": names} + reported = unit["reported_capital"] + if capital_bump is not None and reported >= 0: + reported += capital_bump[1] + benunit["uc_reported_capital"] = {year: reported} + benunits[f"b{i}"] = benunit + stocks = dict(unit["capital"]) + if capital_bump is not None: + stocks[capital_bump[0]] += capital_bump[1] + households[f"h{i}"] = { + "members": names, + "rent": {year: unit["rent"]}, + "tenure_type": {year: unit["tenure"]}, + "region": {year: unit["region"]}, + "main_residence_value": {year: unit["main_residence_value"]}, + **{source: {year: value} for source, value in stocks.items()}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def listed_sources(sim, year): + """The model's reg. 66(1) list for the year, as variable names.""" + parameters = sim.tax_benefit_system.parameters(f"{year}-01-01") + means_test = parameters.gov.dwp.universal_credit.means_test + return list(means_test.income_definitions.unearned) + + +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} + 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 + values["other_listed_sources"] = sum( + np.asarray(sim.calculate(v, year, map_to="benunit")) + for v in sources + if v != "uc_tariff_income" + ) + return values + + +def reference_tariff_income(assessable_capital, eligible): + """Annual assumed yield under reg. 72(1), computed without the model.""" + excess = np.maximum(0.0, assessable_capital - TARIFF_THRESHOLD) + steps = np.ceil(excess / TARIFF_STEP) + return np.where(eligible, steps * TARIFF_MONTHLY * 12, 0.0) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + year=st.sampled_from(YEARS), +) +def test_capital_yield_is_counted_once_as_tariff_income(units, year): + values = calculate(units, year) + eligible = values["is_uc_eligible"].astype(bool) + tariff = reference_tariff_income(values["uc_assessable_capital"], eligible) + np.testing.assert_allclose( + values["uc_tariff_income"], tariff, atol=0.01, err_msg=str(units) + ) + np.testing.assert_allclose( + values["uc_unearned_income"], + tariff + values["other_listed_sources"], + atol=0.01, + err_msg=str(units), + ) + + +@PROPERTY_SETTINGS +@given( + 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_do_not_enter_the_means_test(units, scale, year): + base = calculate(units, year) + scaled = calculate(units, year, income_scale=scale) + for variable in [ + "uc_unearned_income", + "uc_tariff_income", + "uc_assessable_capital", + "is_uc_eligible", + "uc_maximum_amount", + ]: + np.testing.assert_allclose( + scaled[variable], base[variable], atol=0.01, err_msg=f"{variable}: {units}" + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(with_earnings=False), 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) + for variable in UC_VARIABLES: + np.testing.assert_allclose( + scaled[variable], base[variable], atol=0.01, err_msg=f"{variable}: {units}" + ) + + +@PROPERTY_SETTINGS +@given( + units=st.lists(families(), min_size=1, max_size=20), + source=st.sampled_from(CAPITAL_SOURCES), + bump=st.floats(0, 20_000, allow_nan=False, allow_infinity=False), + year=st.sampled_from(YEARS), +) +def test_uc_is_non_increasing_in_capital(units, source, bump, year): + low = calculate(units, year) + high = calculate(units, year, capital_bump=(source, bump)) + for variable in ["universal_credit", "universal_credit_pre_benefit_cap"]: + assert np.all(high[variable] <= low[variable] + 0.01), (variable, units) + both_eligible = low["is_uc_eligible"].astype(bool) & high["is_uc_eligible"].astype( + bool + ) + assert np.all( + high["uc_tariff_income"][both_eligible] + >= low["uc_tariff_income"][both_eligible] - 0.01 + ), units + # More capital never makes an ineligible family eligible. + assert not np.any( + high["is_uc_eligible"].astype(bool) & ~low["is_uc_eligible"].astype(bool) + ), 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 + # respect of the employment. The 30,000 of dividends is taxed, but that + # tax is not in respect of the employment (reg. 55(5)(b)), so earned + # income stays 10,000. + unit = dict( + ages=[30], + children=[], + tenure="OWNED_OUTRIGHT", + region="LONDON", + rent=0.0, + capital={source: 0.0 for source in CAPITAL_SOURCES}, + main_residence_value=0.0, + reported_capital=-1.0, + earnings=10_000.0, + private_pension_income=0.0, + capital_income={ + "savings_interest_income": 0.0, + "dividend_income": 30_000.0, + "property_income": 0.0, + }, + recipient=0, + ) + values = calculate([unit], 2026) + assert values["uc_earned_income"][0] == pytest.approx(10_000, abs=0.01) diff --git a/policyengine_uk/tests/test_uc_state_pension_properties.py b/policyengine_uk/tests/test_uc_state_pension_properties.py index 9b6802274..70ee6af40 100644 --- a/policyengine_uk/tests/test_uc_state_pension_properties.py +++ b/policyengine_uk/tests/test_uc_state_pension_properties.py @@ -16,21 +16,24 @@ 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). + 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 + income from capital remains the same in all three cases. -Invariants 2 and 3 are restricted to families without earnings because the +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 3 to all families then. +when #1942 is fixed; widen invariants 2 and 4 to all families then. """ import numpy as np import pytest -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -199,11 +202,37 @@ def test_state_pension_counts_like_private_pension(units, year): 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), +@example( + units=[ + dict( + ages=[70, 60], + children=[], + tenure="RENT_FROM_COUNCIL", + rent=6_240.0, + savings=0.0, + earnings=0.0, + state_pension=9_000.0, + ) + ], + 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) + assert_same(property_income, without_income, str(units)) + pensions = np.array([unit["state_pension"] for unit in units]) + np.testing.assert_allclose( + pension["uc_unearned_income"] - property_income["uc_unearned_income"], + pensions, + atol=0.01, + err_msg=str(units), + ) + np.testing.assert_allclose( + pension["universal_credit_pre_benefit_cap"], + np.maximum(0, property_income["universal_credit_pre_benefit_cap"] - pensions), + atol=0.01, + err_msg=str(units), ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py index 82b6ec328..25f13198c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/income/uc_unearned_income.py @@ -5,8 +5,15 @@ class uc_unearned_income(Variable): value_type = float entity = BenUnit label = "Universal Credit unearned income" + documentation = ( + "Income falling within the descriptions in regulation 66(1) of the " + "Universal Credit Regulations 2013. Capital counts only through its " + "assumed yield (uc_tariff_income, regulation 72(1)); actual interest, " + "dividends and rent are not unearned income at any capital level." + ) definition_period = YEAR unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/66" def formula(benunit, period, parameters): # Members whose income counts: the claimant and partner and, as the model did @@ -18,20 +25,4 @@ def formula(benunit, period, parameters): "is_child_or_qualifying_young_person_for_universal_credit", period ) p = parameters(period).gov.dwp.universal_credit.means_test - household = benunit.household - total = add_for_members(benunit, period, p.income_definitions.unearned, members) - tariff_income_applies = benunit("uc_tariff_income", period) > 0 - reported_capital = benunit("uc_reported_capital", period) - has_reported_capital = reported_capital >= 0 - property_capital = household( - "other_residential_property_value", period - ) + household("non_residential_property_value", period) - capital_derived_income = ( - ((household("savings", period) > 0) | has_reported_capital) - * add_for_members(benunit, period, ["savings_interest_income"], members) - + ((household("corporate_wealth", period) > 0) | has_reported_capital) - * add_for_members(benunit, period, ["dividend_income"], members) - + ((property_capital > 0) | has_reported_capital) - * add_for_members(benunit, period, ["property_income"], members) - ) - return total - tariff_income_applies * capital_derived_income + return add_for_members(benunit, period, p.income_definitions.unearned, members)