From f150ae403f69a3f3b50d4df3dec892cd2c6f2cbf Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 09:49:38 -0400 Subject: [PATCH 01/24] Treat income derived from capital as capital in the legacy means tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Income Support, Housing Benefit, Pension Credit and council tax reduction counted property_income as income and deducted tax on rent, interest and dividends. The regulations treat income derived from capital as capital (IS reg 48(4) and Sch 9 para 22; HB reg 46(4) and Sch 5 para 17; HB (SPC) Sch 5 para 22; SPC Sch IV para 18; the CTR schemes' equivalents) and disregard tax only on income taken into account. Rent for letting part of the home stays income less the £20 weekly sub-tenant disregard. Co-Authored-By: Claude Opus 5.5 --- .../legacy-capital-derived-income.fixed.md | 1 + .../pension_age.yaml | 22 +++++++++++ .../working_age.yaml | 30 ++++++++++++++ .../guarantee_credit/income.yaml | 8 +++- .../housing_benefit_applicable_income.yaml | 7 +++- .../pension_credit/pension_credit_income.yaml | 13 +++++-- .../housing_benefit_applicable_income.py | 23 ++++++++++- .../dwp/income_support_applicable_income.py | 21 +++++++++- .../legacy_benefits_home_letting_income.py | 39 +++++++++++++++++++ .../gov/dwp/legacy_means_test_income_tax.py | 31 +++++++++++++++ .../pension_credit/pension_credit_income.py | 6 ++- ...council_tax_reduction_applicable_income.py | 39 ++++++++++++++++++- 12 files changed, 227 insertions(+), 13 deletions(-) create mode 100644 changelog.d/legacy-capital-derived-income.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/working_age.yaml create mode 100644 policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py create mode 100644 policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py diff --git a/changelog.d/legacy-capital-derived-income.fixed.md b/changelog.d/legacy-capital-derived-income.fixed.md new file mode 100644 index 0000000000..b88d34bca3 --- /dev/null +++ b/changelog.d/legacy-capital-derived-income.fixed.md @@ -0,0 +1 @@ +Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard. diff --git a/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml new file mode 100644 index 0000000000..d44087669c --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml @@ -0,0 +1,22 @@ +description: > + Weekly amount disregarded from the rent a claimant over the qualifying age + for State Pension Credit receives from a person who occupies part of a + property the claimant lives in, under an agreement to pay rent; rent below + this amount is disregarded in full. Pension Credit and pension-age Housing + Benefit use it, and the pensioner council tax reduction schemes in England, + Wales and Scotland copy it. +metadata: + label: Legacy benefits pension-age sub-tenant rent disregard + period: week + unit: currency-GBP + reference: + - title: The State Pension Credit Regulations 2002 Schedule IV para. 9 + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/9 + - title: The State Pension Credit Regulations 2002 Schedule IV para. 9 (as made) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/made + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 5 para. 10 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/10 + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 5 para. 10 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/10 +values: + 2003-10-06: 20 diff --git a/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/working_age.yaml b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/working_age.yaml new file mode 100644 index 0000000000..af9db8f7bc --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/working_age.yaml @@ -0,0 +1,30 @@ +description: > + Weekly amount disregarded from the rent a working-age claimant receives from + a person who occupies part of the claimant's home under a contractual + liability to pay for it, other than board and lodging; rent below this + amount is disregarded in full. Income Support, income-based Jobseeker's + Allowance, income-related Employment and Support Allowance and working-age + Housing Benefit use it, and the working-age council tax reduction schemes + in Wales and the English default scheme copy it. Before April 2008 the + disregard was £4 plus a further amount where the payment included heating; + the heating amount is not modelled. +metadata: + label: Legacy benefits working-age sub-tenant rent disregard + period: week + unit: currency-GBP + reference: + - title: The Income Support (General) Regulations 1987 Schedule 9 para. 19 + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/19 + - title: The Income Support (General) Regulations 1987 Schedule 9 para. 19 (as made) + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/made + - title: The Housing Benefit Regulations 2006 Schedule 5 para. 22 + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/22 + - title: The Jobseeker's Allowance Regulations 1996 Schedule 7 para. 20 + href: https://www.legislation.gov.uk/uksi/1996/207/schedule/7/paragraph/20 + - title: The Employment and Support Allowance Regulations 2008 Schedule 8 para. 20 + href: https://www.legislation.gov.uk/uksi/2008/794/schedule/8/paragraph/20 + - title: The Social Security (Miscellaneous Amendments) (No. 5) Regulations 2007 regs. 1(2), 1(3), 5(12) and 11(12) + href: https://www.legislation.gov.uk/uksi/2007/2618/regulation/1/made +values: + 1988-04-11: 4 + 2008-04-07: 20 diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml index fd57b6d800..32dd383233 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/income.yaml @@ -7,6 +7,8 @@ metadata: title: State Pension Credit Act 2002 s. 15(1) - href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/15 title: State Pension Credit Regulations 2002 reg. 15 + - href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/18 + title: State Pension Credit Regulations 2002 Schedule IV para. 18 (actual income from capital disregarded) unit: list values: 2002-01-01: @@ -14,7 +16,11 @@ values: - working_tax_credit - state_pension - private_pension_income - - property_income + # Rent from other property is actual income from capital, disregarded by + # Schedule IV para. 18; the property counts through deemed income. Rent for + # part of the claimant's home is reg. 15(5)(i) income, less the Schedule IV + # para. 9 disregard. + - legacy_benefits_home_letting_income - pension_credit_deemed_income - esa_contrib - jsa_contrib diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml index 135bd6cebc..615309248a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml @@ -70,7 +70,10 @@ output: housing_benefit_applicable_income: 2_500 -- name: Added Tax and income +- name: Added tax credits; rent from let property is capital, not income + # HB Regs 2006 reg 46(4) treats income derived from capital as capital and + # Sch 5 para 17 disregards it, so the £1,000 of property income does not + # count: 1,000 child benefit + 2,250 IS + 1,000 tax credits - 1,500 / 2. period: 2025 input: # Benefits @@ -92,7 +95,7 @@ housing_benefit_applicable_income_disregard: 0 housing_benefit_applicable_income_childcare_element: 0 output: - housing_benefit_applicable_income: 4_500 + housing_benefit_applicable_income: 3_500 - name: Pension contributions capped at 0 period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml index b6cf5c9493..154382ab89 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml @@ -59,7 +59,12 @@ pension_credit_deemed_income: 52 pension_credit_income: 52 -- name: Case 4, property income and non-main-home capital count for Pension Credit. +- name: Case 4, rent from a let property is not Pension Credit income; the property counts through deemed income. + # SPC Regs 2002 Sch IV para 18 disregards any actual income from capital, so + # the £10,476 of rent does not count. Deemed income (reg 15(6)): capital + # 55,825 - 10,000 = 45,825, ceil(45,825 / 500) = 92, so £92 a week, 4,784 a + # year. Guarantee credit = (238 - 92) x 52 = 7,592. Savings credit is nil, as + # qualifying income (£92 a week) is below the savings credit threshold. period: 2026 absolute_error_margin: 0.01 input: @@ -78,8 +83,10 @@ output: pension_credit_assessable_capital: 55_825 pension_credit_deemed_income: 4_784 - pension_credit_income: 15_260 - pension_credit: 0 + pension_credit_income: 4_784 + guarantee_credit: 7_592 + savings_credit: 0 + pension_credit: 7_592 - name: Case 5, 2026 Pension Credit uses tariff income with the published guarantee rate. period: 2026 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 999da75d2a..c8a2f81118 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -5,8 +5,27 @@ class housing_benefit_applicable_income(Variable): value_type = float entity = BenUnit label = "relevant income for Housing Benefit means test" + documentation = ( + "Income taken into account for Housing Benefit. Income derived from " + "capital, such as rent from property, interest and dividends, is not " + "income: for working-age claimants regulation 46(4) treats it as " + "capital and Schedule 5 paragraph 17 disregards it, and for claimants " + "over the qualifying age for State Pension Credit Schedule 5 paragraph " + "22 of the pension-age regulations disregards any actual income from " + "capital. The capital counts through the capital limit and tariff " + "income. Tax on that income is not deducted. Rent for letting part of " + "the home stays income, less the sub-tenant disregard. Rent from other " + "premises whose value is disregarded also stays income, but the model " + "cannot identify those premises." + ) definition_period = YEAR unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/2006/213/regulation/46", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/5", + ] def formula(benunit, period, parameters): any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) @@ -30,8 +49,8 @@ def formula(benunit, period, parameters): INCOME_COMPONENTS = [ "employment_income", "self_employment_income", - "property_income", "private_pension_income", + "legacy_benefits_home_letting_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income # Add personal benefits, credits and total benefits to income @@ -46,7 +65,7 @@ def formula(benunit, period, parameters): increased_income -= add(benunit, period, ["basic_income"]) # Reduce increased income by pension contributions and tax pension_contributions = add(benunit, period, ["pension_contributions"]) * 0.5 - TAX_COMPONENTS = ["income_tax", "national_insurance"] + TAX_COMPONENTS = ["legacy_means_test_income_tax", "national_insurance"] tax = add(benunit, period, TAX_COMPONENTS) increased_income_reduced_by_tax_and_pensions = ( increased_income - tax - pension_contributions diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index 5f6909cfb8..8dd78b738c 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -5,16 +5,33 @@ class income_support_applicable_income(Variable): value_type = float entity = BenUnit label = "Relevant income for Income Support means test" + documentation = ( + "Income taken into account for Income Support. Income derived from " + "capital, such as rent from property, interest and dividends, is " + "treated as capital from the date it is due (regulation 48(4)) and " + "disregarded as income (Schedule 9 paragraph 22); the capital counts " + "through the capital limit and tariff income. Tax on that income is " + "not deducted, as Schedule 9 paragraph 1 disregards tax only on income " + "taken into account. Rent for letting part of the home stays income, " + "less the paragraph 19 disregard. Rent from other premises whose value " + "is disregarded (Schedule 10 paragraphs 2, 4 and 25 to 28) also stays " + "income, but the model cannot identify those premises." + ) definition_period = YEAR unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/40", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/48", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9", + ] def formula(benunit, period, parameters): IS = parameters(period).gov.dwp.income_support INCOME_COMPONENTS = [ "employment_income", "self_employment_income", - "property_income", "private_pension_income", + "legacy_benefits_home_letting_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: @@ -23,7 +40,7 @@ def formula(benunit, period, parameters): tax = add( benunit, period, - ["income_tax", "national_insurance"], + ["legacy_means_test_income_tax", "national_insurance"], ) income += add(benunit, period, ["social_security_income"]) income += benunit("income_support_tariff_income", period) diff --git a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py new file mode 100644 index 0000000000..5ebe2d5a59 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * + + +class legacy_benefits_home_letting_income(Variable): + value_type = float + entity = BenUnit + label = "Rent from letting part of the home counted in the legacy means tests" + documentation = ( + "Rent the benefit unit receives for letting part of the home it lives " + "in (sublet_income), after the weekly sub-tenant disregard. The legacy " + "means tests treat other income derived from capital, such as rent from " + "other property, interest and dividends, as capital, but the home is " + "disregarded capital, so rent for part of it stays income: Income " + "Support Schedule 9 paragraph 19, Housing Benefit Schedule 5 paragraph " + "22, and for claimants over the qualifying age for State Pension Credit " + "Pension Credit regulation 15(5)(i) with Schedule IV paragraph 9 and " + "pension-age Housing Benefit regulation 29(1)(v) with Schedule 5 " + "paragraph 10. The disregard applies per occupier; the data give one " + "annual amount and no count of occupiers, so the model assumes one. " + "Board and lodging, which has its own disregard (£20 and half the " + "excess), is not modelled." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/19", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/22", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/5/paragraph/10", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/15", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/9", + ] + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.legacy_means_tests.sub_tenant_rent_disregard + weekly_rent = add(benunit, period, ["sublet_income"]) / WEEKS_IN_YEAR + pension_age = benunit.any(benunit.members("is_SP_age", period)) + disregard = where(pension_age, p.pension_age, p.working_age) + return max_(0, weekly_rent - disregard) * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py new file mode 100644 index 0000000000..042addb578 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py @@ -0,0 +1,31 @@ +from policyengine_uk.model_api import * + + +class legacy_means_test_income_tax(Variable): + value_type = float + entity = Person + label = "Income Tax deducted in the legacy means tests" + documentation = ( + "Income Tax less the tax on savings interest, dividends and property " + "income, floored at zero. The legacy means tests disregard tax only on " + "income they take into account, and they treat income derived from " + "capital as capital, not income, so tax on it is not deducted. Tax " + "reductions are set against the remaining tax first." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/1", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/1", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/33", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/17", + ] + + def formula(person, period, parameters): + tax_on_income_from_capital = add( + person, + period, + ["savings_income_tax", "dividend_income_tax", "property_income_tax"], + ) + return max_(0, person("income_tax", period) - tax_on_income_from_capital) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index df44d712f8..5ba12ad5f3 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -17,7 +17,11 @@ def formula(benunit, period, parameters): if bi.interactions.include_in_means_tests: total += add(benunit, period, ["basic_income"]) pension_contributions = add(benunit, period, ["pension_contributions"]) - tax = add(benunit, period, ["income_tax", "national_insurance"]) + # Reg 17(10)(a) disregards tax only on income taken into account, and + # Schedule IV paragraph 18 disregards actual income from capital. + tax = add( + benunit, period, ["legacy_means_test_income_tax", "national_insurance"] + ) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate return max_(0, total - deductions) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 8cf5f764b8..0cc16616d1 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -1,12 +1,38 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_scotland_scheme, +) class council_tax_reduction_applicable_income(Variable): value_type = float entity = BenUnit label = "relevant income for Council Tax Reduction means test" + documentation = ( + "Income taken into account for council tax reduction. No scheme counts " + "income derived from capital, such as rent from property, interest and " + "dividends, as income, so it is not listed and tax on it is not " + "deducted. The pensioner schemes disregard any actual income from " + "capital; the Welsh working-age scheme and the English default scheme " + "treat it as capital; the Scottish working-age scheme counts only the " + "unearned income it lists, which includes the assumed yield from " + "capital but not actual rent, interest or dividends. Rent for letting " + "part of the home counts, less the sub-tenant disregard, except in the " + "Scottish working-age scheme, which does not list it." + ) definition_period = YEAR unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/16", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/64", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/4", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/9", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/3", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/57", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/63", + ] def formula(benunit, period, parameters): benunit_means_tested_benefits = [ @@ -29,7 +55,6 @@ def formula(benunit, period, parameters): income_components = [ "employment_income", "self_employment_income", - "property_income", "private_pension_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income @@ -38,10 +63,20 @@ def formula(benunit, period, parameters): personal_benefit_income = add(benunit, period, personal_benefits) credits = add(benunit, period, ["tax_credits"]) increased_income = income + personal_benefit_income + credits + benefits + scotland_working_age = is_scotland_scheme( + benunit.household("country", period) + ) & ~benunit.household("council_tax_reduction_household_has_pensioner", period) + increased_income += where( + scotland_working_age, + 0, + benunit("legacy_benefits_home_letting_income", period), + ) if not bi.interactions.include_in_means_tests: increased_income -= add(benunit, period, ["basic_income"]) pension_contributions = add(benunit, period, ["pension_contributions"]) * 0.5 - tax = add(benunit, period, ["income_tax", "national_insurance"]) + tax = add( + benunit, period, ["legacy_means_test_income_tax", "national_insurance"] + ) return max_(0, increased_income - tax - pension_contributions) From 58e8cdc3e6fbdce083212aadf10807e000723edb Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 10:07:21 -0400 Subject: [PATCH 02/24] Test income from capital in the legacy means tests Hand-computed YAML cases for Income Support, working- and pension-age Housing Benefit, Pension Credit and the English, Welsh and Scottish council tax reduction schemes, and Hypothesis properties: invariance to income from capital, home-letting bounds and monotonicity, and bounds on the tax the means tests deduct. Co-Authored-By: Claude Opus 5.5 --- .../income_from_capital.yaml | 304 ++++++++++++++++++ ...ans_test_income_from_capital_properties.py | 217 +++++++++++++ 2 files changed, 521 insertions(+) create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml create mode 100644 policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml new file mode 100644 index 0000000000..63a7582245 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -0,0 +1,304 @@ +# Income derived from capital in the legacy means tests. +# +# Income Support, Housing Benefit, Pension Credit and council tax reduction +# treat rent, interest and dividends as capital, not income (IS Regs 1987 reg +# 48(4) and Sch 9 para 22; HB Regs 2006 reg 46(4) and Sch 5 para 17; HB (SPC) +# Regs 2006 Sch 5 para 22; SPC Regs 2002 Sch IV para 18; CTR equivalents), and +# disregard tax only on income they take into account (IS Sch 9 para 1; HB Sch 5 +# para 1; HB (SPC) reg 33(12); SPC reg 17(10)(a)). Rent for letting part of the +# home stays income less £20 a week per occupier (IS Sch 9 para 19; HB Sch 5 +# para 22; HB (SPC) Sch 5 para 10; SPC Sch IV para 9). +# +# Tax figures: personal allowance £12,570; basic rate 20%; property allowance +# £1,000; personal savings allowance £1,000 for a basic-rate taxpayer; +# dividend allowance £500; dividend ordinary rate 8.75% in 2025-26 and 10.75% +# in 2026-27 (the model's rate); employee NI 8% above £12,570. + +- name: Income Support, lone parent with earnings and rent from a let property + # Earnings 10,000 use 10,000 of the personal allowance, so the remaining + # 2,570 goes against taxable rent of 5,200 - 1,000 = 4,200. Tax on rent: + # (4,200 - 2,570) x 20% = 326. Tax on earnings and NI: nil. + # Main counted the rent and deducted its tax: 10,000 + 5,200 - 326 - 1,040. + # The law counts neither: 10,000 - 1,040 (the £20 lone parent disregard). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 10_000 + property_income: 5_200 + income_support_reported: 1 + child: + age: 3 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + output: + income_tax: [326, 0] + property_income_tax: [326, 0] + legacy_means_test_income_tax: [0, 0] + income_support_applicable_income: 8_960 + +- name: Working-age Housing Benefit counts earnings net of the tax on them only + # Earnings 14,000: tax (14,000 - 12,570) x 20% = 286; NI (14,000 - 12,570) + # x 8% = 114.40. Rent 6,000: tax (6,000 - 1,000) x 20% = 1,000. Interest 500 + # is within the personal savings allowance. Dividends 1,000: (1,000 - 500) x + # 8.75% = 43.75. Income tax 1,329.75, of which 286 is on counted income. + # Applicable income: 14,000 - 286 - 114.40 - 260 (the £5 weekly disregard, + # set as an input here) = 13,339.60. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 14_000 + property_income: 6_000 + savings_interest_income: 500 + dividend_income: 1_000 + benunits: + benunit: + members: [person] + housing_benefit_applicable_income_disregard: 260 + households: + household: + members: [person] + output: + income_tax: 1_329.75 + property_income_tax: 1_000 + dividend_income_tax: 43.75 + savings_income_tax: 0 + legacy_means_test_income_tax: 286 + national_insurance: 114.40 + housing_benefit_applicable_income: 13_339.60 + +- name: Pension-age Housing Benefit counts rent from part of the home less £20 a week, not rent from other property + # State Pension 12,000 and private pension 2,000 use the personal allowance + # and leave 1,430 taxable: tax 286. Rent from other property 3,000: taxable + # 2,000, tax 400, not deducted. Sub-tenant rent 5,200 (£100 a week): HB (SPC) + # reg 29(1)(v) less Sch 5 para 10's £20, so 80 x 52 = 4,160. + # Applicable income: 14,000 + 4,160 - 286 = 17,874 (no earnings disregard, + # set to 0). Pension Credit income is the same 17,874, above the £238 x 52 = + # 12,376 guarantee, so there is no guarantee credit passport. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension: 12_000 + private_pension_income: 2_000 + property_income: 3_000 + sublet_income: 5_200 + benunits: + benunit: + members: [person] + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person] + output: + income_tax: 686 + legacy_means_test_income_tax: 286 + legacy_benefits_home_letting_income: 4_160 + pension_credit_income: 17_874 + guarantee_credit: 0 + housing_benefit_applicable_income: 17_874 + +- name: Pension Credit ignores rent from a let property + # SPC Sch IV para 18. Rent 4,000: taxable 3,000, of which 3,000 - (12,570 - + # 10,000) = 430 is taxed: 86, not deducted. Income 10,000 (State Pension). + # Guarantee credit (238 - 10,000 / 52) x 52 = 12,376 - 10,000 = 2,376. + # Savings credit: qualifying income 10,000 is below the threshold of + # 208.07 x 52 = 10,819.64, so nil. Main counted the rent less its tax: + # 13,914, which paid no guarantee credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 80 + state_pension: 10_000 + property_income: 4_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + output: + income_tax: 86 + legacy_means_test_income_tax: 0 + pension_credit_income: 10_000 + guarantee_credit: 2_376 + savings_credit: 0 + pension_credit: 2_376 + +- name: Pension Credit does not deduct tax on dividends + # State Pension 12,000 leaves 570 of the personal allowance for dividends + # 3,000: taxable 2,430 - 500 allowance = 1,930, at 10.75% = 207.475. That tax + # is on income Pension Credit does not count, so it is not deducted. + # Deemed income on savings 20,000: ceil((20,000 - 10,000) / 500) = £20 a week, + # 1,040 a year. Income 12,000 + 1,040 = 13,040, above 12,376: no guarantee + # credit. Savings credit: threshold 10,819.64; maximum 0.6 x (12,376 - + # 10,819.64) = 933.816; phase-in 0.6 x (13,040 - 10,819.64) = 1,332.22, + # capped at 933.816; less 0.4 x (13,040 - 12,376) = 265.60; 668.216. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 80 + state_pension: 12_000 + dividend_income: 3_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + savings: 20_000 + output: + dividend_income_tax: 207.475 + legacy_means_test_income_tax: 0 + pension_credit_deemed_income: 1_040 + pension_credit_income: 13_040 + guarantee_credit: 0 + savings_credit: 668.216 + pension_credit: 668.216 + +- name: English pensioner council tax reduction counts sub-tenant rent less £20, not other rent + # SI 2012/2885 Sch 1 para 16(1)(v) and Sch 5 para 10 for the sub-tenant rent: + # 2,600 / 52 = £50 a week, less £20 = 30 x 52 = 1,560. Sch 5 para 23 + # disregards actual income from capital, so the other rent 4,000 and its tax + # 486 ((15,000 - 12,570) x 20%) are left out: 12,000 + 1,560 = 13,560. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension: 12_000 + property_income: 4_000 + sublet_income: 2_600 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + region: NORTH_EAST + output: + income_tax: 486 + legacy_means_test_income_tax: 0 + council_tax_reduction_applicable_income: 13_560 + +- name: Scottish working-age council tax reduction counts neither rent from property nor sub-tenant rent + # SSI 2021/249 reg 57(1) lists unearned income in full; it includes the + # assumed yield from capital (reg 63) but no head for actual rent or rent + # from part of the home. Earnings 12,000 are below the personal allowance + # and the NI threshold, so applicable income is 12,000. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 12_000 + property_income: 3_000 + sublet_income: 2_600 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + region: SCOTLAND + output: + legacy_means_test_income_tax: 0 + council_tax_reduction_applicable_income: 12_000 + +- name: Welsh working-age council tax reduction counts sub-tenant rent less £20, not other rent + # WSI 2013/3029 Sch 6 para 27(5) treats income derived from capital as + # capital; Sch 9 para 26 disregards £20 a week of sub-tenant rent: + # 12,000 + (50 - 20) x 52 = 13,560. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 12_000 + property_income: 3_000 + sublet_income: 2_600 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + region: WALES + output: + legacy_means_test_income_tax: 0 + council_tax_reduction_applicable_income: 13_560 + +- name: Sub-tenant rent below £20 a week is disregarded in full + # 1,000 / 52 = 19.23 a week. + period: 2026 + input: + age: 40 + sublet_income: 1_000 + output: + legacy_benefits_home_letting_income: 0 + +- name: Working-age sub-tenant disregard was £4 before April 2008 + # IS Sch 9 para 19 before SI 2007/2618: £4 (the extra heating amount is not + # modelled). (100 - 4) x 52 = 4,992. + period: 2007 + input: + age: 40 + sublet_income: 5_200 + output: + legacy_benefits_home_letting_income: 4_992 + +- name: Working-age sub-tenant disregard is £20 from April 2008 + period: 2009 + input: + age: 40 + sublet_income: 5_200 + output: + legacy_benefits_home_letting_income: 4_160 + +- name: Pension-age sub-tenant disregard was already £20 before 2008 + # SPC Sch IV para 9 as made. + period: 2007 + input: + age: 70 + sublet_income: 5_200 + output: + legacy_benefits_home_letting_income: 4_160 + +- name: A couple's sub-tenant rent is treated as from one occupier + # The data give no count of occupiers, so the model assumes one: + # (2 x 2,600) / 52 = £100 a week, less £20 = 4,160. + period: 2026 + input: + people: + claimant: + age: 40 + sublet_income: 2_600 + partner: + age: 40 + sublet_income: 2_600 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + legacy_benefits_home_letting_income: 4_160 diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py new file mode 100644 index 0000000000..670e750943 --- /dev/null +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -0,0 +1,217 @@ +"""Property-based tests for income derived from capital in the legacy means tests. + +Income Support, Housing Benefit, Pension Credit and council tax reduction +treat rent, interest and dividends as capital, not income (IS Regs 1987 reg +48(4) and Sch 9 para 22; HB Regs 2006 reg 46(4) and Sch 5 para 17; HB (SPC) +Regs 2006 Sch 5 para 22; SPC Regs 2002 Sch IV para 18), and disregard tax only +on income they take into account. Rent for letting part of the home stays +income, less £20 a week per occupier. + +Invariants, for any generated population of families: + +1. Invariance: scaling property, savings interest and dividend income (by 0 + and 3) changes none of the four means-test incomes, and none of + Income Support, Housing Benefit, Pension Credit or council tax reduction. + Universal Credit and tax credits, which have their own income rules, are + held fixed. Incomes are kept within the basic rate band, where extra + income from capital cannot change the tax on other income through the + personal allowance taper, the High Income Child Benefit Charge or + Marriage Allowance. +2. Bounds: the counted home-letting income is between nil and the rent, and + equals the rent less £20 a week, floored at nil. +3. Monotone: more rent from part of the home never lowers a means-test income + and never raises Income Support, Housing Benefit, Pension Credit or council + tax reduction. +4. Tax: legacy_means_test_income_tax is between nil and income tax, and equals + income tax when there is no savings, dividend or property income. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +PROPERTY_SETTINGS = settings( + max_examples=8, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +YEARS = [2025, 2026] +REGIONS = ["NORTH_EAST", "LONDON", "WALES", "SCOTLAND", "NORTHERN_IRELAND"] +TENURES = ["RENT_FROM_COUNCIL", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] +PENSION_AGE = st.integers(67, 95) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single_pension": [("adult", PENSION_AGE)], + "couple_pension": [("adult", PENSION_AGE), ("adult", PENSION_AGE)], + "single_working": [("adult", WORKING_AGE)], + "couple_working": [("adult", WORKING_AGE), ("adult", WORKING_AGE)], + "lone_parent": [("adult", WORKING_AGE), ("child", st.integers(0, 5))], +} +MEANS_TEST_INCOMES = [ + "income_support_applicable_income", + "housing_benefit_applicable_income", + "pension_credit_income", + "council_tax_reduction_applicable_income", +] +AWARDS = ["income_support", "housing_benefit", "pension_credit", "council_tax_benefit"] +# Each adult's earnings plus pensions stay below £22,000 and each income from +# capital below £2,000, so even at three times the capital income an adult's +# income stays inside the basic rate band. +small = st.floats(0, 2_000, allow_nan=False, allow_infinity=False) + + +@st.composite +def adults(draw, age): + return dict( + age=age, + employment_income=draw(st.one_of(st.just(0.0), st.floats(0, 12_000))), + state_pension=draw(st.floats(0, 10_000)) if age >= 67 else 0.0, + private_pension_income=draw(st.one_of(st.just(0.0), st.floats(0, 2_000))), + property_income=draw(st.one_of(st.just(0.0), small)), + savings_interest_income=draw(st.one_of(st.just(0.0), small)), + dividend_income=draw(st.one_of(st.just(0.0), small)), + sublet_income=draw(st.one_of(st.just(0.0), st.floats(0, 8_000))), + ) + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(sorted(SHAPES))) + members = [] + for role, age in SHAPES[shape]: + a = draw(age) + members.append((role, draw(adults(a)) if role == "adult" else dict(age=a))) + return dict( + shape=shape, + members=members, + region=draw(st.sampled_from(REGIONS)), + tenure=draw(st.sampled_from(TENURES)), + rent=draw(st.floats(0, 15_000)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + reported=draw(st.booleans()), + universal_credit=draw(st.sampled_from([0.0, 3_000.0])), + ) + + +populations = st.lists(families(), min_size=1, max_size=4) + + +def situation(units, year, capital_income_scale=1.0, sublet_extra=0.0): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, (role, attrs) in enumerate(unit["members"]): + name = f"p{i}_{j}" + person = {"age": {year: attrs["age"]}} + if role == "adult": + for k, v in attrs.items(): + if k == "age": + continue + if k in ( + "property_income", + "savings_interest_income", + "dividend_income", + ): + v = v * capital_income_scale + if k == "sublet_income" and j == 0: + v = v + sublet_extra + person[k] = {year: v} + if j == 0 and unit["reported"]: + person["income_support_reported"] = {year: 1.0} + person["housing_benefit_reported"] = {year: 1.0} + people[name] = person + names.append(name) + benunits[f"b{i}"] = { + "members": names, + # Universal Credit and tax credits have their own income rules and + # are held fixed here. + "universal_credit": {year: unit["universal_credit"]}, + "tax_credits": {year: 0.0}, + "would_claim_uc": {year: False}, + } + households[f"h{i}"] = { + "members": names, + "region": {year: unit["region"]}, + "tenure_type": {year: unit["tenure"]}, + "rent": {year: unit["rent"]}, + "savings": {year: unit["savings"]}, + "council_tax": {year: 1_500.0}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, year, variables, **kwargs): + sim = Simulation(situation=situation(units, year, **kwargs)) + return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in variables} + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS)) +def test_income_from_capital_changes_no_legacy_means_test(units, year): + variables = MEANS_TEST_INCOMES + AWARDS + base = calculate(units, year, variables) + for scale in (0.0, 3.0): + scaled = calculate(units, year, variables, capital_income_scale=scale) + for v in variables: + np.testing.assert_allclose( + scaled[v], base[v], atol=0.01, err_msg=f"{v} at scale {scale}" + ) + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS + [2007])) +def test_home_letting_income_bounds(units, year): + sim = Simulation(situation=situation(units, year)) + counted = np.asarray(sim.calculate("legacy_benefits_home_letting_income", year)) + rent = np.asarray(sim.calculate("sublet_income", year, map_to="benunit")) + pension_age = np.asarray(sim.calculate("is_SP_age", year, map_to="benunit")) > 0 + disregard = 20 if year >= 2009 else np.where(pension_age, 20, 4) + assert np.all(counted >= 0) + assert np.all(counted <= rent + 0.01) + np.testing.assert_allclose( + counted, np.maximum(0, rent / 52 - disregard) * 52, atol=0.01 + ) + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS), st.floats(1, 10_000)) +def test_home_letting_rent_is_monotone(units, year, extra): + variables = MEANS_TEST_INCOMES + AWARDS + base = calculate(units, year, variables) + more = calculate(units, year, variables, sublet_extra=extra) + for v in MEANS_TEST_INCOMES: + assert np.all(more[v] >= base[v] - 0.01), v + for v in AWARDS: + assert np.all(more[v] <= base[v] + 0.01), v + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS)) +def test_legacy_means_test_income_tax_bounds(units, year): + variables = [ + "legacy_means_test_income_tax", + "income_tax", + "savings_income_tax", + "dividend_income_tax", + "property_income_tax", + ] + v = calculate(units, year, variables) + tax = v["legacy_means_test_income_tax"] + assert np.all(tax >= 0) + assert np.all(tax <= v["income_tax"] + 0.01) + no_capital_tax = ( + v["savings_income_tax"] + v["dividend_income_tax"] + v["property_income_tax"] + ) == 0 + np.testing.assert_allclose(tax[no_capital_tax], v["income_tax"][no_capital_tax]) + without = calculate( + units, + year, + ["income_tax", "legacy_means_test_income_tax"], + capital_income_scale=0.0, + ) + np.testing.assert_allclose( + without["legacy_means_test_income_tax"], without["income_tax"], atol=0.01 + ) From 329e8cef77ccfd3e86954b6ed552de11f6e33479 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 10:18:49 -0400 Subject: [PATCH 03/24] Describe the income-from-capital rule on the Housing Benefit docs page Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/housing-benefit.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 4f928b173d..c90b121c3c 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -40,7 +40,7 @@ "\n", "1. **Eligible rent** (`benunit_rent`): the contracted rent for the property, capped at the Local Housing Allowance rate where the LHA applies.\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", - "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household.\n", + "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household. Applicable income counts earnings, pensions and benefits net of the income tax and National Insurance on them. Income derived from capital (rent from property, interest and dividends) is not income: regulation 46(4) of the Housing Benefit Regulations 2006 treats it as capital, and Schedule 5 paragraph 22 of the pension-age regulations disregards it. The capital counts instead through `housing_benefit_tariff_income` and the capital limit. Rent for letting part of the home is the exception: it counts, less £20 a week per occupier.\n", "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `benefit_cap_reduction` if the household's total benefit income exceeds the cap and no cap exemption applies.\n", "\n", "The take-up gate (`would_claim_housing_benefit`) is applied inside `housing_benefit_pre_benefit_cap`." From 89bdbeed966c1e69f19accdd8cd72912992303ed Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:59:32 -0400 Subject: [PATCH 04/24] Address review: CTR capital, tax attribution, Scottish scheme, history - Count land and property other than the home in council tax reduction capital (national schemes and the modelled local schemes), so a let property worth more than 16,000 ends CTR now that its rent is not income (SI 2012/2885 Sch 1 para 31(1); WSI 2013/3029 reg 30). Shares stay out pending the pension-wealth split (#1936). - Deduct only the Step 4 tax on counted income (ITA 2007 s.23), less its pro-rata share of Step 6 reductions; Step 7 charges (HICBC, annual allowance charge; s.30) are not tax on counted income. - Select the Scottish working-age scheme under SSI 2021/249 reg 3 (under pensionable age, or over it with UC or a qualifying income-related benefit) and date its exclusion of sub-tenant rent from 1 April 2022; before then SSI 2012/303 Sch 4 para 25 counted it less 20. - Count only the claimant's and partner's sublet rent; give the pension-age disregard its pre-October-2003 value (4) so early years compute. - Update #1909's CTR property test to the new income and capital rules, hold Marriage Allowance fixed in the invariance property, qualify the isolated HB fixture, and add YAML cases for each fix. Co-Authored-By: Claude Opus 5.5 --- .../pension_age.yaml | 7 +- .../capital_sources.yaml | 30 +++ ...orking_age_counts_home_letting_income.yaml | 21 ++ .../housing_benefit_applicable_income.yaml | 9 +- .../income_from_capital.yaml | 232 ++++++++++++++++++ .../council_tax_reduction_pension_credit.yaml | 14 +- ...tax_reduction_pension_credit_properties.py | 24 +- ...ans_test_income_from_capital_properties.py | 16 +- .../legacy_benefits_home_letting_income.py | 38 +-- .../gov/dwp/legacy_means_test_income_tax.py | 55 ++++- .../council_tax_reduction/_legacy.py | 2 +- ...council_tax_reduction_applicable_income.py | 37 ++- ...ouncil_tax_reduction_assessable_capital.py | 19 +- ...gston_upon_thames_council_tax_reduction.py | 2 +- .../oxford_council_tax_reduction.py | 2 +- .../westminster_council_tax_reduction.py | 2 +- 16 files changed, 448 insertions(+), 62 deletions(-) create mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/working_age_counts_home_letting_income.yaml diff --git a/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml index d44087669c..135bc3414d 100644 --- a/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml +++ b/policyengine_uk/parameters/gov/dwp/legacy_means_tests/sub_tenant_rent_disregard/pension_age.yaml @@ -4,7 +4,9 @@ description: > property the claimant lives in, under an agreement to pay rent; rent below this amount is disregarded in full. Pension Credit and pension-age Housing Benefit use it, and the pensioner council tax reduction schemes in England, - Wales and Scotland copy it. + Wales and Scotland copy it. Before Pension Credit began on 6 October 2003 + there was no separate pension-age amount; Income Support, including for + claimants over 60, disregarded £4 (Schedule 9 paragraph 19 as made). metadata: label: Legacy benefits pension-age sub-tenant rent disregard period: week @@ -12,6 +14,8 @@ metadata: reference: - title: The State Pension Credit Regulations 2002 Schedule IV para. 9 href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/paragraph/9 + - title: The Income Support (General) Regulations 1987 Schedule 9 para. 19 (as made) + href: https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/made - title: The State Pension Credit Regulations 2002 Schedule IV para. 9 (as made) href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV/made - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 5 para. 10 @@ -19,4 +23,5 @@ metadata: - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 5 para. 10 href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/10 values: + 1988-04-11: 4 2003-10-06: 20 diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml new file mode 100644 index 0000000000..2d11082d6e --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml @@ -0,0 +1,30 @@ +description: > + Household capital counted for the council tax reduction capital limit and + tariff income, outside the Pension Credit routes. Every scheme counts the + whole of the applicant's capital unless a schedule disregards it, including + land and property other than the home: the prescribed requirements for + pensioners in England (Schedule 1 paragraph 31(1)), Wales (Schedule 1) and + Scotland (regulation 40 onwards), the English default scheme for working + age and the Welsh and Scottish working-age schemes. `main_residence_value` + is left out because the home is disregarded. `corporate_wealth` is also left + out for now: in the data it is mostly imputed pension wealth, which the + schemes disregard, and it cannot yet be separated (issue 1936). +metadata: + label: Council tax reduction capital sources + unit: list + period: year + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 reg. 11(2) and Schedule 1 para. 31 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/31 + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012, Schedule + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 reg. 30 + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/30 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 40 + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/40 +values: + 2013-04-01: + - savings + - owned_land + - other_residential_property_value + - non_residential_property_value diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/working_age_counts_home_letting_income.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/working_age_counts_home_letting_income.yaml new file mode 100644 index 0000000000..56fee06843 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/working_age_counts_home_letting_income.yaml @@ -0,0 +1,21 @@ +description: > + Whether the Scottish council tax reduction scheme for people under pension + age counts rent from letting part of the applicant's home as income. The + Council Tax Reduction (Scotland) Regulations 2012 did, less £20 a week + (Schedule 4 paragraph 25). The Council Tax Reduction (Scotland) Regulations + 2021, in force from 1 April 2022, list unearned income in full in + regulation 57(1), and the list has no head for it. +metadata: + label: Scotland working-age council tax reduction counts home-letting income + unit: bool + period: year + reference: + - title: The Council Tax Reduction (Scotland) Regulations 2012 Schedule 4 para. 25 (as at 1 April 2021) + href: https://www.legislation.gov.uk/ssi/2012/303/schedule/4/2021-04-01 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 1 (commencement, 1 April 2022) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/1 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 57 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/57 +values: + 2013-04-01: true + 2022-04-01: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml index 615309248a..29588ade54 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml @@ -71,9 +71,12 @@ housing_benefit_applicable_income: 2_500 - name: Added tax credits; rent from let property is capital, not income - # HB Regs 2006 reg 46(4) treats income derived from capital as capital and - # Sch 5 para 17 disregards it, so the £1,000 of property income does not - # count: 1,000 child benefit + 2,250 IS + 1,000 tax credits - 1,500 / 2. + # An isolated check of the formula's components, not a full assessment: in + # law an Income Support recipient's whole income is disregarded (Sch 5 + # para 4), as is child benefit (para 65). HB Regs 2006 reg 46(4) treats + # income derived from capital as capital and Sch 5 para 17 disregards it, so + # the £1,000 of property income does not count: 1,000 child benefit + 2,250 + # IS + 1,000 tax credits - 1,500 / 2. period: 2025 input: # Benefits diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 63a7582245..2a55b678d1 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -302,3 +302,235 @@ members: [claimant, partner] output: legacy_benefits_home_letting_income: 4_160 + +- name: A dependant's sublet rent is not the claimant's + # legacy_benefits_home_letting_income counts the claimant's and partner's + # rent only. + period: 2026 + input: + people: + parent: + age: 40 + young_person: + age: 17 + current_education: UPPER_SECONDARY + sublet_income: 5_200 + benunits: + benunit: + members: [parent, young_person] + households: + household: + members: [parent, young_person] + output: + legacy_benefits_home_letting_income: 0 + +- name: Before Pension Credit, the £4 disregard applied at every age + # IS Regs 1987 Sch 9 para 19 as made; the pension-age £20 starts with + # Pension Credit on 6 October 2003. (100 - 4) x 52 = 4,992. + period: 2002 + input: + age: 70 + sublet_income: 5_200 + output: + legacy_benefits_home_letting_income: 4_992 + +- name: Tax reductions are shared between counted income and income from capital + # ITA 2007 s.23: Step 4 tax is 286 on earnings ((14,000 - 12,570) x 20%) and + # 1,000 on rent ((6,000 - 1,000) x 20%), 1,286 in all; Step 6 deducts a + # 1,000 tax reduction. Neither tax law nor the benefit regulations say which + # income it relieves, so the model shares it in proportion to the Step 4 + # tax: the rent's share is 1,000 x 1,000 / 1,286 = 777.60, leaving 222.40 of + # tax on rent. Income tax 286 less 222.40 = 63.60 is tax on the earnings, + # and HB applicable income is 14,000 - 63.60 - 114.40 (NI) - 260 = 13,562.00. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 14_000 + property_income: 6_000 + other_tax_credits: 1_000 + benunits: + benunit: + members: [person] + housing_benefit_applicable_income_disregard: 260 + households: + household: + members: [person] + output: + income_tax: 286 + legacy_means_test_income_tax: 63.605 + housing_benefit_applicable_income: 13_561.995 + +- name: Step 7 charges are not tax on counted income + # The High Income Child Benefit Charge is added at Step 7 of ITA 2007 s.23 + # (s.30(1)); it is not tax calculated on any income the means tests count. + # Earnings 20,000: (20,000 - 12,570) x 20% = 1,486. Income tax with a 500 + # charge is 1,986; 1,486 is deducted. + period: 2025 + input: + age: 40 + employment_income: 20_000 + CB_HITC: 500 + output: + income_tax: 1_986 + legacy_means_test_income_tax: 1_486 + +- name: Working-age Housing Benefit award with rent from a let property + # A continuing award (2025-26). Earnings 10,000 use 10,000 of the personal + # allowance; taxable rent 5,000 - 1,000 = 4,000, of which 4,000 - 2,570 = + # 1,430 is taxed: 286, all on rent, so none is deducted. NI is nil below + # 12,570. Applicable income: 10,000 - 260 (the £5 disregard, as an input) = + # 9,740. With the single 25-or-over personal allowance of £92.05 a week + # (4,786.60, an input here), HB = 6,000 - 65% x (9,740 - 4,786.60) = + # 6,000 - 3,219.71 = 2,780.29. Counting the rent would leave nothing. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 10_000 + property_income: 5_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [person] + would_claim_uc: false + housing_benefit_applicable_income_disregard: 260 + housing_benefit_applicable_amount: 4_786.60 + households: + household: + members: [person] + tenure_type: RENT_FROM_COUNCIL + rent: 6_000 + region: NORTH_EAST + output: + property_income_tax: 286 + legacy_means_test_income_tax: 0 + housing_benefit_applicable_income: 9_740 + housing_benefit: 2_780.29 + +- name: An English pensioner with a let property worth more than £16,000 gets no council tax reduction + # The rent no longer counts as income, but the property counts as capital: + # SI 2012/2885 Sch 1 para 31(1) ("the whole of his capital") and reg 11(2) + # (no reduction above 16,000). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 14_000 + property_income: 7_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + other_residential_property_value: 100_000 + output: + council_tax_reduction_assessable_capital: 100_000 + council_tax_reduction: 0 + +- name: A Welsh working-age applicant with a let property worth more than £16,000 gets no council tax reduction + # WSI 2013/3029 reg 30: no reduction where capital exceeds 16,000. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 9_000 + property_income: 5_200 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + household: + members: [person] + country: WALES + local_authority: CARDIFF + tenure_type: RENT_FROM_COUNCIL + rent: 5_000 + council_tax: 1_500 + other_residential_property_value: 50_000 + output: + council_tax_reduction_assessable_capital: 50_000 + council_tax_reduction_applicable_income: 9_000 + council_tax_reduction: 0 + +- name: A Scottish mixed-age couple on Universal Credit is under the working-age scheme + # SSI 2021/249 reg 3(1)(b)(ii): it applies where the applicant has reached + # pensionable age but the applicant or partner has an award of UC. Its reg + # 57(1) has no head for sub-tenant rent, so only the UC (1,000, an input) + # counts here. + period: 2026 + input: + people: + claimant: + age: 70 + sublet_income: 5_200 + partner: + age: 40 + benunits: + benunit: + members: [claimant, partner] + universal_credit: 1_000 + households: + household: + members: [claimant, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + legacy_benefits_home_letting_income: 4_160 + council_tax_reduction_applicable_income: 1_000 + +- name: Before April 2022 the Scottish working-age scheme counted sub-tenant rent less £20 + # SSI 2012/303 Sch 4 para 25 (as at 1 April 2021): (100 - 20) x 52 = 4,160. + period: 2021 + input: + people: + person: + age: 40 + sublet_income: 5_200 + benunits: + benunit: + members: [person] + universal_credit: 0 + households: + household: + members: [person] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + council_tax_reduction_applicable_income: 4_160 + +- name: From April 2022 the Scottish working-age scheme does not count sub-tenant rent + # SSI 2021/249, in force 1 April 2022 (reg 1); reg 57(1) has no head for it. + period: 2023 + input: + people: + person: + age: 40 + sublet_income: 5_200 + benunits: + benunit: + members: [person] + universal_credit: 0 + households: + household: + members: [person] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + council_tax_reduction_applicable_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml index 6e52f66d3d..2c8539c671 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml @@ -474,7 +474,7 @@ council_tax_reduction_assessable_capital: 17_000 council_tax_reduction: 0 -- name: The same pensioner not claiming Pension Credit is tested on household savings only +- name: The same pensioner not claiming Pension Credit is tested on savings and the second property period: 2026 absolute_error_margin: 0.01 input: @@ -499,13 +499,13 @@ other_residential_property_value: 7_000 output: in_receipt_of_savings_credit_only: false - # Pensioners without Pension Credit are tested on household savings, a - # proxy that leaves out the second property the schemes' own capital rules - # count (SI 2012/2885 Sch 1 Part 6 Chapter 3). So, unlike the claimant - # above, this pensioner keeps CTR: the gap is in the proxy, not the law. - council_tax_reduction_assessable_capital: 10_000 + # SI 2012/2885 Sch 1 para 31(1) counts "the whole of his capital", so the + # second property counts as it does for the claimant above: 10,000 + + # 7,000 = 17,000, over the 16,000 limit (reg 11(2)). Shares and other + # investments are still left out of the proxy (issue 1936). + council_tax_reduction_assessable_capital: 17_000 council_tax_reduction_applicable_income: 13_314 - council_tax_reduction: 1_999.60 + council_tax_reduction: 0 - name: Capital of exactly 16,000 is within the limit for a pensioner without Pension Credit period: 2026 diff --git a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py index e32f8363e7..9ff7b15eef 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py @@ -21,9 +21,9 @@ 3. Savings-credit-only recipients get max(0, L - 0.2 x max(0, PC income + SC - A) - ND) when the Pension Credit assessment of capital is within 16,000, and nothing otherwise. -4. Differential: everyone else keeps the previous formula, max(0, L - 0.2 x - max(0, I - A) - ND) when household savings are within 16,000, where I is the - income definition on main before this change, recomputed here from its +4. Differential: everyone else keeps the general formula, max(0, L - 0.2 x + max(0, I - A) - ND) when household savings and other property are within + 16,000, where I is the general income definition, recomputed here from its components rather than read from the variable under test. 5. Metamorphic: a guarantee credit recipient's CTR does not change when the State Pension changes. @@ -258,13 +258,15 @@ def situation( "pension_credit_income", "pension_credit_assessable_capital", ] -# council_tax_reduction_applicable_income on main before this change (1c5b4d04): -# these incomes and benefits, less income tax, National Insurance and half of -# pension contributions, floored at zero. +# council_tax_reduction_applicable_income outside the Pension Credit routes: +# these incomes and benefits, less the income tax on them, National Insurance +# and half of pension contributions, floored at zero. Rent from property, +# interest and dividends are income from capital and do not count, and nor +# does the tax on them; rent for part of the home counts less £20 a week. MAIN_INCOME_COMPONENTS = [ "employment_income", "self_employment_income", - "property_income", + "legacy_benefits_home_letting_income", "private_pension_income", "carers_allowance", "esa_contrib", @@ -281,11 +283,12 @@ def situation( "esa_income", "universal_credit", ] -MAIN_DEDUCTIONS = ["income_tax", "national_insurance"] +MAIN_DEDUCTIONS = ["legacy_means_test_income_tax", "national_insurance"] HOUSEHOLD_VARIABLES = [ "council_tax_reduction_maximum_eligible_liability", "council_tax_reduction_household_has_pensioner", "savings", + "other_residential_property_value", ] @@ -394,8 +397,11 @@ def check_routes(units, values): abs(values["council_tax_reduction_applicable_income"][i] - income) < 0.01 ), unit + capital = ( + values["savings"][i] + values["other_residential_property_value"][i] + ) expected = tapered(liability, income, applicable_amount, non_dep) * ( - values["savings"][i] <= CAPITAL_LIMIT + capital <= CAPITAL_LIMIT ) assert abs(ctr - expected) < 0.01, (unit, ctr, expected) diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 670e750943..81f4b05fa7 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -15,15 +15,20 @@ Universal Credit and tax credits, which have their own income rules, are held fixed. Incomes are kept within the basic rate band, where extra income from capital cannot change the tax on other income through the - personal allowance taper, the High Income Child Benefit Charge or - Marriage Allowance. + personal allowance taper or the High Income Child Benefit Charge. + Marriage Allowance is held at nil: whether a spouse can transfer it + depends on their income including income from capital, which is a + legitimate way for income from capital to change the tax on counted + income. 2. Bounds: the counted home-letting income is between nil and the rent, and equals the rent less £20 a week, floored at nil. 3. Monotone: more rent from part of the home never lowers a means-test income and never raises Income Support, Housing Benefit, Pension Credit or council tax reduction. -4. Tax: legacy_means_test_income_tax is between nil and income tax, and equals - income tax when there is no savings, dividend or property income. +4. Tax: legacy_means_test_income_tax is between nil and both income tax and + the tax on counted income before reductions (earned_income_tax), and + equals income tax when there is no savings, dividend or property income + and no Step 7 charge. """ import numpy as np @@ -119,6 +124,7 @@ def situation(units, year, capital_income_scale=1.0, sublet_extra=0.0): if k == "sublet_income" and j == 0: v = v + sublet_extra person[k] = {year: v} + person["marriage_allowance"] = {year: 0.0} if j == 0 and unit["reported"]: person["income_support_reported"] = {year: 1.0} person["housing_benefit_reported"] = {year: 1.0} @@ -194,6 +200,7 @@ def test_legacy_means_test_income_tax_bounds(units, year): variables = [ "legacy_means_test_income_tax", "income_tax", + "earned_income_tax", "savings_income_tax", "dividend_income_tax", "property_income_tax", @@ -202,6 +209,7 @@ def test_legacy_means_test_income_tax_bounds(units, year): tax = v["legacy_means_test_income_tax"] assert np.all(tax >= 0) assert np.all(tax <= v["income_tax"] + 0.01) + assert np.all(tax <= v["earned_income_tax"] + 0.01) no_capital_tax = ( v["savings_income_tax"] + v["dividend_income_tax"] + v["property_income_tax"] ) == 0 diff --git a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py index 5ebe2d5a59..2cf125e315 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py +++ b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py @@ -6,19 +6,22 @@ class legacy_benefits_home_letting_income(Variable): entity = BenUnit label = "Rent from letting part of the home counted in the legacy means tests" documentation = ( - "Rent the benefit unit receives for letting part of the home it lives " - "in (sublet_income), after the weekly sub-tenant disregard. The legacy " - "means tests treat other income derived from capital, such as rent from " - "other property, interest and dividends, as capital, but the home is " - "disregarded capital, so rent for part of it stays income: Income " - "Support Schedule 9 paragraph 19, Housing Benefit Schedule 5 paragraph " - "22, and for claimants over the qualifying age for State Pension Credit " - "Pension Credit regulation 15(5)(i) with Schedule IV paragraph 9 and " - "pension-age Housing Benefit regulation 29(1)(v) with Schedule 5 " - "paragraph 10. The disregard applies per occupier; the data give one " - "annual amount and no count of occupiers, so the model assumes one. " - "Board and lodging, which has its own disregard (£20 and half the " - "excess), is not modelled." + "Rent the claimant and any partner receive for letting part of the home " + "they live in (sublet_income), after the weekly sub-tenant disregard. " + "The legacy means tests treat other income derived from capital, such as " + "rent from other property, interest and dividends, as capital, but the " + "home is disregarded capital, so rent for part of it stays income: " + "Income Support Schedule 9 paragraph 19, Housing Benefit Schedule 5 " + "paragraph 22, and for claimants over the qualifying age for State " + "Pension Credit Pension Credit regulation 15(5)(i) with Schedule IV " + "paragraph 9 and pension-age Housing Benefit regulation 29(1)(v) with " + "Schedule 5 paragraph 10. The disregard applies per occupier; the data " + "give one annual amount and no count of occupiers, so the model assumes " + "one. The pension-age amount applies when the claimant or partner is " + "over State Pension age, which stands in for the qualifying age for " + "State Pension Credit; the two amounts have been equal since April " + "2008. The calculation is annual, so a year with an April change uses " + "the amount in force at the start of the year." ) definition_period = YEAR unit = GBP @@ -33,7 +36,12 @@ class legacy_benefits_home_letting_income(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.legacy_means_tests.sub_tenant_rent_disregard - weekly_rent = add(benunit, period, ["sublet_income"]) / WEEKS_IN_YEAR - pension_age = benunit.any(benunit.members("is_SP_age", period)) + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + weekly_rent = ( + benunit.sum(person("sublet_income", period) * claimant_or_partner) + / WEEKS_IN_YEAR + ) + pension_age = benunit.any(person("is_SP_age", period) & claimant_or_partner) disregard = where(pension_age, p.pension_age, p.working_age) return max_(0, weekly_rent - disregard) * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py index 042addb578..f8139532e6 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py @@ -6,11 +6,23 @@ class legacy_means_test_income_tax(Variable): entity = Person label = "Income Tax deducted in the legacy means tests" documentation = ( - "Income Tax less the tax on savings interest, dividends and property " - "income, floored at zero. The legacy means tests disregard tax only on " - "income they take into account, and they treat income derived from " - "capital as capital, not income, so tax on it is not deducted. Tax " - "reductions are set against the remaining tax first." + "Income Tax on the income the legacy means tests take into account. The " + "tests disregard tax only on income they count (Income Support Schedule 9 " + "paragraph 1; Housing Benefit Schedule 5 paragraph 1; pension-age Housing " + "Benefit regulation 33(12); Pension Credit regulation 17(10)(a); the " + "council tax reduction schemes' equivalents), and they treat savings " + "interest, dividends and property income as capital, not income. So this " + "is the tax calculated on the person's other income (earned_income_tax, " + "which covers earnings, pensions and taxable benefits) at Step 4 of " + "Income Tax Act 2007 s.23, less that income's share of the Step 6 tax " + "reductions (the married couple's allowance and other tax credits). " + "Neither tax law nor the benefit regulations say which income a " + "reduction relieves (s.27 orders reductions only to give the greatest " + "reduction in liability), so the model shares them in proportion to the " + "Step 4 tax on each kind of income. Step 7 charges, such as the High " + "Income Child Benefit Charge and the pension annual allowance charge " + "(s.30), are not tax calculated on any of this income and are not " + "deducted." ) definition_period = YEAR unit = GBP @@ -20,12 +32,41 @@ class legacy_means_test_income_tax(Variable): "https://www.legislation.gov.uk/uksi/2006/214/regulation/33", "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/17", + "https://www.legislation.gov.uk/ukpga/2007/3/section/23", + "https://www.legislation.gov.uk/ukpga/2007/3/section/27", + "https://www.legislation.gov.uk/ukpga/2007/3/section/30", ] def formula(person, period, parameters): - tax_on_income_from_capital = add( + p = parameters(period).gov.hmrc.income_tax + # Step 4 tax on the income the means tests count, and on the income + # they treat as capital. + counted = person("earned_income_tax", period) + capital = add( person, period, ["savings_income_tax", "dividend_income_tax", "property_income_tax"], ) - return max_(0, person("income_tax", period) - tax_on_income_from_capital) + before_reductions = counted + capital + # Step 7 charges: everything income tax adds beyond the Step 4 tax. + charges = max_( + 0, add(person, period, p.income_tax_additions) - before_reductions + ) + # Step 6 reductions, shared in proportion to the Step 4 tax. + reductions = min_( + add(person, period, p.income_tax_subtractions), before_reductions + ) + capital_share = np.divide( + capital, + before_reductions, + out=np.zeros_like(capital, dtype=float), + where=before_reductions > 0, + ) + capital_after_reductions = capital - reductions * capital_share + # Starting from income tax keeps any supplied income_tax value: the + # result is the tax on counted income less its share of reductions + # whenever income tax is calculated. + return max_( + 0, + person("income_tax", period) - capital_after_reductions - charges, + ) 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..da61b358c9 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 @@ -54,7 +54,7 @@ def legacy_council_tax_reduction( capital = where( has_uc_award, benunit("uc_assessable_capital", period), - benunit.household("savings", period), + benunit("council_tax_reduction_assessable_capital", period), ) capital_eligible = capital <= ctr.means_test.capital_limit return ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 674697f34e..53bba409a8 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -24,7 +24,8 @@ class council_tax_reduction_applicable_income(Variable): "unearned income it lists, which includes the assumed yield from " "capital but not actual rent, interest or dividends. Rent for letting " "part of the home counts, less the sub-tenant disregard, except in the " - "Scottish working-age scheme, which does not list it." + "Scottish scheme for people under pension age from April 2022, which " + "does not list it." ) definition_period = YEAR unit = GBP @@ -88,13 +89,37 @@ def formula(benunit, period, parameters): ) credits = add_for_members(benunit, period, ["tax_credits"], members) increased_income = income + personal_benefit_income + credits + benefits - scotland_working_age = is_scotland_scheme( - benunit.household("country", period) - ) & ~benunit.household("council_tax_reduction_household_has_pensioner", period) + # Rent for letting part of the home counts in every scheme except the + # Scottish one for people under pension age from 1 April 2022 (SSI + # 2021/249, whose reg 57(1) list has no head for it). That scheme + # applies under reg 3(1) to an applicant under pensionable age, or over + # it where the applicant or partner has an award of universal credit or + # a qualifying income-related benefit. + claimant_or_partner_over_pension_age = benunit.any( + person("is_SP_age", period) & person("is_claimant_or_partner", period) + ) + on_uc_or_income_related_benefit = ( + add( + benunit, + period, + ["universal_credit", "income_support", "jsa_income", "esa_income"], + ) + > 0 + ) + scotland = is_scotland_scheme(benunit.household("country", period)) + scottish_working_age_scheme = scotland & ( + ~claimant_or_partner_over_pension_age | on_uc_or_income_related_benefit + ) + p_scotland = parameters( + period + ).gov.local_authorities.scotland.council_tax_reduction.means_test + counts_home_letting = ~scottish_working_age_scheme | ( + p_scotland.working_age_counts_home_letting_income + ) increased_income += where( - scotland_working_age, - 0, + counts_home_letting, benunit("legacy_benefits_home_letting_income", period), + 0, ) if not bi.interactions.include_in_means_tests: diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py index 16b63ab2a9..5a6dbd4a77 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py @@ -10,17 +10,20 @@ class council_tax_reduction_assessable_capital(Variable): "pensioner schemes disregard the whole capital of an applicant who, or " "whose partner, is in receipt of Pension Credit guarantee credit, and " "use the Secretary of State's Pension Credit assessment of capital where " - "the award is savings credit only. Otherwise household savings stand in " - "for the applicant's capital; the schemes' own capital rules also count " - "other property and investments, so this understates capital for " - "pensioners without Pension Credit. The recalculation when capital rises " - "above the limit during an assessed income period is not modelled." + "the award is savings credit only. Otherwise the household's capital " + "stands in for the applicant's: savings, land and property other than " + "the home (gov.local_authorities.council_tax_reduction.capital_sources). " + "The schemes also count shares and other investments, but in the data " + "those are mixed with pension wealth the schemes disregard, so they are " + "left out for now. The recalculation when capital rises above the limit " + "during an assessed income period is not modelled." ) definition_period = YEAR unit = GBP quantity_type = STOCK reference = [ "https://www.legislation.gov.uk/uksi/2012/2885/regulation/11", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/31", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/13", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/14", "https://www.legislation.gov.uk/wsi/2013/3029/regulation/30", @@ -37,8 +40,12 @@ def formula(benunit, period, parameters): has_savings_credit_only_award = benunit( "in_receipt_of_savings_credit_only", period ) + sources = parameters( + period + ).gov.local_authorities.council_tax_reduction.capital_sources + household_capital = sum(benunit.household(source, period) for source in sources) return select( [in_receipt_of_guarantee_credit, has_savings_credit_only_award], [0, benunit("pension_credit_assessable_capital", period)], - default=benunit.household("savings", period), + default=household_capital, ) diff --git a/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py index 7f9b1b7663..0979df1f26 100644 --- a/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/kingston_upon_thames/council_tax_reduction/kingston_upon_thames_council_tax_reduction.py @@ -26,7 +26,7 @@ def formula(benunit, period, parameters): household("council_tax_reduction_household_has_pensioner", period), ) has_uc_award = benunit("universal_credit", period) > 0 - capital = household("savings", period) + capital = benunit("council_tax_reduction_assessable_capital", period) weekly_tariff_income = np.ceil( max_(0, capital - ctr.means_test.tariff_income_threshold) / ctr.means_test.tariff_income_step diff --git a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py index 332b9781c0..59fe9d99fd 100644 --- a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py @@ -25,7 +25,7 @@ def formula(benunit, period, parameters): universal_credit = benunit("universal_credit", period) has_uc_award = universal_credit > 0 - capital = household("savings", period) + capital = benunit("council_tax_reduction_assessable_capital", period) capital_eligible = capital <= ctr.means_test.capital_limit weekly_tariff_income = np.ceil( max_(0, capital - ctr.means_test.tariff_income_threshold) diff --git a/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py index 3c86931aee..c0f7e16487 100644 --- a/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/westminster/council_tax_reduction/westminster_council_tax_reduction.py @@ -24,7 +24,7 @@ def formula(benunit, period, parameters): household("council_tax_reduction_household_has_pensioner", period), ) has_uc_award = benunit("universal_credit", period) > 0 - capital = household("savings", period) + capital = benunit("council_tax_reduction_assessable_capital", period) weekly_tariff_income = np.ceil( max_(0, capital - ctr.means_test.tariff_income_threshold) / ctr.means_test.tariff_income_step From 0f4e0e3d881a0e0c169499ae76207bd9d964a36f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 07:54:35 -0400 Subject: [PATCH 05/24] Address review round 2: CTR tariff income, sale expenses, UC capital, tax attribution docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add council_tax_reduction_tariff_income to the national schemes' general route: pensioners £1 a week per £500 over £10,000 (SI 2012/2885 Sch 1 para 37; WSI 2013/3029 Sch 1 para 31; SSI 2012/319 reg 27(2)); Wales and Scotland under pension age £1 per £250 over £6,000 (WSI 2013/3029 Sch 6 para 33; SSI 2012/303 reg 51; SSI 2021/249 reg 63(1)(b)); with UC the £4.35 monthly assumed yield. Nil on the Pension Credit routes and for IS/income-based JSA/income-related ESA recipients. - Value land and property in CTR capital less 10% sale expenses (capital.sale_expenses), moving capital_sources to capital.sources. - Take UC recipients' CTR capital from the UC assessment (default scheme para 37(6); WSI 2013/3029 Sch 6 para 9(6); SSI 2012/303 reg 26), except Scotland from April 2022; apply Oxford's Appendix 6 disregard. - Select the Scottish working-age scheme as ~council_tax_reduction_pensioner, so a UC award held only by people over pension age does not select it. - Correct the tax helper's documentation: foreign tax credit and finance-cost relief are source-specific (TIOPA 2010 ss.18, 36; ITTOIA 2005 ss.274A-274AA); other_tax_credits has no source, so pro rata stays a stated convention. - New YAML cases for each; update #1909's capital and tariff expectations; tariff property test; #1909's property reference includes tariff income. Co-Authored-By: Claude Opus 5.5 --- .../legacy-capital-derived-income.fixed.md | 2 +- .../capital/sale_expenses/rate.yaml | 27 + .../capital/sale_expenses/sources.yaml | 29 ++ .../capital/sources.yaml | 44 ++ .../capital_sources.yaml | 30 -- .../means_test/tariff_income/amount.yaml | 10 + .../means_test/tariff_income/step.yaml | 10 + .../means_test/tariff_income/threshold.yaml | 10 + .../tariff_income/pensioner/amount.yaml | 10 + .../tariff_income/pensioner/step.yaml | 10 + .../tariff_income/pensioner/threshold.yaml | 10 + .../tariff_income/working_age/amount.yaml | 12 + .../tariff_income/working_age/step.yaml | 12 + .../tariff_income/working_age/threshold.yaml | 12 + ...s_universal_credit_capital_assessment.yaml | 24 + .../tariff_income/pensioner/amount.yaml | 10 + .../tariff_income/pensioner/step.yaml | 10 + .../tariff_income/pensioner/threshold.yaml | 10 + .../tariff_income/working_age/amount.yaml | 10 + .../tariff_income/working_age/step.yaml | 10 + .../tariff_income/working_age/threshold.yaml | 10 + .../income_from_capital.yaml | 466 +++++++++++++++++- .../council_tax_reduction_pension_credit.yaml | 26 +- ...tax_reduction_pension_credit_properties.py | 52 +- ...ans_test_income_from_capital_properties.py | 53 +- .../gov/dwp/legacy_means_test_income_tax.py | 30 +- ...council_tax_reduction_applicable_income.py | 55 ++- ...ouncil_tax_reduction_assessable_capital.py | 73 ++- .../council_tax_reduction_pensioner.py | 4 +- .../council_tax_reduction_tariff_income.py | 132 +++++ .../oxford_council_tax_reduction.py | 22 +- 31 files changed, 1111 insertions(+), 114 deletions(-) create mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/sources.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sources.yaml delete mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/step.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/threshold.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/step.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/uses_universal_credit_capital_assessment.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/step.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py diff --git a/changelog.d/legacy-capital-derived-income.fixed.md b/changelog.d/legacy-capital-derived-income.fixed.md index b88d34bca3..0a97bfbb8f 100644 --- a/changelog.d/legacy-capital-derived-income.fixed.md +++ b/changelog.d/legacy-capital-derived-income.fixed.md @@ -1 +1 @@ -Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard. +Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale, adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland), uses the Universal Credit assessment of capital for Universal Credit recipients where the scheme says so, and applies Oxford's disregard of capital already assessed for Universal Credit or income-related benefits. diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/rate.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/rate.yaml new file mode 100644 index 0000000000..6ec187b3fb --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/rate.yaml @@ -0,0 +1,27 @@ +description: > + Share of the current market or surrender value deducted from council tax + reduction capital where there would be expenses attributable to sale. It is + a flat 10% whatever the actual costs. The amount of any encumbrance secured + on the asset is also deducted, but the data cannot identify debt secured on + property other than the home, so that deduction is not modelled. +values: + 2013-04-01: 0.1 +metadata: + label: Council tax reduction capital sale expense deduction + period: year + unit: /1 + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 1 para. 32 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/32 + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012, Schedule para. 65 + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/65 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 1 para. 26 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/26 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 6 para. 28 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/28 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 42 + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/42 + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 46 (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/46 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 70 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/70 diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/sources.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/sources.yaml new file mode 100644 index 0000000000..fda5d09021 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sale_expenses/sources.yaml @@ -0,0 +1,29 @@ +description: > + Council tax reduction capital sources whose sale would incur expenses, so + that they are valued at 90% of market value. Real property always has costs + of sale (R(IS) 21/93, as DMG 29622 applies it to the identically worded + Income Support rule), so `owned_land`, `other_residential_property_value` + and `non_residential_property_value` are listed. `savings` is not: a bank or + building society account is valued at its balance, and the costs of + withdrawing money are not costs of sale (HBGM BW1 Annex F and BP1 F2.03, + on the same words in the Housing Benefit regulations). +metadata: + label: Council tax reduction capital sources with sale expenses + period: year + unit: list + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 1 para. 32(a) + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/32 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 6 para. 28(a) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/28 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 70(1)(a) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/70 + - title: Decision Makers' Guide chapter 29, para. 29622 (real property, R(IS) 21/93) + href: https://assets.publishing.service.gov.uk/media/69d8f3a496c86b751317022c/dmg-Ch-29.pdf#page=124 + - title: Housing Benefit Guidance Manual BW1, Annex F + href: https://assets.publishing.service.gov.uk/media/5a749328ed915d0e8e3994c0/hbgm-bw1-assessment-of-capital.pdf#page=93 +values: + 2013-04-01: + - owned_land + - other_residential_property_value + - non_residential_property_value diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sources.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sources.yaml new file mode 100644 index 0000000000..c8d6ee6133 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital/sources.yaml @@ -0,0 +1,44 @@ +description: > + Household capital counted for the council tax reduction capital limit and + tariff income, outside the Pension Credit and Universal Credit routes. Every + scheme counts the whole of the applicant's capital unless a schedule + disregards it, including land and property other than the home: the + prescribed requirements for pensioners in England (Schedule 1 paragraph + 31(1)) and Wales (Schedule 1 paragraph 25(1)), the Welsh working-age rules + (Schedule 6 paragraph 26(1)), the English default scheme (paragraph 63(1)), + the Scottish pension-age scheme (regulation 41(1)) and the Scottish + working-age schemes (SSI 2012/303 regulation 43(1) to 31 March 2022, then + SSI 2021/249 regulations 67 and 69). `main_residence_value` is left out + because the home is disregarded. `corporate_wealth` is also left out for + now: in the data it is mostly imputed pension wealth, which the schemes + disregard, and it cannot yet be separated (issue 1936). Land and property + are valued less sale expenses (`sale_expenses`). +metadata: + label: Council tax reduction capital sources + unit: list + period: year + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 reg. 11(2) and Schedule 1 para. 31 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/31 + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012, Schedule para. 63 + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/63 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 reg. 30 + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/30 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 1 para. 25 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/25 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 6 para. 26 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/26 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 regs. 40 and 41 + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/41 + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 43 (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/43 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 67 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/67 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 69 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/69 +values: + 2013-04-01: + - savings + - owned_land + - other_residential_property_value + - non_residential_property_value diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml deleted file mode 100644 index 2d11082d6e..0000000000 --- a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/capital_sources.yaml +++ /dev/null @@ -1,30 +0,0 @@ -description: > - Household capital counted for the council tax reduction capital limit and - tariff income, outside the Pension Credit routes. Every scheme counts the - whole of the applicant's capital unless a schedule disregards it, including - land and property other than the home: the prescribed requirements for - pensioners in England (Schedule 1 paragraph 31(1)), Wales (Schedule 1) and - Scotland (regulation 40 onwards), the English default scheme for working - age and the Welsh and Scottish working-age schemes. `main_residence_value` - is left out because the home is disregarded. `corporate_wealth` is also left - out for now: in the data it is mostly imputed pension wealth, which the - schemes disregard, and it cannot yet be separated (issue 1936). -metadata: - label: Council tax reduction capital sources - unit: list - period: year - reference: - - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 reg. 11(2) and Schedule 1 para. 31 - href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/31 - - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012, Schedule - href: https://www.legislation.gov.uk/uksi/2012/2886/schedule - - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 reg. 30 - href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/30 - - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 40 - href: https://www.legislation.gov.uk/ssi/2012/319/regulation/40 -values: - 2013-04-01: - - savings - - owned_land - - other_residential_property_value - - non_residential_property_value diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/amount.yaml new file mode 100644 index 0000000000..9bcd3e8f7c --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/amount.yaml @@ -0,0 +1,10 @@ +description: Weekly tariff income the England Council Tax Reduction pensioner scheme counts for each step of capital, or part of one, above the threshold. +values: + 2013-04-01: 1 +metadata: + unit: currency-GBP + period: year + label: England Council Tax Reduction pensioner weekly tariff income per capital step + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 1 para. 37 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/37 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/step.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/step.yaml new file mode 100644 index 0000000000..92751f8871 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/step.yaml @@ -0,0 +1,10 @@ +description: Step of capital above the threshold, or part of one, that the England Council Tax Reduction pensioner scheme treats as yielding one amount of weekly tariff income. +values: + 2013-04-01: 500 +metadata: + unit: currency-GBP + period: year + label: England Council Tax Reduction pensioner tariff income capital step + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 1 para. 37 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/37 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/threshold.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/threshold.yaml new file mode 100644 index 0000000000..2dc0adce1f --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/tariff_income/threshold.yaml @@ -0,0 +1,10 @@ +description: Capital above which the England Council Tax Reduction pensioner scheme treats capital as yielding a weekly tariff income. +values: + 2013-04-01: 10_000 +metadata: + unit: currency-GBP + period: year + label: England Council Tax Reduction pensioner tariff income threshold + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Schedule 1 para. 37 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/37 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml new file mode 100644 index 0000000000..0563d2fd66 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml @@ -0,0 +1,10 @@ +description: Weekly tariff income the Scotland Council Tax Reduction pensioner scheme counts for each step of capital, or part of one, above the threshold. +values: + 2013-04-01: 1 +metadata: + unit: currency-GBP + period: year + label: Scotland Council Tax Reduction pensioner weekly tariff income per capital step + reference: + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 27(2) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/27 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml new file mode 100644 index 0000000000..236abcc702 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml @@ -0,0 +1,10 @@ +description: Step of capital above the threshold, or part of one, that the Scotland Council Tax Reduction pensioner scheme treats as yielding one amount of weekly tariff income. +values: + 2013-04-01: 500 +metadata: + unit: currency-GBP + period: year + label: Scotland Council Tax Reduction pensioner tariff income capital step + reference: + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 27(2) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/27 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml new file mode 100644 index 0000000000..cb51a4ec39 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml @@ -0,0 +1,10 @@ +description: Capital above which the Scotland Council Tax Reduction pensioner scheme treats capital as yielding a weekly tariff income. +values: + 2013-04-01: 10_000 +metadata: + unit: currency-GBP + period: year + label: Scotland Council Tax Reduction pensioner tariff income threshold + reference: + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 27(2) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/27 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml new file mode 100644 index 0000000000..7ef585b74b --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml @@ -0,0 +1,12 @@ +description: Weekly tariff income the Scotland Council Tax Reduction working-age scheme counts for each step of capital, or part of one, above the threshold. +values: + 2013-04-01: 1 +metadata: + unit: currency-GBP + period: year + label: Scotland Council Tax Reduction working-age weekly tariff income per capital step + reference: + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 51 (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/51 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 63(1)(b) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/63 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/step.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/step.yaml new file mode 100644 index 0000000000..64edd8615f --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/step.yaml @@ -0,0 +1,12 @@ +description: Step of capital above the threshold, or part of one, that the Scotland Council Tax Reduction working-age scheme treats as yielding one amount of weekly tariff income. +values: + 2013-04-01: 250 +metadata: + unit: currency-GBP + period: year + label: Scotland Council Tax Reduction working-age tariff income capital step + reference: + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 51 (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/51 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 63(1)(b) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/63 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml new file mode 100644 index 0000000000..49a77b2b39 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml @@ -0,0 +1,12 @@ +description: Capital above which the Scotland Council Tax Reduction working-age scheme treats capital as yielding a weekly tariff income. +values: + 2013-04-01: 6_000 +metadata: + unit: currency-GBP + period: year + label: Scotland Council Tax Reduction working-age tariff income threshold + reference: + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 51 (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/51 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 63(1)(b) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/63 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/uses_universal_credit_capital_assessment.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/uses_universal_credit_capital_assessment.yaml new file mode 100644 index 0000000000..7fc675e578 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/uses_universal_credit_capital_assessment.yaml @@ -0,0 +1,24 @@ +description: > + Whether the Scottish council tax reduction scheme for people under pension + age takes the capital of an applicant with an award of Universal Credit + from the Secretary of State's calculation for that award. The Council Tax + Reduction (Scotland) Regulations 2012 did (regulation 26(4) as made; + regulation 26(6) after SSI 2018/69 substituted the regulation). The Council Tax Reduction (Scotland) Regulations 2021, in force + from 1 April 2022, have no such rule: the authority calculates every + applicant's capital itself (regulations 66 to 70). +metadata: + label: Scotland working-age council tax reduction uses the Universal Credit capital assessment + unit: bool + period: year + reference: + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 26(4) (as made) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/26/made + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 26(6) (as at 1 April 2021) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/26/2021-04-01 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 1 (commencement, 1 April 2022) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/1 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 66 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/66 +values: + 2013-04-01: true + 2022-04-01: false diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml new file mode 100644 index 0000000000..dd91c21484 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/amount.yaml @@ -0,0 +1,10 @@ +description: Weekly tariff income the Wales Council Tax Reduction pensioner scheme counts for each step of capital, or part of one, above the threshold. +values: + 2013-04-01: 1 +metadata: + unit: currency-GBP + period: year + label: Wales Council Tax Reduction pensioner weekly tariff income per capital step + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 1 para. 31 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/31 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml new file mode 100644 index 0000000000..ad37548b57 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/step.yaml @@ -0,0 +1,10 @@ +description: Step of capital above the threshold, or part of one, that the Wales Council Tax Reduction pensioner scheme treats as yielding one amount of weekly tariff income. +values: + 2013-04-01: 500 +metadata: + unit: currency-GBP + period: year + label: Wales Council Tax Reduction pensioner tariff income capital step + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 1 para. 31 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/31 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml new file mode 100644 index 0000000000..5f1c4de9ce --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/pensioner/threshold.yaml @@ -0,0 +1,10 @@ +description: Capital above which the Wales Council Tax Reduction pensioner scheme treats capital as yielding a weekly tariff income. +values: + 2013-04-01: 10_000 +metadata: + unit: currency-GBP + period: year + label: Wales Council Tax Reduction pensioner tariff income threshold + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 1 para. 31 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/31 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml new file mode 100644 index 0000000000..8f0e0aba6a --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/amount.yaml @@ -0,0 +1,10 @@ +description: Weekly tariff income the Wales Council Tax Reduction working-age scheme counts for each step of capital, or part of one, above the threshold. +values: + 2013-04-01: 1 +metadata: + unit: currency-GBP + period: year + label: Wales Council Tax Reduction working-age weekly tariff income per capital step + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 6 para. 33 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/33 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/step.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/step.yaml new file mode 100644 index 0000000000..c9f3b07f99 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/step.yaml @@ -0,0 +1,10 @@ +description: Step of capital above the threshold, or part of one, that the Wales Council Tax Reduction working-age scheme treats as yielding one amount of weekly tariff income. +values: + 2013-04-01: 250 +metadata: + unit: currency-GBP + period: year + label: Wales Council Tax Reduction working-age tariff income capital step + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 6 para. 33 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/33 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml new file mode 100644 index 0000000000..cbe82c917e --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/tariff_income/working_age/threshold.yaml @@ -0,0 +1,10 @@ +description: Capital above which the Wales Council Tax Reduction working-age scheme treats capital as yielding a weekly tariff income. +values: + 2013-04-01: 6_000 +metadata: + unit: currency-GBP + period: year + label: Wales Council Tax Reduction working-age tariff income threshold + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Schedule 6 para. 33 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/33 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 2a55b678d1..f3ae83a17e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -337,10 +337,11 @@ - name: Tax reductions are shared between counted income and income from capital # ITA 2007 s.23: Step 4 tax is 286 on earnings ((14,000 - 12,570) x 20%) and # 1,000 on rent ((6,000 - 1,000) x 20%), 1,286 in all; Step 6 deducts a - # 1,000 tax reduction. Neither tax law nor the benefit regulations say which - # income it relieves, so the model shares it in proportion to the Step 4 - # tax: the rent's share is 1,000 x 1,000 / 1,286 = 777.60, leaving 222.40 of - # tax on rent. Income tax 286 less 222.40 = 63.60 is tax on the earnings, + # 1,000 tax reduction. other_tax_credits records no source, so the model + # shares it in proportion to the Step 4 tax (a stated convention: a credit + # for foreign tax on the rent would relieve only the rent, TIOPA 2010 + # s.18(2)): the rent's share is 1,000 x 1,000 / 1,286 = 777.60, leaving + # 222.40 of tax on rent. Income tax 286 less 222.40 = 63.60 is tax on the earnings, # and HB applicable income is 14,000 - 63.60 - 114.40 (NI) - 260 = 13,562.00. period: 2025 absolute_error_margin: 0.01 @@ -377,6 +378,25 @@ income_tax: 1_986 legacy_means_test_income_tax: 1_486 +- name: Tax on counted income is floored at nil when reductions exceed the Step 4 tax + # Step 4 tax is 286 on earnings and 1,000 on rent; Step 6 reductions of + # 2,000 exceed it, so income tax is nil even with a 500 Step 7 charge + # (1,786 - 2,000 < 0). The rent's share of the reductions (1,000 of the + # 1,286 used) removes its tax, and the charge is not tax on counted income: + # 0 - 0 - 500 is floored at nil. + period: 2025 + input: + age: 40 + employment_income: 14_000 + property_income: 6_000 + other_tax_credits: 2_000 + CB_HITC: 500 + output: + earned_income_tax: 286 + property_income_tax: 1_000 + income_tax: 0 + legacy_means_test_income_tax: 0 + - name: Working-age Housing Benefit award with rent from a let property # A continuing award (2025-26). Earnings 10,000 use 10,000 of the personal # allowance; taxable rent 5,000 - 1,000 = 4,000, of which 4,000 - 2,570 = @@ -415,7 +435,8 @@ - name: An English pensioner with a let property worth more than £16,000 gets no council tax reduction # The rent no longer counts as income, but the property counts as capital: # SI 2012/2885 Sch 1 para 31(1) ("the whole of his capital") and reg 11(2) - # (no reduction above 16,000). + # (no reduction above 16,000). It is valued less 10% for the expenses of + # sale (para 32(a)): 100,000 x 0.9 = 90,000. period: 2026 absolute_error_margin: 0.01 input: @@ -437,11 +458,15 @@ council_tax: 2_000 other_residential_property_value: 100_000 output: - council_tax_reduction_assessable_capital: 100_000 + council_tax_reduction_assessable_capital: 90_000 council_tax_reduction: 0 - name: A Welsh working-age applicant with a let property worth more than £16,000 gets no council tax reduction - # WSI 2013/3029 reg 30: no reduction where capital exceeds 16,000. + # WSI 2013/3029 reg 30: no reduction where capital exceeds 16,000. The + # property is valued less 10% for the expenses of sale (Sch 6 para 28(a)): + # 45,000. Tariff income (Sch 6 para 33) counts capital up to 16,000: + # (16,000 - 6,000) / 250 = 40 a week, 2,080 a year, so income is 9,000 + + # 2,080 = 11,080. period: 2026 absolute_error_margin: 0.01 input: @@ -465,8 +490,9 @@ council_tax: 1_500 other_residential_property_value: 50_000 output: - council_tax_reduction_assessable_capital: 50_000 - council_tax_reduction_applicable_income: 9_000 + council_tax_reduction_assessable_capital: 45_000 + council_tax_reduction_tariff_income: 2_080 + council_tax_reduction_applicable_income: 11_080 council_tax_reduction: 0 - name: A Scottish mixed-age couple on Universal Credit is under the working-age scheme @@ -534,3 +560,425 @@ local_authority: GLASGOW_CITY output: council_tax_reduction_applicable_income: 0 + +- name: Welsh working-age tariff income on a let property below the capital limit + # The rent no longer counts, but the property counts as capital at 90% of + # its value (WSI 2013/3029 Sch 6 para 28(a)): 9,000. Sch 6 para 33 treats it + # as a weekly income of 1 for each 250, or part of 250, over 6,000: 12 a + # week, 624 a year. Earnings 9,000 are below the personal allowance and the + # NI threshold. Income 9,000 + 624 = 9,624. With the applicable amount + # supplied as 6,000, CTR = 1,500 - 20% x (9,624 - 6,000) = 775.20. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 9_000 + property_income: 5_200 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + would_claim_uc: false + council_tax_reduction_applicable_amount: 6_000 + households: + household: + members: [person] + country: WALES + local_authority: CARDIFF + tenure_type: RENT_FROM_COUNCIL + rent: 5_000 + council_tax: 1_500 + other_residential_property_value: 10_000 + output: + council_tax_reduction_assessable_capital: 9_000 + council_tax_reduction_tariff_income: 624 + council_tax_reduction_applicable_income: 9_624 + council_tax_reduction: 775.20 + +- name: Scottish working-age assumed yield on a let property below the capital limit + # SSI 2021/249 reg 70(1)(a) values the property less 10%: 9,000. Reg + # 63(1)(b): a weekly income of 1 for each 250 over 6,000 and 1 for any part + # of 250: 12 a week, 624 a year; reg 63(3) treats the actual rent as + # capital. CTR = 1,500 - 20% x (9,624 - 6,000) = 775.20. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 9_000 + property_income: 5_200 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + would_claim_uc: false + council_tax_reduction_applicable_amount: 6_000 + households: + household: + members: [person] + country: SCOTLAND + local_authority: GLASGOW_CITY + tenure_type: RENT_FROM_COUNCIL + rent: 5_000 + council_tax: 1_500 + other_residential_property_value: 10_000 + output: + council_tax_reduction_assessable_capital: 9_000 + council_tax_reduction_tariff_income: 624 + council_tax_reduction_applicable_income: 9_624 + council_tax_reduction: 775.20 + +- name: An English pensioner's let property valued less sale expenses stays within the limit + # A 17,000 property is valued at 15,300 (SI 2012/2885 Sch 1 para 32(a)), so + # the 16,000 limit (reg 11(2)) does not exclude the pensioner. Tariff income + # (Sch 1 para 37): 5,300 / 500 = 10.6, so 11 a week, 572 a year. Pension + # 14,000 less its tax 286 ((14,000 - 12,570) x 20%) + 572 = 14,286; the rent + # and its tax (400) do not count (Sch 5 para 23). With the 238 a week + # allowance (12,376) supplied as the applicable amount, CTR = 2,000 - 20% x + # (14,286 - 12,376) = 1,618. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 14_000 + property_income: 3_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + would_claim_pc: false + council_tax_reduction_applicable_amount: 12_376 + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + other_residential_property_value: 17_000 + output: + legacy_means_test_income_tax: 286 + council_tax_reduction_assessable_capital: 15_300 + council_tax_reduction_tariff_income: 572 + council_tax_reduction_applicable_income: 14_286 + council_tax_reduction: 1_618 + +- name: Welsh and Scottish pensioner tariff income is 1 a week per 500 over 10,000 + # WSI 2013/3029 Sch 1 para 31 and SSI 2012/319 reg 27(2): savings 12,600 + # are 2,600 over 10,000, 5.2 steps of 500, so 6 a week, 312 a year. + period: 2026 + input: + people: + welsh_pensioner: + age: 75 + private_pension_income: 12_000 + scottish_pensioner: + age: 75 + private_pension_income: 12_000 + benunits: + welsh: + members: [welsh_pensioner] + would_claim_pc: false + scottish: + members: [scottish_pensioner] + would_claim_pc: false + households: + welsh_household: + members: [welsh_pensioner] + country: WALES + local_authority: CARDIFF + savings: 12_600 + scottish_household: + members: [scottish_pensioner] + country: SCOTLAND + local_authority: GLASGOW_CITY + savings: 12_600 + output: + council_tax_reduction_pensioner: [true, true] + council_tax_reduction_tariff_income: [312, 312] + council_tax_reduction_applicable_income: [12_312, 12_312] + +- name: A savings credit recipient's council tax reduction income does not fall when more sub-tenant rent ends the savings credit + # Before: 3,130.54 of sub-tenant rent counts 2,090.54 (less 20 a week). + # Pension Credit income is 10,000 + 2,000 + 2,090.54 + 520 deemed income + # (savings 15,000: 10 steps of 500 over 10,000) = 14,610.54. Savings credit + # = 60% x (238 - 208.07) x 52 - 40% x (14,610.54 - 238 x 52) = 933.816 - + # 893.816 = 40.00, so the award is savings credit only and council tax + # reduction counts 14,610.54 + 40 = 14,650.54 (SI 2012/2885 Sch 1 para 14). + # CTR = 2,000 - 20% x (14,650.54 - 13,312) = 1,732.29. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 10_000 + private_pension_income: 2_000 + sublet_income: 3_130.54 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 15_000 + output: + pension_credit_income: 14_610.54 + savings_credit: 40 + in_receipt_of_savings_credit_only: true + council_tax_reduction_tariff_income: 0 + council_tax_reduction_applicable_income: 14_650.54 + council_tax_reduction: 1_732.29 + +- name: The same pensioner with 200 more sub-tenant rent is assessed under the general rules with tariff income + # 3,330.54 counts 2,290.54. Pension Credit income 14,810.54 leaves a savings + # credit of 933.816 - 40% x (14,810.54 - 12,376) < 0, so nil. Under the + # general rules: 12,000 + 2,290.54, no tax (12,000 is under the personal + # allowance), plus tariff income on 15,000 (Sch 1 para 37): 10 a week, 520 + # a year = 14,810.54, more than the 14,650.54 counted before. CTR = 2,000 - + # 20% x (14,810.54 - 13,312) = 1,700.29. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 10_000 + private_pension_income: 2_000 + sublet_income: 3_330.54 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 15_000 + output: + savings_credit: 0 + in_receipt_of_savings_credit_only: false + council_tax_reduction_tariff_income: 520 + council_tax_reduction_applicable_income: 14_810.54 + council_tax_reduction: 1_700.29 + +- name: Oxford tests a Universal Credit recipient on the Universal Credit assessment of capital + # The household's let property is worth 20,000 (18,000 after sale + # expenses), but the claimant's Universal Credit capital is 15,000 (a + # reported assessment). Oxford's Appendix 6 ignores capital already taken + # into account for Universal Credit, so the 16,000 limit (paragraph 28) + # does not exclude the claimant. UC: 424.90 - 36 x 4.35 (assumed yield on + # 9,000 over 6,000) = 268.30 a month, 3,219.60 a year, 61.92 a week: band 1, + # 100% of 2,000. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + would_claim_uc: true + uc_reported_capital: 15_000 + households: + household: + members: [person] + country: ENGLAND + local_authority: OXFORD + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + other_residential_property_value: 20_000 + output: + universal_credit: 3_219.60 + council_tax_reduction_assessable_capital: 15_000 + oxford_council_tax_reduction: 2_000 + +- name: Oxford ignores the capital of an Income Support recipient + # Appendix 6 also ignores capital already taken into account for Income + # Support, so the household's 18,000 of property (after sale expenses) does + # not exclude the claimant; Income Support recipients' income is nil for the + # bands, so the award is 100% of 2,000. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + would_claim_uc: false + income_support: 4_000 + households: + household: + members: [person] + country: ENGLAND + local_authority: OXFORD + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + other_residential_property_value: 20_000 + output: + council_tax_reduction_assessable_capital: 18_000 + oxford_council_tax_reduction: 2_000 + +- name: Wales uses the Secretary of State's capital for a Universal Credit recipient + # WSI 2013/3029 Sch 6 para 9(6): with a UC award the authority uses the + # Secretary of State's calculation of capital, 15,000 here (a reported + # assessment), not the household's 18,000 of property after sale expenses. + # The income includes the UC assumed yield (UC Regs 2013 reg 72): 36 x 4.35 + # x 12 = 1,879.20. Income 3,219.60 (UC, an input) + 1,879.20 = 5,098.80 is + # below the applicable amount (6,000, an input), so CTR is the full 1,500. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + universal_credit: 3_219.60 + uc_reported_capital: 15_000 + council_tax_reduction_applicable_amount: 6_000 + households: + household: + members: [person] + country: WALES + local_authority: CARDIFF + tenure_type: OWNED_OUTRIGHT + council_tax: 1_500 + other_residential_property_value: 20_000 + output: + council_tax_reduction_assessable_capital: 15_000 + council_tax_reduction_tariff_income: 1_879.20 + council_tax_reduction_applicable_income: 5_098.80 + council_tax_reduction: 1_500 + +- name: From April 2022 Scotland calculates a Universal Credit recipient's capital itself + # SSI 2021/249 regs 66 to 70 have no rule taking capital from the + # Universal Credit assessment, so the household's property counts: 18,000 + # after sale expenses, over the 16,000 limit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + universal_credit: 3_219.60 + uc_reported_capital: 15_000 + council_tax_reduction_applicable_amount: 6_000 + households: + household: + members: [person] + country: SCOTLAND + local_authority: GLASGOW_CITY + tenure_type: OWNED_OUTRIGHT + council_tax: 1_500 + other_residential_property_value: 20_000 + output: + council_tax_reduction_assessable_capital: 18_000 + council_tax_reduction: 0 + +- name: Before April 2022 Scotland used the Universal Credit assessment of capital + # SSI 2012/303 reg 26(6) (reg 26(4) as made): the Secretary of State's + # capital, 15,000. Income 3,219.60 + 1,879.20 assumed yield = 5,098.80, + # below the applicable amount, so CTR is the full 1,500. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + universal_credit: 3_219.60 + uc_reported_capital: 15_000 + council_tax_reduction_applicable_amount: 6_000 + households: + household: + members: [person] + country: SCOTLAND + local_authority: GLASGOW_CITY + tenure_type: OWNED_OUTRIGHT + council_tax: 1_500 + other_residential_property_value: 20_000 + output: + council_tax_reduction_assessable_capital: 15_000 + council_tax_reduction_tariff_income: 1_879.20 + council_tax_reduction: 1_500 + +- name: A Welsh Income Support recipient has no tariff income + # WSI 2013/3029 Sch 10 para 8 disregards the whole capital of an applicant + # on Income Support, so it yields no tariff income. + period: 2026 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + would_claim_uc: false + income_support: 4_000 + households: + household: + members: [person] + country: WALES + local_authority: CARDIFF + savings: 10_000 + output: + council_tax_reduction_tariff_income: 0 + +- name: A Scottish couple whose joint Universal Credit award is held only by people over pension age is under the pension-age scheme + # SSI 2021/249 reg 3(2): the working-age scheme does not apply where the + # award is held only by people over pensionable age. Universal Credit + # itself needs a claimant under that age (UC Regs 2013 reg 3(2)(a)), so the + # model treats such an award as not selecting the working-age scheme. SSI + # 2012/319 counts the sub-tenant rent less 20 a week: (100 - 20) x 52 = + # 4,160, plus the UC (1,000, an input) as the model counts it: 5,160. The + # couple do not claim Pension Credit, whose guarantee credit would + # otherwise disregard their whole income. + period: 2026 + input: + people: + claimant: + age: 71 + sublet_income: 5_200 + partner: + age: 70 + benunits: + benunit: + members: [claimant, partner] + universal_credit: 1_000 + would_claim_pc: false + households: + household: + members: [claimant, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + council_tax_reduction_pensioner: true + council_tax_reduction_applicable_income: 5_160 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml index 2c8539c671..70033dbbe2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml @@ -500,11 +500,15 @@ output: in_receipt_of_savings_credit_only: false # SI 2012/2885 Sch 1 para 31(1) counts "the whole of his capital", so the - # second property counts as it does for the claimant above: 10,000 + - # 7,000 = 17,000, over the 16,000 limit (reg 11(2)). Shares and other - # investments are still left out of the proxy (issue 1936). - council_tax_reduction_assessable_capital: 17_000 - council_tax_reduction_applicable_income: 13_314 + # second property counts as it does for the claimant above, valued less + # 10% for the expenses of sale (para 32(a)): 10,000 + 7,000 x 0.9 = + # 16,300, over the 16,000 limit (reg 11(2)). Shares and other investments + # are still left out of the proxy (issue 1936). + council_tax_reduction_assessable_capital: 16_300 + # 13,500 - 186 tax ((13,500 - 12,570) x 20%) = 13,314, plus tariff income + # (Sch 1 para 37) on capital up to 16,000: 12 x 52 = 624. + council_tax_reduction_tariff_income: 624 + council_tax_reduction_applicable_income: 13_938 council_tax_reduction: 0 - name: Capital of exactly 16,000 is within the limit for a pensioner without Pension Credit @@ -529,12 +533,16 @@ savings: 16_000 output: # The limit excludes capital that "exceeds" 16,000 (SI 2012/2885 reg 11(2)). - # The award pins the model's current value: it has no pensioner tariff - # income from capital (Sch 1 para 37, 624 a year here), a known gap - # (#1933), so this is not the full legal amount. + # Tariff income (Sch 1 para 37): (16,000 - 10,000) / 500 = 12 a week, 624 + # a year. Income 13,000 - 86 tax ((13,000 - 12,570) x 20%) + 624 = + # 13,538. The award, 2,000 - 0.2 x (13,538 - 13,312) = 1,954.80, uses the + # model's applicable amount of 256 a week; the 238 a week allowance for + # people reaching pensionable age from April 2021 is a known gap (#1933). in_receipt_of_guarantee_credit: false council_tax_reduction_assessable_capital: 16_000 - council_tax_reduction: 2_000 + council_tax_reduction_tariff_income: 624 + council_tax_reduction_applicable_income: 13_538 + council_tax_reduction: 1_954.80 - name: Capital just over 16,000 excludes a pensioner without Pension Credit period: 2026 diff --git a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py index 9ff7b15eef..173229acf6 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py @@ -22,9 +22,18 @@ A) - ND) when the Pension Credit assessment of capital is within 16,000, and nothing otherwise. 4. Differential: everyone else keeps the general formula, max(0, L - 0.2 x - max(0, I - A) - ND) when household savings and other property are within - 16,000, where I is the general income definition, recomputed here from its - components rather than read from the variable under test. + max(0, I - A) - ND) when capital C is within 16,000, where I is the + general income definition plus tariff income on C, both recomputed here + from their components and the regulations rather than read from the + variables under test. C is household savings plus other property at 90% + of its value (SI 2012/2885 Sch 1 para 32(a)), or, for a Welsh Universal + Credit recipient, the Universal Credit assessment of capital (WSI + 2013/3029 Sch 6 para 9(6)). Tariff income is 1 a week per 500, or part, + over 10,000 for pensioners (Sch 1 para 37); for people under pension age + in Wales and Scotland 1 a week per 250, or part, over 6,000 (WSI + 2013/3029 Sch 6 para 33; SSI 2021/249 reg 63(1)(b)), or with Universal + Credit 4.35 a month (UC Regs 2013 reg 72; SSI 2021/249 reg 63(1)(a)), and + nil on Income Support or income-based JSA or ESA. 5. Metamorphic: a guarantee credit recipient's CTR does not change when the State Pension changes. 6. Metamorphic: nor when its savings change. @@ -81,6 +90,7 @@ guarantees = st.one_of(st.none(), st.floats(240, 450)) CAPITAL_LIMIT = 16_000 WITHDRAWAL_RATE = 0.2 +SALE_EXPENSES = 0.1 @st.composite @@ -257,6 +267,10 @@ def situation( "savings_credit", "pension_credit_income", "pension_credit_assessable_capital", + "council_tax_reduction_pensioner", + "council_tax_reduction_relevant_income_based_benefit", + "universal_credit", + "uc_assessable_capital", ] # council_tax_reduction_applicable_income outside the Pension Credit routes: # these incomes and benefits, less the income tax on them, National Insurance @@ -336,6 +350,30 @@ def benunit_total(names): return values +def expected_capital(unit, values, i): + if unit["country"] == "WALES" and values["universal_credit"][i] > 0: + return values["uc_assessable_capital"][i] + return ( + values["savings"][i] + + (1 - SALE_EXPENSES) * values["other_residential_property_value"][i] + ) + + +def expected_tariff_income(unit, values, i, capital): + def steps(threshold, step): + return np.ceil(max(0.0, min(capital, CAPITAL_LIMIT) - threshold) / step) + + if values["council_tax_reduction_pensioner"][i]: + return steps(10_000, 500) * 52 + if unit["country"] == "ENGLAND": + return 0.0 + if values["universal_credit"][i] > 0: + return steps(6_000, 250) * 4.35 * 12 + if values["council_tax_reduction_relevant_income_based_benefit"][i]: + return 0.0 + return steps(6_000, 250) * 52 + + def tapered(liability, income, applicable_amount, non_dep): excess = max(0.0, income - applicable_amount) return max(0.0, liability - WITHDRAWAL_RATE * excess - non_dep) @@ -392,14 +430,14 @@ def check_routes(units, values): values["pension_credit_assessable_capital"][i] <= CAPITAL_LIMIT ) else: - income = values["income_under_main_definition"][i] + capital = expected_capital(unit, values, i) + income = values["income_under_main_definition"][i] + expected_tariff_income( + unit, values, i, capital + ) assert ( abs(values["council_tax_reduction_applicable_income"][i] - income) < 0.01 ), unit - capital = ( - values["savings"][i] + values["other_residential_property_value"][i] - ) expected = tapered(liability, income, applicable_amount, non_dep) * ( capital <= CAPITAL_LIMIT ) diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 81f4b05fa7..d8541b3ccc 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -29,6 +29,12 @@ the tax on counted income before reductions (earned_income_tax), and equals income tax when there is no savings, dividend or property income and no Step 7 charge. +5. Tariff: the council tax reduction tariff income that replaces the actual + income from capital is never negative, never more than the yield at the + 16,000 capital limit (40 steps of 250 at 4.35 a month, 2,088 a year), nil + outside the national schemes (Northern Ireland, and English people under + pension age, whose local schemes set their own), and never falls when + savings rise while the family stays on the same route. """ import numpy as np @@ -104,7 +110,9 @@ def families(draw): populations = st.lists(families(), min_size=1, max_size=4) -def situation(units, year, capital_income_scale=1.0, sublet_extra=0.0): +def situation( + units, year, capital_income_scale=1.0, sublet_extra=0.0, savings_extra=0.0 +): people, benunits, households = {}, {}, {} for i, unit in enumerate(units): names = [] @@ -143,7 +151,7 @@ def situation(units, year, capital_income_scale=1.0, sublet_extra=0.0): "region": {year: unit["region"]}, "tenure_type": {year: unit["tenure"]}, "rent": {year: unit["rent"]}, - "savings": {year: unit["savings"]}, + "savings": {year: unit["savings"] + savings_extra}, "council_tax": {year: 1_500.0}, } return {"people": people, "benunits": benunits, "households": households} @@ -223,3 +231,44 @@ def test_legacy_means_test_income_tax_bounds(units, year): np.testing.assert_allclose( without["legacy_means_test_income_tax"], without["income_tax"], atol=0.01 ) + + +MAXIMUM_TARIFF_INCOME = 40 * 4.35 * 12 +ENGLISH_REGIONS = {"NORTH_EAST", "LONDON"} + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS), st.floats(1, 20_000)) +def test_council_tax_reduction_tariff_income_is_bounded_and_monotone( + units, year, extra +): + variables = [ + "council_tax_reduction_tariff_income", + "council_tax_reduction_pensioner", + "in_receipt_of_guarantee_credit", + "in_receipt_of_savings_credit_only", + ] + base = calculate(units, year, variables) + more = calculate(units, year, variables, savings_extra=extra) + tariff = base["council_tax_reduction_tariff_income"] + assert np.all(tariff >= 0) + assert np.all(tariff <= MAXIMUM_TARIFF_INCOME + 0.01) + region = np.array([unit["region"] for unit in units]) + pensioner = base["council_tax_reduction_pensioner"].astype(bool) + outside_national_schemes = (region == "NORTHERN_IRELAND") | ( + np.isin(region, list(ENGLISH_REGIONS)) & ~pensioner + ) + assert np.all(tariff[outside_national_schemes] == 0) + + def route(values): + return ( + values["council_tax_reduction_pensioner"].astype(int) * 4 + + values["in_receipt_of_guarantee_credit"].astype(int) * 2 + + values["in_receipt_of_savings_credit_only"].astype(int) + ) + + same_route = route(base) == route(more) + assert np.all( + more["council_tax_reduction_tariff_income"][same_route] + >= tariff[same_route] - 0.01 + ) diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py index f8139532e6..edf28cf15d 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py @@ -16,13 +16,25 @@ class legacy_means_test_income_tax(Variable): "which covers earnings, pensions and taxable benefits) at Step 4 of " "Income Tax Act 2007 s.23, less that income's share of the Step 6 tax " "reductions (the married couple's allowance and other tax credits). " - "Neither tax law nor the benefit regulations say which income a " - "reduction relieves (s.27 orders reductions only to give the greatest " - "reduction in liability), so the model shares them in proportion to the " - "Step 4 tax on each kind of income. Step 7 charges, such as the High " - "Income Child Benefit Charge and the pension annual allowance charge " - "(s.30), are not tax calculated on any of this income and are not " - "deducted." + "Most reductions are not tied to any income: s.27 orders them only to " + "give the greatest reduction in liability. Some are: foreign tax " + "credit relief reduces the tax on the income the foreign tax was paid " + "on (Taxation (International and Other Provisions) Act 2010 ss.18(2) " + "and 36), and the relief for residential finance costs is given on at " + "most the property business's profits (Income Tax (Trading and Other " + "Income) Act 2005 ss.274A and 274AA). The model's other_tax_credits " + "input mixes these with other reductions (venture capital trust, " + "enterprise investment and maintenance relief, among others) and " + "records no source, so the model shares all reductions in proportion " + "to the Step 4 tax on each kind of income. A credit for foreign tax " + "on rent is therefore partly attributed to counted income, and the " + "tax deducted is too low. Step 7 charges, such as the High Income " + "Child Benefit Charge and the pension annual allowance charge (s.30), " + "are not tax calculated on any of this income and are not deducted. " + "The result is income_tax less the tax attributed to capital and the " + "charges, floored at nil, so a supplied income_tax value flows " + "through; if it differs from the tax the components imply, the " + "difference falls on the tax on counted income." ) definition_period = YEAR unit = GBP @@ -35,6 +47,10 @@ class legacy_means_test_income_tax(Variable): "https://www.legislation.gov.uk/ukpga/2007/3/section/23", "https://www.legislation.gov.uk/ukpga/2007/3/section/27", "https://www.legislation.gov.uk/ukpga/2007/3/section/30", + "https://www.legislation.gov.uk/ukpga/2010/8/section/18", + "https://www.legislation.gov.uk/ukpga/2010/8/section/36", + "https://www.legislation.gov.uk/ukpga/2005/5/section/274A", + "https://www.legislation.gov.uk/ukpga/2005/5/section/274AA", ] def formula(person, period, parameters): diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 53bba409a8..deeda0fdcc 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -16,16 +16,20 @@ class council_tax_reduction_applicable_income(Variable): "the savings credit payable, where the award is savings credit only. " "The other adjustments those provisions allow (childcare charges, lone " "parent and maintenance disregards, and the rest) are not modelled. " - "No scheme counts income derived from capital, such as rent from property, interest and " - "dividends, as income, so it is not listed and tax on it is not " - "deducted. The pensioner schemes disregard any actual income from " - "capital; the Welsh working-age scheme and the English default scheme " - "treat it as capital; the Scottish working-age scheme counts only the " - "unearned income it lists, which includes the assumed yield from " - "capital but not actual rent, interest or dividends. Rent for letting " - "part of the home counts, less the sub-tenant disregard, except in the " - "Scottish scheme for people under pension age from April 2022, which " - "does not list it." + "No scheme counts income derived from capital, such as rent from " + "property, interest and dividends, as income, so it is not listed and " + "tax on it is not deducted. The pensioner schemes disregard any actual " + "income from capital; the Welsh working-age scheme and the English " + "default scheme treat it as capital; the Scottish working-age scheme " + "counts only the unearned income it lists, which includes the assumed " + "yield from capital but not actual rent, interest or dividends. " + "Instead, the national schemes (England for pensioners, Wales and " + "Scotland) count the tariff income they treat capital as yielding " + "(council_tax_reduction_tariff_income); the English local schemes for " + "people under pension age add their own. Rent for letting part of the " + "home counts, less the sub-tenant disregard, except in the Scottish " + "scheme for people under pension age from April 2022, which does not " + "list it." ) definition_period = YEAR unit = GBP @@ -43,6 +47,7 @@ class council_tax_reduction_applicable_income(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/4", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/9", "https://www.legislation.gov.uk/ssi/2012/319/schedule/3", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/3", "https://www.legislation.gov.uk/ssi/2021/249/regulation/57", "https://www.legislation.gov.uk/ssi/2021/249/regulation/63", ] @@ -92,23 +97,16 @@ def formula(benunit, period, parameters): # Rent for letting part of the home counts in every scheme except the # Scottish one for people under pension age from 1 April 2022 (SSI # 2021/249, whose reg 57(1) list has no head for it). That scheme - # applies under reg 3(1) to an applicant under pensionable age, or over - # it where the applicant or partner has an award of universal credit or - # a qualifying income-related benefit. - claimant_or_partner_over_pension_age = benunit.any( - person("is_SP_age", period) & person("is_claimant_or_partner", period) - ) - on_uc_or_income_related_benefit = ( - add( - benunit, - period, - ["universal_credit", "income_support", "jsa_income", "esa_income"], - ) - > 0 - ) + # applies under reg 3(1) to everyone the pension-age scheme does not + # cover: an applicant under pensionable age, or over it where the + # applicant or partner has a qualifying income-related benefit or an + # award of universal credit (council_tax_reduction_pensioner). An award + # held only by people over pensionable age does not count (reg 3(2)); + # the selector stands this in by requiring a younger claimant or + # partner, as Universal Credit itself does (UC Regs 2013 reg 3(2)(a)). scotland = is_scotland_scheme(benunit.household("country", period)) - scottish_working_age_scheme = scotland & ( - ~claimant_or_partner_over_pension_age | on_uc_or_income_related_benefit + scottish_working_age_scheme = scotland & ~benunit( + "council_tax_reduction_pensioner", period ) p_scotland = parameters( period @@ -136,9 +134,12 @@ def formula(benunit, period, parameters): ["legacy_means_test_income_tax", "national_insurance"], members, ) + # The national schemes also treat capital as yielding a tariff income + # (council_tax_reduction_tariff_income); the English local schemes for + # people under pension age add their own. income_under_general_rules = max_( 0, increased_income - tax - pension_contributions - ) + ) + benunit("council_tax_reduction_tariff_income", period) # SI 2012/2885 Sch 1 para 13, WSI 2013/3029 Sch 1 para 7 and SSI # 2012/319 reg 24: a guarantee credit recipient's whole income is # disregarded. Para 14, para 8 and reg 25: in savings-credit-only cases diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py index 5a6dbd4a77..eccfbe7d08 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_scotland_scheme, +) class council_tax_reduction_assessable_capital(Variable): @@ -6,17 +9,25 @@ class council_tax_reduction_assessable_capital(Variable): entity = BenUnit label = "capital for the Council Tax Reduction capital limit" documentation = ( - "Capital tested against the Council Tax Reduction capital limit. The " - "pensioner schemes disregard the whole capital of an applicant who, or " - "whose partner, is in receipt of Pension Credit guarantee credit, and " - "use the Secretary of State's Pension Credit assessment of capital where " - "the award is savings credit only. Otherwise the household's capital " - "stands in for the applicant's: savings, land and property other than " - "the home (gov.local_authorities.council_tax_reduction.capital_sources). " - "The schemes also count shares and other investments, but in the data " - "those are mixed with pension wealth the schemes disregard, so they are " - "left out for now. The recalculation when capital rises above the limit " - "during an assessed income period is not modelled." + "Capital tested against the Council Tax Reduction capital limit and " + "used for tariff income. The pensioner schemes disregard the whole " + "capital of an applicant who, or whose partner, is in receipt of " + "Pension Credit guarantee credit, and use the Secretary of State's " + "Pension Credit assessment of capital where the award is savings " + "credit only. Where the applicant or partner has an award of Universal " + "Credit, the working-age schemes use the Secretary of State's " + "assessment of capital for that award (uc_assessable_capital), except " + "the Scottish scheme from April 2022, under which the authority " + "calculates capital itself. Otherwise the household's capital stands " + "in for the applicant's: savings, land and property other than the " + "home (gov.local_authorities.council_tax_reduction.capital.sources), " + "with land and property valued at market value less 10% for the " + "expenses of sale. The schemes also deduct any encumbrance secured on " + "an asset, which the data cannot identify for property other than the " + "home, and count shares and other investments, which in the data are " + "mixed with pension wealth the schemes disregard; both are left out " + "for now. The recalculation when capital rises above the limit during " + "an assessed income period is not modelled." ) definition_period = YEAR unit = GBP @@ -24,13 +35,18 @@ class council_tax_reduction_assessable_capital(Variable): reference = [ "https://www.legislation.gov.uk/uksi/2012/2885/regulation/11", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/31", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/32", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/13", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/14", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/37", "https://www.legislation.gov.uk/wsi/2013/3029/regulation/30", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/7", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/9", "https://www.legislation.gov.uk/ssi/2012/319/regulation/24", "https://www.legislation.gov.uk/ssi/2012/319/regulation/25", + "https://www.legislation.gov.uk/ssi/2012/303/regulation/26", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/70", ] def formula(benunit, period, parameters): @@ -40,12 +56,37 @@ def formula(benunit, period, parameters): has_savings_credit_only_award = benunit( "in_receipt_of_savings_credit_only", period ) - sources = parameters( + # SI 2012/2886 Sch para 37(6), WSI 2013/3029 Sch 6 para 9(6) and SSI + # 2012/303 reg 26: with an award of Universal Credit, the authority + # uses the Secretary of State's calculation of capital. SSI 2021/249 + # has no such rule. + p_scotland = parameters( period - ).gov.local_authorities.council_tax_reduction.capital_sources - household_capital = sum(benunit.household(source, period) for source in sources) + ).gov.local_authorities.scotland.council_tax_reduction.means_test + scotland = is_scotland_scheme(benunit.household("country", period)) + uses_universal_credit_assessment = ~scotland | ( + p_scotland.uses_universal_credit_capital_assessment + ) + has_uc_award = benunit("universal_credit", period) > 0 + p = parameters(period).gov.local_authorities.council_tax_reduction.capital + # Capital is valued at market value less 10% where there would be + # expenses of sale (SI 2012/2885 Sch 1 para 32(a) and equivalents). + household_capital = 0 + for source in p.sources: + value = benunit.household(source, period) + if source in p.sale_expenses.sources: + value = value * (1 - p.sale_expenses.rate) + household_capital = household_capital + value return select( - [in_receipt_of_guarantee_credit, has_savings_credit_only_award], - [0, benunit("pension_credit_assessable_capital", period)], + [ + in_receipt_of_guarantee_credit, + has_savings_credit_only_award, + has_uc_award & uses_universal_credit_assessment, + ], + [ + 0, + benunit("pension_credit_assessable_capital", period), + benunit("uc_assessable_capital", period), + ], default=household_capital, ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py index a9da7f9aa3..097ed4ca7a 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner.py @@ -13,7 +13,9 @@ class council_tax_reduction_pensioner(Variable): "Employment and Support Allowance, or has an award of Universal Credit. " "A mixed-age couple on Universal Credit is therefore not a pensioner in " "England, Wales or Scotland. The model routes English schemes on this " - "test; the Welsh and Scottish formulas do not yet use it." + "test, and uses it to choose the pensioner or working-age tariff " + "income and the Scottish income rules; the Welsh and Scottish award " + "formulas do not otherwise use it yet." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py new file mode 100644 index 0000000000..0a58306ba8 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py @@ -0,0 +1,132 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_scotland_scheme, + is_wales_scheme, +) +from policyengine_uk.variables.household.demographic.country import Country + + +def tariff_amount(capital, tariff, capital_limit): + # `amount` for each complete `step` of capital above `threshold`, and + # `amount` for any part of a step. Capital above the limit leaves no + # entitlement, so it is not counted. + excess = max_(0, min_(capital, capital_limit) - tariff.threshold) + return np.ceil(excess / tariff.step) * tariff.amount + + +class council_tax_reduction_tariff_income(Variable): + value_type = float + entity = BenUnit + label = "Council Tax Reduction tariff income from capital" + documentation = ( + "Income the national Council Tax Reduction schemes (England for " + "pensioners, Wales and Scotland) treat capital as yielding, annualised. " + "Pensioners: £1 a week for each £500, or part of £500, of capital over " + "£10,000. People under pension age in Wales and Scotland: £1 a week " + "for each £250, or part of £250, over £6,000; with an award of " + "Universal Credit, the Universal Credit assumed yield of £4.35 a month " + "for each £250, or part, over £6,000, which the Welsh scheme and the " + "Scottish scheme to March 2022 take from the Secretary of State's " + "assessment of income and the Scottish scheme from April 2022 sets at " + "the same rate. Nil for the Pension Credit routes, whose income " + "already includes the Pension Credit deemed income (or is wholly " + "disregarded), for people on Income Support, income-based " + "Jobseeker's Allowance or income-related Employment and Support " + "Allowance, whose whole capital the Welsh scheme and the Scottish " + "scheme to March 2022 disregard and whose income the Scottish scheme " + "from April 2022 does not assess, and for English people under " + "pension age, whose local schemes set their own tariff income." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/37", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/31", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/9", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/33", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/10/paragraph/8", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/27", + "https://www.legislation.gov.uk/ssi/2012/303/regulation/26", + "https://www.legislation.gov.uk/ssi/2012/303/regulation/51", + "https://www.legislation.gov.uk/ssi/2012/303/schedule/5/paragraph/7", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/13", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/63", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/72", + ] + + def formula(benunit, period, parameters): + local_authorities = parameters(period).gov.local_authorities + england_p = local_authorities.england.council_tax_reduction.pensioners + wales_p = local_authorities.wales.council_tax_reduction.means_test + scotland_p = local_authorities.scotland.council_tax_reduction.means_test + uc_tariff = parameters( + period + ).gov.dwp.universal_credit.means_test.capital.tariff_income + country = benunit.household("country", period) + england = country == Country.ENGLAND + wales = is_wales_scheme(country) + scotland = is_scotland_scheme(country) + capital = benunit("council_tax_reduction_assessable_capital", period) + pensioner_weekly = select( + [england, wales, scotland], + [ + tariff_amount( + capital, + england_p.means_test.tariff_income, + england_p.means_test.capital_limit, + ), + tariff_amount( + capital, + wales_p.tariff_income.pensioner, + wales_p.capital_limit, + ), + tariff_amount( + capital, + scotland_p.tariff_income.pensioner, + scotland_p.capital_limit, + ), + ], + default=0, + ) + working_age_weekly = select( + [wales, scotland], + [ + tariff_amount( + capital, + wales_p.tariff_income.working_age, + wales_p.capital_limit, + ), + tariff_amount( + capital, + scotland_p.tariff_income.working_age, + scotland_p.capital_limit, + ), + ], + default=0, + ) + universal_credit_monthly = select( + [wales, scotland], + [ + tariff_amount(capital, uc_tariff, wales_p.capital_limit), + tariff_amount(capital, uc_tariff, scotland_p.capital_limit), + ], + default=0, + ) + pensioner = benunit("council_tax_reduction_pensioner", period) + has_uc_award = benunit("universal_credit", period) > 0 + income_based_benefit = benunit( + "council_tax_reduction_relevant_income_based_benefit", period + ) + tariff_income = select( + [pensioner, has_uc_award, income_based_benefit], + [ + pensioner_weekly * WEEKS_IN_YEAR, + universal_credit_monthly * MONTHS_IN_YEAR, + 0, + ], + default=working_age_weekly * WEEKS_IN_YEAR, + ) + pension_credit_route = benunit( + "in_receipt_of_guarantee_credit", period + ) | benunit("in_receipt_of_savings_credit_only", period) + return where(pension_credit_route, 0, tariff_income) diff --git a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py index 59fe9d99fd..4bc9c3c35b 100644 --- a/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py +++ b/policyengine_uk/variables/gov/local_authorities/oxford/council_tax_reduction/oxford_council_tax_reduction.py @@ -11,6 +11,10 @@ class oxford_council_tax_reduction(Variable): label = "Oxford Council Tax Reduction" definition_period = YEAR unit = GBP + reference = ( + "https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27", + "https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27/4", + ) def formula(benunit, period, parameters): ctr = parameters(period).gov.local_authorities.oxford.council_tax_reduction @@ -25,7 +29,19 @@ def formula(benunit, period, parameters): universal_credit = benunit("universal_credit", period) has_uc_award = universal_credit > 0 - capital = benunit("council_tax_reduction_assessable_capital", period) + relevant_income_based_benefit = benunit( + "council_tax_reduction_relevant_income_based_benefit", + period, + ) + # Scheme paragraphs 28-29 apply the limit to capital not ignored by + # Appendix 6, which ignores capital already taken into account for + # Universal Credit, Income Support, income-based Jobseeker's Allowance + # or income-related Employment and Support Allowance. + capital = where( + has_uc_award | relevant_income_based_benefit, + 0, + benunit("council_tax_reduction_assessable_capital", period), + ) capital_eligible = capital <= ctr.means_test.capital_limit weekly_tariff_income = np.ceil( max_(0, capital - ctr.means_test.tariff_income_threshold) @@ -36,10 +52,6 @@ def formula(benunit, period, parameters): child_benefit = benunit("child_benefit", period) weekly_income = max_(0, annual_income - child_benefit) / WEEKS_IN_YEAR weekly_income += where(has_uc_award, 0, weekly_tariff_income) - relevant_income_based_benefit = benunit( - "council_tax_reduction_relevant_income_based_benefit", - period, - ) weekly_income = where( relevant_income_based_benefit & ~has_uc_award, 0, weekly_income ) From 274e6293a5881dda931c50b395a4c52a94e9621c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 17:07:51 -0400 Subject: [PATCH 06/24] Correct main's positive-property Pension Credit case to Sch IV para 18 Co-Authored-By: Claude Opus 5.5 --- .../gov/dwp/pension_credit/pension_credit_income.yaml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml index b0ed506227..06f01b2730 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml @@ -177,7 +177,7 @@ pension_credit_income: 18_514 pension_credit: 0 -- name: Positive property income still counts in Pension Credit income. +- name: Positive property income does not count in Pension Credit income, nor does its tax. period: 2026 absolute_error_margin: 1 input: @@ -195,8 +195,11 @@ region: WALES tenure_type: OWNED_OUTRIGHT output: - # 20,000 + 3,000, less income tax of 20% of (23,000 - 12,570) = 2,086. - pension_credit_income: 20_914 + # SPC Regs 2002 Sch IV para 18 disregards actual income from capital; the + # let property counts through deemed income instead. Only the tax on the + # pension is deducted (reg 17(10)(a)): 20,000 - 20% x (20,000 - 12,570) = + # 18,514, as without the property. + pension_credit_income: 18_514 pension_credit: 0 # Issue #2255, earnings. A self-employment loss is not set against wages From 0d36721b0789f26f85df202694e102e0095b3f99 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 17:14:28 -0400 Subject: [PATCH 07/24] Correct main's finance-cost Housing Benefit case: rent from a let property is capital Co-Authored-By: Claude Opus 5.5 --- .../property_income_after_finance_costs.yaml | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml b/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml index 902f7720a1..d232d83771 100644 --- a/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml +++ b/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml @@ -1,10 +1,12 @@ -# Means tests count property income after the costs of dwelling-related -# loans. Tax credits do so by statute: The Tax Credits (Definition and -# Calculation of Income) Regulations 2002 reg. 11(2A) disregards the ITTOIA -# 2005 s. 272A restriction, and reg. 11(1) counts "annual taxable profits", so -# costs above the profit leave nil rather than a loss. The legacy means tests -# deduct income tax, which the finance-cost reduction lowers, so they count -# the profit after the costs too. +# Tax credits count property income after the costs of dwelling-related +# loans by statute: The Tax Credits (Definition and Calculation of Income) +# Regulations 2002 reg. 11(2A) disregards the ITTOIA 2005 s. 272A +# restriction, and reg. 11(1) counts "annual taxable profits", so costs above +# the profit leave nil rather than a loss. The legacy means tests treat rent +# from a let property as capital (HB Regs 2006 reg 46(4) and Sch 5 para 17; +# HB (SPC) Regs 2006 Sch 5 para 22), so it counts in neither case, and they +# deduct only the tax on the income they count: the finance-cost reduction +# comes off the tax on the rent (legacy_means_test_income_tax). - name: Finance costs are deducted from property income for tax credits period: 2023 @@ -56,8 +58,10 @@ output: # (16,000 + 12,000 + 6,000 - 12,570) x 20%. income_tax: 4_286 - # 16,000 + 12,000 + 6,000 - 4,286, less the 5 a week earnings disregard. - housing_benefit_applicable_income: 29_454 + # The rent does not count, nor its tax: the tax on the earnings and + # pension is (28,000 - 12,570) x 20% = 3,086, so 16,000 + 12,000 - 3,086, + # less the 5 a week earnings disregard. + housing_benefit_applicable_income: 24_654 - name: Housing Benefit, finance costs equal to the profit do not lower it period: 2026 @@ -74,6 +78,7 @@ # 4,286 less the 20% x 6,000 reduction. income_tax: 3_086 property_income_after_finance_costs: 0 - # 16,000 + 12,000 - 3,086 - 260: the property income nets to nil, so the - # lower tax does not raise the income counted. + # 16,000 + 12,000 - 3,086 - 260: the rent does not count, and the 1,200 + # reduction comes off the tax on the rent, so the tax on the earnings and + # pension deducted is still 3,086. housing_benefit_applicable_income: 24_654 From 824cfddd90d75d0bea38f36b45c4f067684b614a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 17:25:59 -0400 Subject: [PATCH 08/24] Test the Scottish selector with a mixed-age couple on Income Support The UC-route home-letting exclusion now covers the mixed-age UC case, so a mixed-age couple on Income Support is what distinguishes the reg 3 selector from an age-only one (a surviving mutant in the round-3 check). Co-Authored-By: Claude Opus 5.5 --- .../income_from_capital.yaml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 7c4227b27c..0be21743c4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -1657,3 +1657,33 @@ other_residential_property_secured_debt: 15_000 output: council_tax_reduction_assessable_capital: 12_000 + +- name: A Scottish mixed-age couple on Income Support is under the working-age scheme + # SSI 2021/249 reg 3(1): the scheme for people under pensionable age applies + # where the applicant has reached pensionable age but the applicant or + # partner has a qualifying income-related benefit, here Income Support (an + # input, with no Universal Credit). Its reg 57(1) has no head for + # sub-tenant rent, so only the Income Support, 3,000, counts. + period: 2026 + input: + people: + claimant: + age: 70 + sublet_income: 5_200 + partner: + age: 40 + benunits: + benunit: + members: [claimant, partner] + income_support: 3_000 + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [claimant, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + council_tax_reduction_pensioner: false + legacy_benefits_home_letting_income: 4_160 + council_tax_reduction_applicable_income: 3_000 From 16ce51e418588fde489ca4e66b5beedeb0c55242 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 17:37:07 -0400 Subject: [PATCH 09/24] Fix a missing space in the CTR applicable income documentation Co-Authored-By: Claude Opus 5.5 --- .../council_tax_reduction_applicable_income.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 52ccf66a65..fb801cc329 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -29,7 +29,7 @@ class council_tax_reduction_applicable_income(Variable): "awards (Child Benefit, Income Support, income-based JSA, " "income-related ESA, Universal Credit, tax credits, Pension Credit) " "and its children belong to its claimant and partner, not to the " - "head, so none of them counts." + "head, so none of them counts. " "No scheme " "counts income derived from capital, such as rent from property, " "interest and dividends, as income, so it is not listed and tax on it " From b379e005601ca0329e56e440764f458f9fd5a708 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 18:34:31 -0400 Subject: [PATCH 10/24] Test that a Scottish passported applicant's capital yields no tariff income The Welsh passported case no longer distinguishes the tariff's passport branch, because Wales now disregards the whole capital; Scotland from April 2022 counts the capital but does not assess the income (SSI 2021/249 reg 13(11)). Kills a surviving mutant. Co-Authored-By: Claude Opus 5.5 --- .../income_from_capital.yaml | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 0be21743c4..924d09269d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -1687,3 +1687,29 @@ council_tax_reduction_pensioner: false legacy_benefits_home_letting_income: 4_160 council_tax_reduction_applicable_income: 3_000 + +- name: A Scottish Income Support recipient's capital counts but yields no tariff income + # SSI 2021/249 has no disregard of a passported applicant's capital, so the + # 10,000 of savings is the applicant's capital (under the 16,000 limit, + # reg 13(1)). But reg 13(11) treats a qualifying income-related benefit + # claimant as meeting the income conditions, so their income, including + # the reg 63 assumed yield, is not assessed: nil tariff income. + period: 2026 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + would_claim_uc: false + income_support: 4_000 + households: + household: + members: [person] + country: SCOTLAND + local_authority: GLASGOW_CITY + savings: 10_000 + output: + council_tax_reduction_assessable_capital: 10_000 + council_tax_reduction_tariff_income: 0 From 91bca5b974e9ff79fc8940c913c35890b5755dae Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:41:21 -0400 Subject: [PATCH 11/24] Test that Oxford adds no tariff income for a capped Universal Credit award MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A nil-payment award is still an award (Appendix 3), and Appendix 6 ignores the capital taken into account for it. At 24,000 a year of income, the £36 weekly tariff on £15,000 that a payment-only test would add moves the applicant from band 1 (100%) to band 2 (75%). This kills the surviving "Oxford tests payment, not award" mutant. Co-Authored-By: Claude Opus 5.5 --- .../income_from_capital.yaml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 924d09269d..207935e161 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -1343,6 +1343,43 @@ oxford_council_tax_reduction: 2_000 council_tax_reduction: 2_000 +- name: Oxford adds no tariff income from a capped Universal Credit award's capital + # As above, the award is capped to nil but is still an award (Appendix 3), + # and Appendix 6 ignores the £15,000 taken into account for it, so no tariff + # income is added. Weekly income 24,000 / 52 = 461.54, in band 1 (up to + # £480): 100% of 2,000. Treating the nil payment as no award would add + # ceil((15,000 - 6,000) / 250) = £36 a week, giving 497.54, band 2 (75%). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + claims_all_entitled_benefits: true + claims_universal_credit: true + universal_credit_pre_benefit_cap: 1_000 + uc_benefit_cap_reduction_after_floor: 1_000 + uc_reported_capital: 15_000 + council_tax_reduction_applicable_amount: 6_000 + council_tax_reduction_applicable_income: 24_000 + households: + household: + members: [person] + country: ENGLAND + local_authority: OXFORD + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + other_residential_property_value: 20_000 + output: + universal_credit: 0 + council_tax_reduction_universal_credit_award: true + council_tax_reduction_assessable_capital: 15_000 + oxford_council_tax_reduction: 2_000 + council_tax_reduction: 2_000 + - name: From April 2022 Scotland applies the Universal Credit yield to a capped award on its own capital # SSI 2021/249 reg 10A: an award includes one reduced to nil; reg 63(1)(a) # applies the monthly yield, £4.35 per £250 over £6,000, to the capital From aaf44f107d72aa1225c4e59dcfa95dcf091f0b98 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 01:56:46 -0400 Subject: [PATCH 12/24] Correct the SSI 2021/249 reg 10A citation for capped Universal Credit awards Reg 10A (inserted 1 April 2025 by SSI 2025/24) adds an award reduced to zero by a sanction and one ended after a nil award due to earnings but treated as reclaimed. It says nothing about the benefit cap. A capped award is still an award because UC Regs 2013 reg 81 reduces the award's amount. The award variable now says what reg 10A covers and that neither of its cases is modelled. The Scottish capped-award case cites reg 81 and the monthly-yield chain: reg 57(1)(l), reg 64(1), reg 63(1)(a). There is no behaviour change. Co-Authored-By: Claude Opus 5.5 --- .../gov/dwp/legacy_means_tests/income_from_capital.yaml | 7 +++++-- .../council_tax_reduction_universal_credit_award.py | 6 +++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 207935e161..4fcf8c9c3d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -1381,8 +1381,11 @@ council_tax_reduction: 2_000 - name: From April 2022 Scotland applies the Universal Credit yield to a capped award on its own capital - # SSI 2021/249 reg 10A: an award includes one reduced to nil; reg 63(1)(a) - # applies the monthly yield, £4.35 per £250 over £6,000, to the capital + # The benefit cap reduces the award rather than ending it (UC Regs 2013 + # reg 81), so there is still an award of Universal Credit. With one, + # unearned income, which includes the assumed yield (reg 57(1)(l)), is + # calculated monthly (reg 64(1)), so reg 63(1)(a) applies the monthly + # yield, £4.35 per £250 over £6,000, to the capital # the authority calculates, here the £18,000 of property after sale # expenses (counted up to the limit: 40 x 4.35 x 12 = 2,088). It is over the # £16,000 limit, so there is no award. diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_universal_credit_award.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_universal_credit_award.py index 333bdd0b1e..90c2faeb01 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_universal_credit_award.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_universal_credit_award.py @@ -12,7 +12,11 @@ class council_tax_reduction_universal_credit_award(Variable): "reduce an award rather than end it (UC Regs 2013 reg 81; Oxford's " "scheme, Appendix 3, covers applicants who would be awarded it but " "for deductions). A supplied universal_credit amount counts as an " - "award. The benefit unit's award belongs to its claimant and partner, " + "award. From 1 April 2025 the Scottish scheme also counts an award " + "reduced to zero by a sanction and one whose entitlement ended after " + "a nil award due to earnings but is treated as reclaimed (SSI " + "2021/249 reg 10A); neither is modelled. The benefit unit's award " + "belongs to its claimant and partner, " "so it is not the applicant's where the household head applies " "alone. The schemes' working-age Universal Credit rules (the " "Secretary of State's assessments of income and capital, and the " From 5edc3d05c97f5b64478ea06031fe96013bccd872 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 12:17:26 -0400 Subject: [PATCH 13/24] Return the dividends case's pension_credit expectation to it The wages-and-dividends case was inserted between the "Pension Credit does not deduct tax on dividends" case's savings_credit and pension_credit outputs, which moved the 668.216 expectation into the new case. The new case's own expectations all hold, matching the hand calculation: income tax 4,899.75, tax on earnings 2,368.375, uncounted tax 2,531.375 and income 19,371.625. All 62 cases in the file pass. Co-Authored-By: Claude Opus 5.5 --- .../gov/dwp/legacy_means_tests/income_from_capital.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 8776c68de5..e71f4de87b 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -175,6 +175,7 @@ pension_credit_income: 13_040 guarantee_credit: 0 savings_credit: 668.216 + pension_credit: 668.216 - name: Pension Credit deducts the tax wages add on dividends, but not the tax on dividends due without wages # 2025-26. Wages 10,000 and State Pension 12,000: non-savings tax (22,000 - @@ -209,7 +210,6 @@ pension_credit_uncounted_income_tax_without_earnings: 2_531.375 pension_credit_earnings_disregard: 260 pension_credit_income: 19_371.625 - pension_credit: 668.216 - name: English pensioner council tax reduction counts sub-tenant rent less £20, not other rent # SI 2012/2885 Sch 1 para 16(1)(v) and Sch 5 para 10 for the sub-tenant rent: From 1b8f2dedea4f9d579d4af0150d6325a666a1c707 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 12:54:58 -0400 Subject: [PATCH 14/24] Restate the capital-income and trading-loss properties for the taxes now deducted test_income_from_capital_changes_no_legacy_means_test: with earnings, #2019's tax on earnings (Max's d1424 ruling) includes the extra tax earnings cause on savings and dividends. Scaling income from capital can therefore move Pension Credit income. Hypothesis found such a case: a couple whose wages of 6,707 sit under the personal allowance, so at three times the dividends 279.77 of dividend tax is induced and deducted. The property now asserts: - invariance wherever pension_credit_employment_income_tax does not move; - otherwise, that Pension Credit income moves by exactly minus that change and the disregard's. The awards that follow Pension Credit are compared wherever the tax on earnings is steady. The docstring labels this an intended exception. test_means_tests_see_the_loss_only_through_tax: the legacy means tests deduct legacy_means_test_income_tax, and Pension Credit's new uncounted-tax term comes off too. Both are recomputed from the tax components, so with income_tax held a loss that moves interest between bands moved them, and Housing Benefit income with them (1,047 in the failing family). Both are now held, as the tax the tests deduct. Both pass single-process, with derandomized examples. Co-Authored-By: Claude Opus 5.5 --- ...ans_test_income_from_capital_properties.py | 54 +++++++++++++++++-- .../tests/test_trading_loss_properties.py | 7 +++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 92674f50f1..c2188fac00 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -20,6 +20,15 @@ depends on their income including income from capital, which is a legitimate way for income from capital to change the tax on counted income. + Intended exception: Pension Credit's tax on earnings is the extra tax the + earnings cause, including on savings and dividends (#2019, Max's ruling + d1424). With earnings, income from capital can move that tax: earnings + under the personal allowance can leave dividends taxable. So Pension + Credit income is invariant wherever pension_credit_employment_income_tax + does not move, and elsewhere changes by exactly minus the change in that + tax and in the earnings disregard. The awards that follow Pension Credit + (Pension Credit, and Housing Benefit and council tax reduction for a + family receiving it) are compared wherever it does not move. 2. Bounds: the counted home-letting income is between nil and the rent, and equals the rent less £20 a week, floored at nil. 3. Monotone: more rent from part of the home never lowers a means-test income @@ -166,14 +175,53 @@ def calculate(units, year, variables, **kwargs): @PROPERTY_SETTINGS @given(populations, st.sampled_from(YEARS)) def test_income_from_capital_changes_no_legacy_means_test(units, year): - variables = MEANS_TEST_INCOMES + AWARDS + earnings_tax = [ + "pension_credit_employment_income_tax", + "pension_credit_earnings_disregard", + ] + variables = MEANS_TEST_INCOMES + AWARDS + earnings_tax base = calculate(units, year, variables) for scale in (0.0, 3.0): scaled = calculate(units, year, variables, capital_income_scale=scale) - for v in variables: + moved = {v: scaled[v] - base[v] for v in earnings_tax} + # Intended: income from capital reaches Pension Credit only through + # the tax the earnings cause on it (#2019, d1424). + steady = np.abs(moved["pension_credit_employment_income_tax"]) < 0.01 + on_pension_credit = (base["pension_credit"] > 0) | ( + scaled["pension_credit"] > 0 + ) + follows_pension_credit = { + "pension_credit_income": ~steady, + "pension_credit": ~steady, + "housing_benefit": ~steady & on_pension_credit, + "housing_benefit_applicable_income": ~steady & on_pension_credit, + "council_tax_benefit": ~steady & on_pension_credit, + "council_tax_reduction_applicable_income": ~steady & on_pension_credit, + } + for v in MEANS_TEST_INCOMES + AWARDS: + compared = ~follows_pension_credit.get(v, np.zeros_like(steady)) np.testing.assert_allclose( - scaled[v], base[v], atol=0.01, err_msg=f"{v} at scale {scale}" + scaled[v][compared], + base[v][compared], + atol=0.01, + err_msg=f"{v} at scale {scale}", ) + # Where the tax on earnings moves, Pension Credit income moves by + # exactly minus that change and the disregard's, unless floored. + unfloored = (base["pension_credit_income"] > 0) & ( + scaled["pension_credit_income"] > 0 + ) + np.testing.assert_allclose( + (scaled["pension_credit_income"] - base["pension_credit_income"])[ + unfloored + ], + -( + moved["pension_credit_employment_income_tax"] + + moved["pension_credit_earnings_disregard"] + )[unfloored], + atol=0.01, + err_msg=f"pension_credit_income decomposition at scale {scale}", + ) @PROPERTY_SETTINGS diff --git a/policyengine_uk/tests/test_trading_loss_properties.py b/policyengine_uk/tests/test_trading_loss_properties.py index 2b0c05f5b3..634a250adf 100644 --- a/policyengine_uk/tests/test_trading_loss_properties.py +++ b/policyengine_uk/tests/test_trading_loss_properties.py @@ -771,6 +771,13 @@ def values(sim, variable): "blind_persons_allowance", "marriage_allowance", "married_couples_allowance_deduction", + # The legacy means tests deduct only the tax on income they count, + # and Pension Credit leaves out the tax on uncounted income due + # without earnings. Both are recomputed from the tax components, so a + # loss that moves interest or dividends between bands would move them + # with income_tax held. Hold them as the tax the tests deduct. + "legacy_means_test_income_tax", + "pension_credit_uncounted_income_tax_without_earnings", ] tax = {v: values(with_loss, v) for v in deductions} # SPC Regs reg 17(11) substitutes CER notional deductions for the tax From 8b254ec25f3370043b53dadc5ac54d1902851001 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 16:39:23 +0000 Subject: [PATCH 15/24] Test that statutory sick pay is zeroed with wages for the uncounted tax A pensioner with 1,040 of statutory sick pay and 12,000 of interest keeps Pension Credit income of 12,000, as the wages case does: the 1,086 of tax on interest due without earnings is not deducted. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6G3T6mRhNc2ZzTbNha --- .../pension_credit_earnings_disregard.yaml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml index 3a3f5a6267..772a78ea13 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -464,6 +464,40 @@ pension_credit_uncounted_income_tax_without_earnings: 1_086 pension_credit_income: 12_000 +- name: Statutory sick pay leaves Pension Credit income unchanged when the tax on interest is uncounted, as wages do. + # The wages case above with 1,040 of statutory sick pay in place of the + # wages. SPC Regs 2002 reg 17A(2)(h) makes it earnings of an employed + # earner, so the tax without earnings is measured with it zeroed. The tax + # is the same: 94 non-savings and 1,294 on interest, 1,388 in all; 1,086 + # without the sick pay. The 1,086 due without earnings is all tax on + # interest, which Pension Credit does not count (reg 17(10)(a), Sch IV + # para 18), so it is not deducted; the 302 the sick pay adds is tax on + # earnings. Net earnings and the disregard are 1,040 - 302 = 738, and + # income is 12,000 + 1,040 - 302 - 738 = 12,000. Measuring the uncounted + # tax with the sick pay left in would take 1,388 - 1,294 = 94 as counted + # tax and leave 1,086 - 94 = 992 uncounted. + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + savings_interest_income: 12_000 + attendance_allowance: 3_000 + statutory_sick_pay: 1_040 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + income_tax_before_winter_fuel_payment_charge: 1_388 + pension_credit_employment_income_tax: 302 + pension_credit_uncounted_income_tax_without_earnings: 1_086 + pension_credit_earnings_disregard: 738 + pension_credit_income: 12_000 + - name: Pension contribution relief already available without earnings does not reduce tax attributed to earnings. period: 2024 input: From 16d6dd3fd92fb5740dab968073670476482612ac Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 17:24:31 +0000 Subject: [PATCH 16/24] Restate the savings-credit-exit tests for property in CTR capital Council tax reduction capital counts other property less 10% for the expenses of sale (SI 2012/2885 Sch 1 para 32(a)), so the owner with savings of 12,000 and property of 5,000 is tested on 16,500 once Pension Credit stops, not on savings of 12,000. Both routes exceed the 16,000 limit, so the known-family check now compares capital, not the award. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6G3T6mRhNc2ZzTbNha --- ..._savings_credit_only_receipt_properties.py | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py b/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py index 98d8f646c7..f64659e41d 100644 --- a/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py +++ b/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py @@ -394,9 +394,10 @@ def test_known_families_detect_main(): ) assert not np.any(fix[FLAG]) # Main keeps the Pension Credit assessment. The owner with capital is - # tested on 17,000 against the 16,000 limit and gets no council tax - # reduction, where the scheme's own rules give 2,000 on savings of - # 12,000. The tenant has no Housing Benefit tariff income, where + # tested on 17,000, where the scheme's own rules count the savings of + # 12,000 and the property less 10% for the expenses of sale, 16,500. + # Both exceed the 16,000 limit, so neither gives a reduction. The + # tenant has no Housing Benefit tariff income, where # Housing Benefit's own rules count 4 a week on savings of 12,000 # (208). Its Housing Benefit income is the same on both routes: with # no savings credit paid, the Pension Credit assessment is the State @@ -405,12 +406,11 @@ def test_known_families_detect_main(): FLAG, "council_tax_reduction_applicable_income", "council_tax_reduction_assessable_capital", - "simulated_council_tax_reduction_benunit", "housing_benefit_tariff_income", ] - reduction = "simulated_council_tax_reduction_benunit" - assert main[reduction][OWNER_WITH_CAPITAL] == 0 - assert abs(fix[reduction][OWNER_WITH_CAPITAL] - 2_000) < 0.01 + capital = "council_tax_reduction_assessable_capital" + assert main[capital][OWNER_WITH_CAPITAL] == 17_000 + assert fix[capital][OWNER_WITH_CAPITAL] == 16_500 assert main["housing_benefit_tariff_income"][TENANT] == 0 assert abs(fix["housing_benefit_tariff_income"][TENANT] - 208) < 0.01 assert assessed_differently(fix, not_claiming, known) == [] @@ -509,9 +509,11 @@ def test_stopping_pension_credit_payments_ends_the_pension_credit_capital_test() assert unreformed.calculate(capital, YEAR)[0] == 17_000 assert unreformed.calculate("council_tax_reduction", YEAR)[0] == 0 - # With Pension Credit not paid the scheme's own capital rule applies, for - # which household savings (12,000) stand in, within the limit. + # With Pension Credit not paid the scheme's own capital rule applies: the + # savings and the property less 10% for the expenses of sale (SI + # 2012/2885 Sch 1 para 32(a)), 12,000 + 4,500 = 16,500. That is also + # above the limit, so there is still no reduction. stopped = Simulation(situation=data, scenario=scenario(stopped=True)) assert stopped.calculate(FLAG, YEAR).tolist() == [False] - assert stopped.calculate(capital, YEAR)[0] == 12_000 - assert abs(stopped.calculate("council_tax_reduction", YEAR)[0] - 2_000) < 0.01 + assert stopped.calculate(capital, YEAR)[0] == 16_500 + assert stopped.calculate("council_tax_reduction", YEAR)[0] == 0 From a8a428834baf2737ee5c791ff549d8f874251ca6 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 19:38:51 +0000 Subject: [PATCH 17/24] Deduct one income tax measure in the four legacy means tests Income Support, Housing Benefit and the council tax reduction line now deduct legacy_means_test_deducted_income_tax, the measure Pension Credit already used: the income tax before the winter fuel payment charge, less the tax that rent, interest and dividends, and the other Step 7 charges, would bear if nobody had employed earnings or self-employment income. Earnings therefore carry the extra tax they cause in every test. pension_credit_uncounted_income_tax_without_earnings becomes legacy_means_test_uncounted_income_tax_without_earnings, now read by all four tests. legacy_means_test_income_tax stays as the Step 4 split read on the branch without earnings. Main's savings-credit-exit tests use a let property of 4,400, so council tax reduction capital (15,960) stays within the limit and the award comparison stays live. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GU2xd3vfduwbp9kHh --- .../legacy-capital-derived-income.fixed.md | 2 +- .../income_from_capital.yaml | 141 ++++++-- .../income_tax_caused_by_earnings.yaml | 205 +++++++++++ .../pension_credit_earnings_disregard.yaml | 6 +- .../property_income_after_finance_costs.yaml | 20 +- ...tax_reduction_pension_credit_properties.py | 12 +- ...ans_test_income_from_capital_properties.py | 336 ++++++++++++++---- ...on_credit_earnings_disregard_properties.py | 2 +- ..._savings_credit_only_receipt_properties.py | 57 ++- .../tests/test_trading_loss_properties.py | 14 +- .../housing_benefit_applicable_income.py | 5 +- .../dwp/income_support_applicable_income.py | 2 +- .../legacy_means_test_deducted_income_tax.py | 46 +++ .../gov/dwp/legacy_means_test_income_tax.py | 75 ++-- ...t_uncounted_income_tax_without_earnings.py | 58 +++ .../pension_credit_earnings_income_tax.py | 47 --- .../pension_credit/pension_credit_income.py | 24 +- .../uc_non_dep_receives_pension_credit.py | 4 +- ...council_tax_reduction_applicable_income.py | 2 +- 19 files changed, 819 insertions(+), 239 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml create mode 100644 policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py create mode 100644 policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py diff --git a/changelog.d/legacy-capital-derived-income.fixed.md b/changelog.d/legacy-capital-derived-income.fixed.md index ef4b8e19f4..2cfe6e50cd 100644 --- a/changelog.d/legacy-capital-derived-income.fixed.md +++ b/changelog.d/legacy-capital-derived-income.fixed.md @@ -1 +1 @@ -Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard, except where a working-age council tax reduction applicant's income is the Universal Credit assessment. The residential finance-cost tax reduction now comes off the tax on property income. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale and less any debt secured on it; adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland); uses the Universal Credit assessment of capital for a working-age applicant with an award of Universal Credit, including one the benefit cap or deductions reduce to nil, where the scheme says so; and disregards the whole capital of an applicant on Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance in Wales, in Scotland before April 2022, and in the Kingston, Merton, Westminster and Oxford schemes. In Pension Credit, the tax that earnings add on interest and dividends stays part of the tax on earnings, which is measured as the extra tax the earnings cause. +Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard, except where a working-age council tax reduction applicant's income is the Universal Credit assessment. The residential finance-cost tax reduction now comes off the tax on property income. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale and less any debt secured on it; adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland); uses the Universal Credit assessment of capital for a working-age applicant with an award of Universal Credit, including one the benefit cap or deductions reduce to nil, where the scheme says so; and disregards the whole capital of an applicant on Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance in Wales, in Scotland before April 2022, and in the Kingston, Merton, Westminster and Oxford schemes. All four tests deduct the same income tax: the tax before the winter fuel payment charge, less the tax that rent, interest and dividends, and any Step 7 charge, would bear if nobody had earnings, so earnings carry the extra tax they cause. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 3ef514b03c..70f0cdb437 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -15,13 +15,22 @@ # s.783BH); personal savings allowance £1,000 for a basic-rate taxpayer; # dividend allowance £500; dividend ordinary rate 8.75% in 2025-26 and 10.75% # in 2026-27 (the model's rate); employee NI 8% above £12,570. +# +# The tax deducted (legacy_means_test_deducted_income_tax) is the income tax +# before the winter fuel payment charge, less the tax the rent, interest and +# dividends, and any Step 7 charge, would bear if nobody had earnings: earnings +# carry the extra tax they cause, including what they add on income from +# capital by using up the personal allowance or a band. - name: Income Support, lone parent with earnings and rent from a let property # Earnings 10,000 use 10,000 of the personal allowance, so the remaining # 2,570 goes against taxable rent of 5,200 - 1,000 = 4,200. Tax on rent: - # (4,200 - 2,570) x 20% = 326. Tax on earnings and NI: nil. + # (4,200 - 2,570) x 20% = 326. NI: nil. Without the earnings the rent is + # within the personal allowance and bears no tax, so the whole 326 is tax + # the earnings cause, and it is deducted. # Main counted the rent and deducted its tax: 10,000 + 5,200 - 326 - 1,040. - # The law counts neither: 10,000 - 1,040 (the £20 lone parent disregard). + # This counts the earnings net of the tax they cause, not the rent: + # 10,000 - 326 - 1,040 (the £20 lone parent disregard) = 8,634. period: 2025 absolute_error_margin: 0.01 input: @@ -44,15 +53,20 @@ income_tax: [326, 0] property_income_tax: [326, 0] legacy_means_test_income_tax: [0, 0] - income_support_applicable_income: 8_960 + legacy_means_test_uncounted_income_tax_without_earnings: [0, 0] + legacy_means_test_deducted_income_tax: [326, 0] + income_support_applicable_income: 8_634 -- name: Working-age Housing Benefit counts earnings net of the tax on them only +- name: Working-age Housing Benefit counts earnings net of the tax they cause, not the rent, interest or dividends # Earnings 14,000: tax (14,000 - 12,570) x 20% = 286; NI (14,000 - 12,570) # x 8% = 114.40. Rent 6,000: tax (6,000 - 1,000) x 20% = 1,000. Interest 500 # is within the personal savings allowance. Dividends 1,000: (1,000 - 500) x - # 8.75% = 43.75. Income tax 1,329.75, of which 286 is on counted income. - # Applicable income: 14,000 - 286 - 114.40 - 260 (the £5 weekly disregard, - # set as an input here) = 13,339.60. + # 8.75% = 43.75. Income tax 1,329.75, of which 286 is the Step 4 tax on + # counted income. Without the earnings, rent 5,000, interest 500 and + # dividends 1,000 total 6,500, within the personal allowance: no tax. So + # all 1,329.75 is tax the earnings cause, and it is deducted. + # Applicable income: 14,000 - 1,329.75 - 114.40 - 260 (the £5 weekly + # disregard, set as an input here) = 12,295.85. period: 2025 absolute_error_margin: 0.01 input: @@ -77,8 +91,10 @@ dividend_income_tax: 43.75 savings_income_tax: 0 legacy_means_test_income_tax: 286 + legacy_means_test_uncounted_income_tax_without_earnings: 0 + legacy_means_test_deducted_income_tax: 1_329.75 national_insurance: 114.40 - housing_benefit_applicable_income: 13_339.60 + housing_benefit_applicable_income: 12_295.85 - name: Pension-age Housing Benefit counts rent from part of the home less £20 a week, not rent from other property # State Pension 12,000 and private pension 2,000 use the personal allowance @@ -208,7 +224,7 @@ output: income_tax_before_winter_fuel_payment_charge: 4_899.75 pension_credit_employment_income_tax: 2_368.375 - pension_credit_uncounted_income_tax_without_earnings: 2_531.375 + legacy_means_test_uncounted_income_tax_without_earnings: 2_531.375 pension_credit_earnings_disregard: 260 pension_credit_income: 19_371.625 @@ -243,7 +259,10 @@ # SSI 2021/249 reg 57(1) lists unearned income in full; it includes the # assumed yield from capital (reg 63) but no head for actual rent or rent # from part of the home. Earnings 12,000 are below the personal allowance - # and the NI threshold, so applicable income is 12,000. + # and the NI threshold, but they use 12,000 of the allowance, leaving 570 + # against the rent of 3,000: (3,000 - 570) x 19% (the Scottish starter + # rate) = 461.70. Without the earnings the rent bears no tax, so the 461.70 + # is tax the earnings cause and is deducted: 12,000 - 461.70 = 11,538.30. period: 2026 absolute_error_margin: 0.01 input: @@ -262,12 +281,15 @@ region: SCOTLAND output: legacy_means_test_income_tax: 0 - council_tax_reduction_applicable_income: 12_000 + legacy_means_test_deducted_income_tax: 461.70 + council_tax_reduction_applicable_income: 11_538.30 - name: Welsh working-age council tax reduction counts sub-tenant rent less £20, not other rent # WSI 2013/3029 Sch 6 para 27(5) treats income derived from capital as - # capital; Sch 9 para 26 disregards £20 a week of sub-tenant rent: - # 12,000 + (50 - 20) x 52 = 13,560. + # capital; Sch 9 para 26 disregards £20 a week of sub-tenant rent. The + # earnings use 12,000 of the personal allowance, so the rent of 3,000 bears + # (3,000 - 570) x 20% = 486, all caused by the earnings and deducted: + # 12,000 + (50 - 20) x 52 - 486 = 13,074. period: 2026 absolute_error_margin: 0.01 input: @@ -286,7 +308,8 @@ region: WALES output: legacy_means_test_income_tax: 0 - council_tax_reduction_applicable_income: 13_560 + legacy_means_test_deducted_income_tax: 486 + council_tax_reduction_applicable_income: 13_074 - name: Sub-tenant rent below £20 a week is disregarded in full # 1,000 / 52 = 19.23 a week. @@ -383,8 +406,11 @@ # shares it in proportion to the Step 4 tax (a stated convention: a credit # for foreign tax on the rent would relieve only the rent, TIOPA 2010 # s.18(2)): the rent's share is 1,000 x 1,000 / 1,286 = 777.60, leaving - # 222.40 of tax on rent. Income tax 286 less 222.40 = 63.60 is tax on the earnings, - # and HB applicable income is 14,000 - 63.60 - 114.40 (NI) - 260 = 13,562.00. + # 222.40 of tax on rent. Income tax 286 less 222.40 = 63.60 is the tax on + # counted income. Without the earnings the rent bears no tax, so all 286 is + # tax the earnings cause and is deducted: HB applicable income is + # 14,000 - 286 - 114.40 (NI) - 260 = 13,339.60. The sharing sets the tax + # deducted only without earnings (the next case). period: 2025 absolute_error_margin: 0.01 input: @@ -405,7 +431,41 @@ output: income_tax: 286 legacy_means_test_income_tax: 63.605 - housing_benefit_applicable_income: 13_561.995 + legacy_means_test_uncounted_income_tax_without_earnings: 0 + legacy_means_test_deducted_income_tax: 286 + housing_benefit_applicable_income: 13_339.60 + +- name: Without earnings, the tax reductions shared with income from capital set the tax deducted + # The same Step 4 and Step 6 figures with a private pension of 14,000 in + # place of the earnings: 286 on the pension, 1,000 on the rent, a 1,000 + # reduction shared pro rata, income tax 286. Nobody has earnings, so the + # tax on uncounted income is the 222.40 left on the rent, and the tax + # deducted is 286 - 222.40 = 63.60, the tax on counted income. A pension + # bears no NI. HB applicable income: 14,000 - 63.60 - 260 (the £5 weekly + # disregard, set as an input) = 13,676.40. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + private_pension_income: 14_000 + property_income: 6_000 + property_rental_income: 6_000 + other_tax_credits: 1_000 + benunits: + benunit: + members: [person] + housing_benefit_applicable_income_disregard: 260 + households: + household: + members: [person] + output: + income_tax: 286 + legacy_means_test_income_tax: 63.605 + legacy_means_test_uncounted_income_tax_without_earnings: 222.395 + legacy_means_test_deducted_income_tax: 63.605 + housing_benefit_applicable_income: 13_676.395 - name: Step 7 charges are not tax on counted income # The High Income Child Benefit Charge is added at Step 7 of ITA 2007 s.23 @@ -502,11 +562,13 @@ - name: Working-age Housing Benefit award with rent from a let property # A continuing award (2025-26). Earnings 10,000 use 10,000 of the personal # allowance; taxable rent 5,000 - 1,000 = 4,000, of which 4,000 - 2,570 = - # 1,430 is taxed: 286, all on rent, so none is deducted. NI is nil below - # 12,570. Applicable income: 10,000 - 260 (the £5 disregard, as an input) = - # 9,740. With the single 25-or-over personal allowance of £92.05 a week - # (4,786.60, an input here), HB = 6,000 - 65% x (9,740 - 4,786.60) = - # 6,000 - 3,219.71 = 2,780.29. Counting the rent would leave nothing. + # 1,430 is taxed: 286, all on rent. Without the earnings the rent bears no + # tax, so the 286 is tax the earnings cause and is deducted. NI is nil + # below 12,570. Applicable income: 10,000 - 286 - 260 (the £5 disregard, as + # an input) = 9,454. With the single 25-or-over personal allowance of + # £92.05 a week (4,786.60, an input here), HB = 6,000 - 65% x (9,454 - + # 4,786.60) = 6,000 - 3,033.81 = 2,966.19. Counting the rent would leave + # nothing. period: 2025 absolute_error_margin: 0.01 input: @@ -532,8 +594,9 @@ output: property_income_tax: 286 legacy_means_test_income_tax: 0 - housing_benefit_applicable_income: 9_740 - housing_benefit: 2_780.29 + legacy_means_test_deducted_income_tax: 286 + housing_benefit_applicable_income: 9_454 + housing_benefit: 2_966.19 - name: An English pensioner with a let property worth more than £16,000 gets no council tax reduction # The rent no longer counts as income, but the property counts as capital: @@ -568,8 +631,10 @@ # WSI 2013/3029 reg 30: no reduction where capital exceeds 16,000. The # property is valued less 10% for the expenses of sale (Sch 6 para 28(a)): # 45,000. Tariff income (Sch 6 para 33) counts capital up to 16,000: - # (16,000 - 6,000) / 250 = 40 a week, 2,080 a year, so income is 9,000 + - # 2,080 = 11,080. + # (16,000 - 6,000) / 250 = 40 a week, 2,080 a year. The earnings use 9,000 + # of the personal allowance, so the rent of 5,200 bears (14,200 - 12,570) + # x 20% = 326, all caused by the earnings and deducted. Income is 9,000 - + # 326 + 2,080 = 10,754. period: 2026 absolute_error_margin: 0.01 input: @@ -595,7 +660,7 @@ output: council_tax_reduction_assessable_capital: 45_000 council_tax_reduction_tariff_income: 2_080 - council_tax_reduction_applicable_income: 11_080 + council_tax_reduction_applicable_income: 10_754 council_tax_reduction: 0 - name: A Scottish mixed-age couple on Universal Credit is under the working-age scheme @@ -673,8 +738,11 @@ # its value (WSI 2013/3029 Sch 6 para 28(a)): 9,000. Sch 6 para 33 treats it # as a weekly income of 1 for each 250, or part of 250, over 6,000: 12 a # week, 624 a year. Earnings 9,000 are below the personal allowance and the - # NI threshold. Income 9,000 + 624 = 9,624. With the applicable amount - # supplied as 6,000, CTR = 1,500 - 20% x (9,624 - 6,000) = 775.20. + # NI threshold, but they leave 3,570 of the allowance for the rent of + # 5,200, which bears (5,200 - 3,570) x 20% = 326. Without the earnings the + # rent bears no tax, so the 326 is deducted. Income 9,000 - 326 + 624 = + # 9,298. With the applicable amount supplied as 6,000, CTR = 1,500 - 20% x + # (9,298 - 6,000) = 840.40. period: 2026 absolute_error_margin: 0.01 input: @@ -701,14 +769,18 @@ output: council_tax_reduction_assessable_capital: 9_000 council_tax_reduction_tariff_income: 624 - council_tax_reduction_applicable_income: 9_624 - council_tax_reduction: 775.20 + legacy_means_test_deducted_income_tax: 326 + council_tax_reduction_applicable_income: 9_298 + council_tax_reduction: 840.40 - name: Scottish working-age assumed yield on a let property below the capital limit # SSI 2021/249 reg 70(1)(a) values the property less 10%: 9,000. Reg # 63(1)(b): a weekly income of 1 for each 250 over 6,000 and 1 for any part # of 250: 12 a week, 624 a year; reg 63(3) treats the actual rent as - # capital. CTR = 1,500 - 20% x (9,624 - 6,000) = 775.20. + # capital. The earnings leave 3,570 of the personal allowance for the rent + # of 5,200, which bears (5,200 - 3,570) x 19% (the Scottish starter rate) = + # 309.70, all caused by the earnings and deducted. Income 9,000 - 309.70 + + # 624 = 9,314.30. CTR = 1,500 - 20% x (9,314.30 - 6,000) = 837.14. period: 2026 absolute_error_margin: 0.01 input: @@ -735,8 +807,9 @@ output: council_tax_reduction_assessable_capital: 9_000 council_tax_reduction_tariff_income: 624 - council_tax_reduction_applicable_income: 9_624 - council_tax_reduction: 775.20 + legacy_means_test_deducted_income_tax: 309.70 + council_tax_reduction_applicable_income: 9_314.30 + council_tax_reduction: 837.14 - name: An English pensioner's let property valued less sale expenses stays within the limit # A 17,000 property is valued at 15,300 (SI 2012/2885 Sch 1 para 32(a)), so diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml new file mode 100644 index 0000000000..dc28bfc670 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml @@ -0,0 +1,205 @@ +# The income tax Income Support, Housing Benefit, Pension Credit and council +# tax reduction deduct (legacy_means_test_deducted_income_tax): the income tax +# before the winter fuel payment charge, less the tax that rent, interest and +# dividends, and any Step 7 charge, would bear if nobody had employed earnings +# or self-employment income. The tests treat income derived from capital as +# capital (IS Regs 1987 reg 48(4) and Sch 9 para 22; HB Regs 2006 reg 46(4) +# and Sch 5 para 17) and disregard tax only on income they take into account +# (IS Sch 9 para 1; HB Sch 5 para 1); earnings are net of the income tax +# deducted from them (IS reg 36(3)(a); HB reg 36(3)(a)). Earnings carry the +# extra tax they cause. +# +# 2026-27 figures: personal allowance £12,570; basic rate 20% to £37,700 of +# taxable income, higher rate 40%; starting rate for savings 0% on £5,000, +# less taxable non-savings income; personal savings allowance £1,000 for a +# basic-rate and £500 for a higher-rate taxpayer; employee NI 8% from £12,570 +# to £50,270. Each person is 40 and lives in England unless stated. + +- name: Pay of 30,000 and interest of 60,000, the tests deduct the tax the pay adds on the interest + # With the pay: non-savings taxable income 30,000 - 12,570 = 17,430, tax + # 3,486. Taxable income 77,430 exceeds 37,700, so the savings allowance is + # 500 and the starting rate is lost (17,430 > 5,000). Interest: 500 at nil + # in the band, 37,700 - 17,430 - 500 = 19,770 at 20% = 3,954, and 60,000 - + # 20,270 = 39,730 at 40% = 15,892. Income tax 3,486 + 19,846 = 23,332. + # Without the pay: taxable interest 60,000 - 12,570 = 47,430; 5,000 at the + # starting rate and 500 savings allowance at nil, 37,700 - 5,500 = 32,200 at + # 20% = 6,440, 47,430 - 37,700 = 9,730 at 40% = 3,892: 10,332, none of it on + # counted income. Tax deducted: 23,332 - 10,332 = 13,000, against the Step 4 + # tax on the pay of 3,486. + # NI: (30,000 - 12,570) x 8% = 1,394.40. + # Income Support: 30,000 - 13,000 - 1,394.40 - 260 (the £5 single + # disregard, IS Sch 8) = 15,345.60. Housing Benefit, with its £5 disregard + # supplied as an input: the same. The council tax reduction general rule, + # with no working-age earnings disregard in the model's English default: + # 30,000 - 13,000 - 1,394.40 = 15,605.60. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 30_000 + savings_interest_income: 60_000 + benunits: + benunit: + members: [person] + housing_benefit_applicable_income_disregard: 260 + households: + household: + members: [person] + region: NORTH_EAST + output: + income_tax_before_winter_fuel_payment_charge: 23_332 + legacy_means_test_income_tax: 3_486 + legacy_means_test_uncounted_income_tax_without_earnings: 10_332 + legacy_means_test_deducted_income_tax: 13_000 + national_insurance: 1_394.40 + income_support_applicable_income: 15_345.60 + housing_benefit_applicable_income: 15_345.60 + council_tax_reduction_applicable_income: 15_605.60 + +- name: Pay and rent with mortgage interest, the tests deduct the rent's tax the pay causes, net of the finance-cost reduction + # Pay 20,000 and a property profit of 10,000 with 4,000 of mortgage + # interest. Non-savings taxable income 30,000 - 12,570 = 17,430; the pay + # takes 7,430 of it (tax 1,486) and the rent 10,000 (tax 2,000). The + # residential finance-cost reduction is 20% x 4,000 = 800 (ITTOIA 2005 + # s.274A), so income tax is 3,486 - 800 = 2,686, and the Step 4 tax on + # counted income is 1,486. Without the pay the rent of 10,000 is within the + # personal allowance and bears no tax, so the whole 2,686 is tax the pay + # causes and is deducted: the 1,200 of tax on rent that the reduction leaves. + # NI: (20,000 - 12,570) x 8% = 594.40. + # Income Support: 20,000 - 2,686 - 594.40 - 260 = 16,459.60. Housing + # Benefit, with its £5 disregard as an input: the same. Council tax + # reduction general rule: 20,000 - 2,686 - 594.40 = 16,719.60. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 20_000 + property_income: 10_000 + property_finance_costs: 4_000 + benunits: + benunit: + members: [person] + housing_benefit_applicable_income_disregard: 260 + households: + household: + members: [person] + region: NORTH_EAST + output: + property_income_tax: 2_000 + property_finance_cost_relief: 800 + income_tax_before_winter_fuel_payment_charge: 2_686 + legacy_means_test_income_tax: 1_486 + legacy_means_test_uncounted_income_tax_without_earnings: 0 + legacy_means_test_deducted_income_tax: 2_686 + income_support_applicable_income: 16_459.60 + housing_benefit_applicable_income: 16_459.60 + council_tax_reduction_applicable_income: 16_719.60 + +- name: The High Income Child Benefit Charge the pay brings in is deducted + # A parent aged 35 with one child, pay 40,000 and interest 30,000. Child + # Benefit 27.05 x 52 = 1,406.60 (1,406 in whole pounds). Adjusted net income + # 70,000 is half-way through the 60,000 to 80,000 taper: charge 50% x 1,406 + # = 703 (ITEPA 2003 s.681C). Pay: (40,000 - 12,570) x 20% = 5,486. Interest, + # higher-rate (savings allowance 500): 500 at nil, 37,700 - 27,430 - 500 = + # 9,770 at 20% = 1,954, 30,000 - 10,270 = 19,730 at 40% = 7,892; 9,846. + # Income tax before the winter fuel payment charge: 5,486 + 9,846 + 703 = + # 16,035. Without the pay: adjusted net income 30,000, no charge; interest + # 17,430 taxable, 5,000 at the starting rate and 1,000 savings allowance at + # nil, 11,430 at 20% = 2,286. Tax deducted: 16,035 - 2,286 = 13,749, which + # includes the whole 703 charge. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 40_000 + savings_interest_income: 30_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + region: NORTH_EAST + output: + CB_HITC: [703, 0] + income_tax_before_winter_fuel_payment_charge: [16_035, 0] + legacy_means_test_income_tax: [5_486, 0] + legacy_means_test_uncounted_income_tax_without_earnings: [2_286, 0] + legacy_means_test_deducted_income_tax: [13_749, 0] + +- name: Only the part of the High Income Child Benefit Charge that the pay adds is deducted + # A parent aged 35 with one child, pay 10,000 and interest 65,000. With the + # pay: adjusted net income 75,000, charge 75% x 1,406 = 1,054.50, 1,054 in + # whole pounds. The pay is within the personal allowance and leaves 2,570 of + # it; taxable interest 62,430, higher-rate: 5,000 starting rate and 500 + # savings allowance at nil, 32,200 at 20% = 6,440, 24,730 at 40% = 9,892; + # 16,332. Income tax 16,332 + 1,054 = 17,386. Without the pay: adjusted net + # income 65,000, charge 25% x 1,406 = 351.50, 351; taxable interest 52,430: + # 6,440 + 14,730 x 40% = 12,332; 12,683 in all. Tax deducted: 17,386 - + # 12,683 = 4,703: the 4,000 the pay adds on the interest and 703 of the + # charge. The 351 the parent would pay without the pay is not deducted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + employment_income: 10_000 + savings_interest_income: 65_000 + child: + age: 5 + benunits: + benunit: + members: [parent, child] + households: + household: + members: [parent, child] + region: NORTH_EAST + output: + CB_HITC: [1_054, 0] + income_tax_before_winter_fuel_payment_charge: [17_386, 0] + legacy_means_test_uncounted_income_tax_without_earnings: [12_683, 0] + legacy_means_test_deducted_income_tax: [4_703, 0] + +- name: Only the part of the annual allowance charge that the pay adds is deducted + # Pay 45,000, interest 2,000 and employer contributions of 70,000 to a + # money-purchase scheme: a chargeable amount of 70,000 - 60,000 = 10,000 + # (Finance Act 2004 s.227). Pay: (45,000 - 12,570) x 20% = 6,486; interest + # (basic-rate, savings allowance 1,000): 1,000 x 20% = 200. The charge is + # the chargeable amount as the top slice of reduced net income 47,000 - + # 12,570 = 34,430: 3,270 at 20% = 654 and 6,730 at 40% = 2,692; 3,346. + # Income tax 6,486 + 200 + 3,346 = 10,032. Without the pay: the interest is + # within the personal allowance, reduced net income is nil, and the charge + # is 10,000 x 20% = 2,000. Tax deducted: 10,032 - 2,000 = 8,032: the 6,686 + # on the pay and interest and the 1,346 of the charge the pay adds. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + employment_income: 45_000 + savings_interest_income: 2_000 + employer_pension_contributions: 70_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + region: NORTH_EAST + output: + pension_annual_allowance_chargeable_amount: 10_000 + personal_pension_contributions_tax: 3_346 + income_tax_before_winter_fuel_payment_charge: 10_032 + legacy_means_test_income_tax: 6_486 + legacy_means_test_uncounted_income_tax_without_earnings: 2_000 + legacy_means_test_deducted_income_tax: 8_032 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml index 772a78ea13..2131ed247c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -437,7 +437,7 @@ # 738 = 12,000. HMRC: /apply-tax-free-interest-on-savings/how-much-is-tax-free. income_tax: 1_388 pension_credit_employment_income_tax: 302 - pension_credit_uncounted_income_tax_without_earnings: 1_086 + legacy_means_test_uncounted_income_tax_without_earnings: 1_086 pension_credit_earnings_disregard: 738 pension_credit_income: 12_000 @@ -461,7 +461,7 @@ pension_credit_earnings_disregard: 0 # The 1,086 is all tax on the interest, which Pension Credit does not # count, so none of it is deducted: 12,000. - pension_credit_uncounted_income_tax_without_earnings: 1_086 + legacy_means_test_uncounted_income_tax_without_earnings: 1_086 pension_credit_income: 12_000 - name: Statutory sick pay leaves Pension Credit income unchanged when the tax on interest is uncounted, as wages do. @@ -494,7 +494,7 @@ output: income_tax_before_winter_fuel_payment_charge: 1_388 pension_credit_employment_income_tax: 302 - pension_credit_uncounted_income_tax_without_earnings: 1_086 + legacy_means_test_uncounted_income_tax_without_earnings: 1_086 pension_credit_earnings_disregard: 738 pension_credit_income: 12_000 diff --git a/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml b/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml index d232d83771..33c8f7df91 100644 --- a/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml +++ b/policyengine_uk/tests/policy/baseline/household/income/property_income_after_finance_costs.yaml @@ -58,10 +58,14 @@ output: # (16,000 + 12,000 + 6,000 - 12,570) x 20%. income_tax: 4_286 - # The rent does not count, nor its tax: the tax on the earnings and - # pension is (28,000 - 12,570) x 20% = 3,086, so 16,000 + 12,000 - 3,086, - # less the 5 a week earnings disregard. - housing_benefit_applicable_income: 24_654 + # The rent does not count, nor the tax it would bear without the + # earnings: (12,000 + 6,000 - 12,570) x 20% = 1,086. The earnings carry + # the rest, 4,286 - 1,086 = 3,200: the 3,086 on the earnings and pension + # and the 114 they add on the rent by using the personal allowance. So + # 16,000 + 12,000 - 3,200, less the 5 a week earnings disregard. + legacy_means_test_uncounted_income_tax_without_earnings: 1_086 + legacy_means_test_deducted_income_tax: 3_200 + housing_benefit_applicable_income: 24_540 - name: Housing Benefit, finance costs equal to the profit do not lower it period: 2026 @@ -78,7 +82,9 @@ # 4,286 less the 20% x 6,000 reduction. income_tax: 3_086 property_income_after_finance_costs: 0 - # 16,000 + 12,000 - 3,086 - 260: the rent does not count, and the 1,200 - # reduction comes off the tax on the rent, so the tax on the earnings and - # pension deducted is still 3,086. + # 16,000 + 12,000 - 3,086 - 260: the rent does not count. Without the + # earnings the 1,200 reduction clears the 1,086 of tax on the rent, so no + # tax is left on uncounted income and the whole 3,086 is deducted. + legacy_means_test_uncounted_income_tax_without_earnings: 0 + legacy_means_test_deducted_income_tax: 3_086 housing_benefit_applicable_income: 24_654 diff --git a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py index 25529a52ac..b87444776e 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py @@ -304,12 +304,12 @@ def situation( "esa_income", "universal_credit", ] -# The means tests deduct only the income tax on the income they take into -# account (legacy_means_test_income_tax), which is measured before the -# winter fuel payment charge (ITEPA 2003 s.681I): the charge recovers a -# payment rather than taxing the income they assess. A pensioner over -# £35,000 of total income is charged it. -MAIN_DEDUCTIONS = ["legacy_means_test_income_tax", "national_insurance"] +# The means tests deduct the income tax before the winter fuel payment charge +# (ITEPA 2003 s.681I), less the tax on income they do not take into account +# due without earnings (legacy_means_test_deducted_income_tax): the charge +# recovers a payment rather than taxing the income they assess. A pensioner +# over £35,000 of total income is charged it. +MAIN_DEDUCTIONS = ["legacy_means_test_deducted_income_tax", "national_insurance"] # The pensioner earnings disregards in 2026, written out from the law, not # read from the parameters: £25 a week for a lone parent, £10 with a partner, # £5 otherwise, plus £17.10 (SI 2012/2885 Sch 4 paras 2, 8 and 10; WSI diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index c2188fac00..ba5f4981a6 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -9,35 +9,50 @@ Invariants, for any generated population of families: -1. Invariance: scaling property, savings interest and dividend income (by 0 - and 3) changes none of the four means-test incomes, and none of - Income Support, Housing Benefit, Pension Credit or council tax reduction. - Universal Credit and tax credits, which have their own income rules, are - held fixed. Incomes are kept within the basic rate band, where extra - income from capital cannot change the tax on other income through the - personal allowance taper or the High Income Child Benefit Charge. +1. Invariance: where nobody in the population has employed earnings or + self-employment income, scaling property, savings interest and dividend + income (by 0 and 3) changes none of the four means-test incomes, and none + of Income Support, Housing Benefit, Pension Credit or council tax + reduction. Universal Credit and tax credits, which have their own income + rules, are held fixed. Incomes are kept within the basic rate band, where + extra income from capital cannot change the tax on other income through + the personal allowance taper or the High Income Child Benefit Charge. Marriage Allowance is held at nil: whether a spouse can transfer it depends on their income including income from capital, which is a legitimate way for income from capital to change the tax on counted income. - Intended exception: Pension Credit's tax on earnings is the extra tax the - earnings cause, including on savings and dividends (#2019, Max's ruling - d1424). With earnings, income from capital can move that tax: earnings - under the personal allowance can leave dividends taxable. So Pension - Credit income is invariant wherever pension_credit_employment_income_tax - does not move, and elsewhere changes by exactly minus the change in that - tax and in the earnings disregard. The awards that follow Pension Credit - (Pension Credit, and Housing Benefit and council tax reduction for a - family receiving it) are compared wherever it does not move. + Intended exception: earnings carry the extra tax they cause, including on + savings, dividends and rent (d1424, applied to all four tests). With + earnings, income from capital can move that tax: earnings under the + personal allowance can leave rent or dividends taxable. Where someone has + earnings, each means-test income changes by exactly minus the change in + the tax that test deducts (legacy_means_test_deducted_income_tax, summed + over the members it counts) and in its earnings disregard, which is + capped at net earnings, unless floored or on a route that does not use + the test's own income rules. The awards are compared wherever none of + those taxes or disregards moves. 2. Bounds: the counted home-letting income is between nil and the rent, and equals the rent less £20 a week, floored at nil. 3. Monotone: more rent from part of the home never lowers a means-test income and never raises Income Support, Housing Benefit, Pension Credit or council tax reduction. -4. Tax: legacy_means_test_income_tax is between nil and both income tax and - the tax on counted income before reductions (earned_income_tax), and - equals income tax when there is no savings, dividend or property income - and no Step 7 charge. +4. Tax: legacy_means_test_income_tax, the Step 4 split the tests read + without earnings, is between nil and both income tax and the tax on + counted income before reductions (earned_income_tax), and equals income + tax when there is no savings, dividend or property income and no Step 7 + charge. The tax each test deducts (legacy_means_test_deducted_income_tax) + is between nil and the income tax before the winter fuel payment charge; + it equals that tax when nobody has savings, dividend or property income + and no Step 7 charge applies; and without earnings it equals + legacy_means_test_income_tax, except where the nil floor on the tax on + uncounted income binds. Within the basic rate band it is never below + legacy_means_test_income_tax: earnings never lower the tax on income from + capital there. +6. Differential: for every generated family without self-employment income, + Income Support, Housing Benefit and the council tax reduction line deduct, + for each person they count, the same income tax as Pension Credit's tax + line: the pre-charge income tax less + legacy_means_test_uncounted_income_tax_without_earnings. 5. Tariff: the council tax reduction tariff income that replaces the actual income from capital is never negative, never more than the yield at the 16,000 capital limit (40 steps of 250 at 4.35 a month, 2,088 a year), nil @@ -172,56 +187,104 @@ def calculate(units, year, variables, **kwargs): return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in variables} +# The members whose income and tax each test counts. +TEST_MEMBERS = { + "income_support_applicable_income": "is_claimant_or_partner", + "housing_benefit_applicable_income": "is_claimant_or_partner", + "pension_credit_income": "is_claimant_or_partner", + "council_tax_reduction_applicable_income": ( + "is_council_tax_reduction_applicant_or_partner" + ), +} +# Each test's earnings disregard, which is capped at net earnings, so it moves +# with the tax on them. +EARNINGS_DISREGARDS = { + "income_support_applicable_income": "income_support_earnings_disregard", + "housing_benefit_applicable_income": "housing_benefit_applicable_income_disregard", + "pension_credit_income": "pension_credit_earnings_disregard", + "council_tax_reduction_applicable_income": ( + "council_tax_reduction_pensioner_earnings_disregard" + ), +} +EARNINGS = ["employment_income", "self_employment_income"] + + +def calculate_with_taxes(units, year, variables, **kwargs): + """Benefit-unit values of ``variables``, each test's deducted tax summed + over the members it counts (keyed ``tax:``), and whether anyone in + the unit has earnings (``earners``).""" + sim = Simulation(situation=situation(units, year, **kwargs)) + out = {v: np.asarray(sim.calculate(v, year), dtype=float) for v in variables} + tax = np.asarray( + sim.calculate("legacy_means_test_deducted_income_tax", year), dtype=float + ) + for income, members in TEST_MEMBERS.items(): + mask = np.asarray(sim.calculate(members, year), dtype=float) + out[f"tax:{income}"] = sim.map_result(tax * mask, "person", "benunit") + earnings = sum( + np.abs(np.asarray(sim.calculate(v, year), dtype=float)) for v in EARNINGS + ) + out["earners"] = sim.map_result(earnings, "person", "benunit") > 0 + return out + + @PROPERTY_SETTINGS @given(populations, st.sampled_from(YEARS)) def test_income_from_capital_changes_no_legacy_means_test(units, year): - earnings_tax = [ - "pension_credit_employment_income_tax", - "pension_credit_earnings_disregard", + disregards = sorted(set(EARNINGS_DISREGARDS.values())) + routes = [ + "in_receipt_of_pension_credit", + "council_tax_reduction_universal_credit_award", + "housing_benefit_on_passporting_benefit", ] - variables = MEANS_TEST_INCOMES + AWARDS + earnings_tax - base = calculate(units, year, variables) + variables = MEANS_TEST_INCOMES + AWARDS + disregards + routes + base = calculate_with_taxes(units, year, variables) for scale in (0.0, 3.0): - scaled = calculate(units, year, variables, capital_income_scale=scale) - moved = {v: scaled[v] - base[v] for v in earnings_tax} - # Intended: income from capital reaches Pension Credit only through - # the tax the earnings cause on it (#2019, d1424). - steady = np.abs(moved["pension_credit_employment_income_tax"]) < 0.01 - on_pension_credit = (base["pension_credit"] > 0) | ( - scaled["pension_credit"] > 0 + scaled = calculate_with_taxes( + units, year, variables, capital_income_scale=scale ) - follows_pension_credit = { - "pension_credit_income": ~steady, - "pension_credit": ~steady, - "housing_benefit": ~steady & on_pension_credit, - "housing_benefit_applicable_income": ~steady & on_pension_credit, - "council_tax_benefit": ~steady & on_pension_credit, - "council_tax_reduction_applicable_income": ~steady & on_pension_credit, + moved = { + income: (scaled[f"tax:{income}"] - base[f"tax:{income}"]) + + (scaled[EARNINGS_DISREGARDS[income]] - base[EARNINGS_DISREGARDS[income]]) + for income in MEANS_TEST_INCOMES } + steady = np.all( + [np.abs(moved[income]) < 0.01 for income in MEANS_TEST_INCOMES], axis=0 + ) + # Without earnings, the tax each test deducts does not move. + assert np.all(steady[~base["earners"]]) + # Where the deducted tax is steady, nothing moves. for v in MEANS_TEST_INCOMES + AWARDS: - compared = ~follows_pension_credit.get(v, np.zeros_like(steady)) np.testing.assert_allclose( - scaled[v][compared], - base[v][compared], + scaled[v][steady], + base[v][steady], atol=0.01, err_msg=f"{v} at scale {scale}", ) - # Where the tax on earnings moves, Pension Credit income moves by - # exactly minus that change and the disregard's, unless floored. - unfloored = (base["pension_credit_income"] > 0) & ( - scaled["pension_credit_income"] > 0 - ) - np.testing.assert_allclose( - (scaled["pension_credit_income"] - base["pension_credit_income"])[ - unfloored - ], - -( - moved["pension_credit_employment_income_tax"] - + moved["pension_credit_earnings_disregard"] - )[unfloored], - atol=0.01, - err_msg=f"pension_credit_income decomposition at scale {scale}", + # Intended exception (d1424): where earnings make it move, each + # means-test income moves by exactly minus that change, wherever + # neither run is floored and the test uses its own income rules. + pension_credit_route = (base["in_receipt_of_pension_credit"] > 0) | ( + scaled["in_receipt_of_pension_credit"] > 0 ) + own_rules = { + "income_support_applicable_income": np.ones_like(steady), + "pension_credit_income": np.ones_like(steady), + "housing_benefit_applicable_income": ~pension_credit_route + & (base["housing_benefit_on_passporting_benefit"] == 0), + "council_tax_reduction_applicable_income": ~pension_credit_route + & (base["council_tax_reduction_universal_credit_award"] == 0), + } + for income in MEANS_TEST_INCOMES: + checked = ( + ~steady & own_rules[income] & (base[income] > 0) & (scaled[income] > 0) + ) + np.testing.assert_allclose( + (scaled[income] - base[income])[checked], + -moved[income][checked], + atol=0.01, + err_msg=f"{income} decomposition at scale {scale}", + ) @PROPERTY_SETTINGS @@ -256,7 +319,11 @@ def test_home_letting_rent_is_monotone(units, year, extra): def test_legacy_means_test_income_tax_bounds(units, year): variables = [ "legacy_means_test_income_tax", + "legacy_means_test_deducted_income_tax", + "legacy_means_test_uncounted_income_tax_without_earnings", "income_tax", + "income_tax_before_winter_fuel_payment_charge", + "income_tax_charges", "earned_income_tax", "savings_income_tax", "dividend_income_tax", @@ -271,15 +338,160 @@ def test_legacy_means_test_income_tax_bounds(units, year): v["savings_income_tax"] + v["dividend_income_tax"] + v["property_income_tax"] ) == 0 np.testing.assert_allclose(tax[no_capital_tax], v["income_tax"][no_capital_tax]) - without = calculate( - units, + + # The tax each test deducts. + deducted = v["legacy_means_test_deducted_income_tax"] + pre_charge = v["income_tax_before_winter_fuel_payment_charge"] + assert np.all(deducted >= -0.01) + assert np.all(deducted <= pre_charge + 0.01) + # Within the basic rate band, earnings never lower the tax on income + # from capital, so the deduction is never below the Step 4 split. + assert np.all(deducted >= tax - 0.01) + + # Nobody has savings, dividend or property income: it is the pre-charge + # tax wherever no Step 7 charge applies. + without_capital = calculate(units, year, variables, capital_income_scale=0.0) + no_charge = without_capital["income_tax_charges"] == 0 + np.testing.assert_allclose( + without_capital["legacy_means_test_deducted_income_tax"][no_charge], + without_capital["income_tax_before_winter_fuel_payment_charge"][no_charge], + atol=0.01, + ) + np.testing.assert_allclose( + without_capital["legacy_means_test_income_tax"], + without_capital["income_tax"], + atol=0.01, + ) + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS)) +def test_deducted_tax_without_earnings_is_the_step_4_split(units, year): + no_earnings = [ + dict( + unit, + members=[ + (role, dict(attrs, employment_income=0.0) if role == "adult" else attrs) + for role, attrs in unit["members"] + ], + ) + for unit in units + ] + v = calculate( + no_earnings, year, - ["income_tax", "legacy_means_test_income_tax"], - capital_income_scale=0.0, + [ + "legacy_means_test_income_tax", + "legacy_means_test_deducted_income_tax", + "income_tax_before_winter_fuel_payment_charge", + ], + ) + # Equal unless the nil floor on the tax on uncounted income binds, where + # the Step 4 split exceeds the pre-charge tax and the deduction is the + # pre-charge tax. + np.testing.assert_allclose( + v["legacy_means_test_deducted_income_tax"], + np.minimum( + v["legacy_means_test_income_tax"], + v["income_tax_before_winter_fuel_payment_charge"], + ), + atol=0.01, + ) + floor_binds = ( + v["legacy_means_test_income_tax"] + > v["income_tax_before_winter_fuel_payment_charge"] + 0.01 ) np.testing.assert_allclose( - without["legacy_means_test_income_tax"], without["income_tax"], atol=0.01 + v["legacy_means_test_deducted_income_tax"][~floor_binds], + v["legacy_means_test_income_tax"][~floor_binds], + atol=0.01, + ) + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS)) +def test_the_four_tests_deduct_the_same_income_tax(units, year): + """Income Support, Housing Benefit and the council tax reduction line + deduct, for each person they count, the income tax Pension Credit's tax + line deducts. Each test's income is recomputed with the person's income + tax raised by one pound and compared with the original, so the check + reads each formula rather than the shared variable. The families have no + self-employment income, where Pension Credit's notional tax applies.""" + sim = Simulation(situation=situation(units, year)) + people = sim.populations["person"].count + pension_credit_tax = np.asarray( + sim.calculate("income_tax_before_winter_fuel_payment_charge", year) + ) - np.asarray( + sim.calculate("legacy_means_test_uncounted_income_tax_without_earnings", year) + ) + np.testing.assert_allclose( + np.asarray(sim.calculate("legacy_means_test_deducted_income_tax", year)), + pension_credit_tax, + atol=0.01, + ) + # Each test's income responds one-for-one to the shared deduction for the + # members it counts, and not at all to the Step 4 split. + bumped = Simulation(situation=situation(units, year)) + bumped.set_input( + "legacy_means_test_deducted_income_tax", + year, + np.asarray(sim.calculate("legacy_means_test_deducted_income_tax", year)) + 1.0, + ) + split = Simulation(situation=situation(units, year)) + split.set_input( + "legacy_means_test_income_tax", + year, + np.asarray(sim.calculate("legacy_means_test_income_tax", year)) + 1.0, ) + split.set_input( + "legacy_means_test_deducted_income_tax", + year, + np.asarray(sim.calculate("legacy_means_test_deducted_income_tax", year)), + ) + assert people > 0 + for income, members in TEST_MEMBERS.items(): + base = np.asarray(sim.calculate(income, year), dtype=float) + counted = sim.map_result( + np.asarray(sim.calculate(members, year), dtype=float), "person", "benunit" + ) + moved = np.asarray(bumped.calculate(income, year), dtype=float) + unfloored = (base > 1) & (moved > 0) + own_rules = ( + np.asarray(sim.calculate("in_receipt_of_pension_credit", year)) == 0 + ) | (income == "pension_credit_income") + if income == "housing_benefit_applicable_income": + own_rules &= ( + np.asarray( + sim.calculate("housing_benefit_on_passporting_benefit", year) + ) + == 0 + ) + if income == "council_tax_reduction_applicable_income": + own_rules &= ( + np.asarray( + sim.calculate("council_tax_reduction_universal_credit_award", year) + ) + == 0 + ) + checked = unfloored & own_rules + # The earnings disregard is capped at net earnings, so it can fall + # with them. + disregard = EARNINGS_DISREGARDS[income] + disregard_change = np.asarray( + bumped.calculate(disregard, year), dtype=float + ) - np.asarray(sim.calculate(disregard, year), dtype=float) + np.testing.assert_allclose( + (base - moved)[checked], + (counted + disregard_change)[checked], + atol=0.01, + err_msg=income, + ) + np.testing.assert_allclose( + np.asarray(split.calculate(income, year), dtype=float), + base, + atol=0.01, + err_msg=f"{income} reads the Step 4 split", + ) MAXIMUM_TARIFF_INCOME = 40 * 4.35 * 12 diff --git a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py index 1f9311730d..db43f0d917 100644 --- a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py @@ -226,7 +226,7 @@ def test_statute_bounds_differential_and_monotonicity(case): no_earnings - person_values(no_earnings_sim, "legacy_means_test_income_tax"), ) assert np.allclose( - person_values(sim, "pension_credit_uncounted_income_tax_without_earnings"), + person_values(sim, "legacy_means_test_uncounted_income_tax_without_earnings"), uncounted_without_earnings, atol=0.01, ) diff --git a/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py b/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py index f64659e41d..7197148782 100644 --- a/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py +++ b/policyengine_uk/tests/test_savings_credit_only_receipt_properties.py @@ -185,7 +185,7 @@ def family(ages, **overrides): # recipient; and a working-age family. KNOWN_FAMILIES = [ family([90], state_pension=12_480.0), - family([90], state_pension=12_480.0, savings=12_000.0, other_property=5_000.0), + family([90], state_pension=12_480.0, savings=12_000.0, other_property=4_400.0), family([90], state_pension=12_480.0, savings=12_000.0, rent=6_000.0), family([90], state_pension=12_480.0, would_claim_pc=False), family([70], state_pension=9_000.0), @@ -394,10 +394,11 @@ def test_known_families_detect_main(): ) assert not np.any(fix[FLAG]) # Main keeps the Pension Credit assessment. The owner with capital is - # tested on 17,000, where the scheme's own rules count the savings of - # 12,000 and the property less 10% for the expenses of sale, 16,500. - # Both exceed the 16,000 limit, so neither gives a reduction. The - # tenant has no Housing Benefit tariff income, where + # tested on 16,400 (savings 12,000 and property 4,400) against the + # 16,000 limit and gets no council tax reduction, where the scheme's + # own rules count the property less 10% for the expenses of sale, + # 12,000 + 3,960 = 15,960, and give 2,000. The tenant has no Housing + # Benefit tariff income, where # Housing Benefit's own rules count 4 a week on savings of 12,000 # (208). Its Housing Benefit income is the same on both routes: with # no savings credit paid, the Pension Credit assessment is the State @@ -406,11 +407,15 @@ def test_known_families_detect_main(): FLAG, "council_tax_reduction_applicable_income", "council_tax_reduction_assessable_capital", + "simulated_council_tax_reduction_benunit", "housing_benefit_tariff_income", ] capital = "council_tax_reduction_assessable_capital" - assert main[capital][OWNER_WITH_CAPITAL] == 17_000 - assert fix[capital][OWNER_WITH_CAPITAL] == 16_500 + assert main[capital][OWNER_WITH_CAPITAL] == 16_400 + assert fix[capital][OWNER_WITH_CAPITAL] == 15_960 + reduction = "simulated_council_tax_reduction_benunit" + assert main[reduction][OWNER_WITH_CAPITAL] == 0 + assert abs(fix[reduction][OWNER_WITH_CAPITAL] - 2_000) < 0.01 assert main["housing_benefit_tariff_income"][TENANT] == 0 assert abs(fix["housing_benefit_tariff_income"][TENANT] - 208) < 0.01 assert assessed_differently(fix, not_claiming, known) == [] @@ -496,24 +501,40 @@ def read(simulation, variable): def test_stopping_pension_credit_payments_ends_the_pension_credit_capital_test(): - # Savings of 12,000 and other property worth 5,000. The Pension Credit - # assessment of capital counts both (17,000), above the 16,000 limit, so + # Savings of 12,000 and other property worth 4,400. The Pension Credit + # assessment of capital counts both (16,400), above the 16,000 limit, so # the savings-credit-only award leaves no council tax reduction. Deemed - # income is 1 a week for each 500 over 10,000: 14 a week, so the savings - # credit is 0.6 x 29.93 - 0.4 x (254.00 - 238.00) = 11.558 a week (601.02). - data = single_pensioner(savings=12_000.0, other_property=5_000.0) + # income is 1 a week for each 500, or part of 500, over 10,000: 6,400 / + # 500 = 12.8, so 13 a week (SPC Regs 2002 reg 15(6)). Income is 240.00 + + # 13.00 = 253.00 a week, so the savings credit is 0.6 x (238.00 - 208.07) + # - 0.4 x (253.00 - 238.00) = 17.958 - 6.00 = 11.958 a week (621.82). + data = single_pensioner(savings=12_000.0, other_property=4_400.0) unreformed = Simulation(situation=data) - assert abs(unreformed.calculate("savings_credit", YEAR)[0] - 601.02) < 0.01 + assert abs(unreformed.calculate("savings_credit", YEAR)[0] - 621.82) < 0.01 assert unreformed.calculate(FLAG, YEAR).tolist() == [True] capital = "council_tax_reduction_assessable_capital" - assert unreformed.calculate(capital, YEAR)[0] == 17_000 + assert unreformed.calculate(capital, YEAR)[0] == 16_400 assert unreformed.calculate("council_tax_reduction", YEAR)[0] == 0 # With Pension Credit not paid the scheme's own capital rule applies: the # savings and the property less 10% for the expenses of sale (SI - # 2012/2885 Sch 1 para 32(a)), 12,000 + 4,500 = 16,500. That is also - # above the limit, so there is still no reduction. + # 2012/2885 Sch 1 para 32(a)), 12,000 + 3,960 = 15,960, within the limit. + # Tariff income is 1 a week for each 500, or part of 500, over 10,000 + # (Sch 1 para 37): 5,960 / 500 = 11.92, so 12 a week, 624 a year. Income + # 12,480 + 624 = 13,104 is below the applicable amount of 256.00 a week + # (13,312), so the reduction is the whole 2,000. stopped = Simulation(situation=data, scenario=scenario(stopped=True)) assert stopped.calculate(FLAG, YEAR).tolist() == [False] - assert stopped.calculate(capital, YEAR)[0] == 16_500 - assert stopped.calculate("council_tax_reduction", YEAR)[0] == 0 + assert stopped.calculate(capital, YEAR)[0] == 15_960 + assert ( + abs(stopped.calculate("council_tax_reduction_tariff_income", YEAR)[0] - 624) + < 0.01 + ) + assert ( + abs( + stopped.calculate("council_tax_reduction_applicable_income", YEAR)[0] + - 13_104 + ) + < 0.01 + ) + assert abs(stopped.calculate("council_tax_reduction", YEAR)[0] - 2_000) < 0.01 diff --git a/policyengine_uk/tests/test_trading_loss_properties.py b/policyengine_uk/tests/test_trading_loss_properties.py index 9136be1135..6dc0a3a7b8 100644 --- a/policyengine_uk/tests/test_trading_loss_properties.py +++ b/policyengine_uk/tests/test_trading_loss_properties.py @@ -933,13 +933,15 @@ def values(sim, variable): "blind_persons_allowance", "marriage_allowance", "married_couples_allowance_deduction", - # The legacy means tests deduct only the tax on income they count, - # and Pension Credit leaves out the tax on uncounted income due - # without earnings. Both are recomputed from the tax components, so a - # loss that moves interest or dividends between bands would move them - # with income_tax held. Hold them as the tax the tests deduct. + # The legacy means tests leave out the tax on uncounted income due + # without earnings, which reads the Step 4 split on its branch, and + # deduct the rest (legacy_means_test_deducted_income_tax). Each is + # recomputed from the tax components, so a loss that moves interest + # or dividends between bands would move them with income_tax held. + # Hold them as the tax the tests deduct. "legacy_means_test_income_tax", - "pension_credit_uncounted_income_tax_without_earnings", + "legacy_means_test_uncounted_income_tax_without_earnings", + "legacy_means_test_deducted_income_tax", ] tax = {v: values(with_loss, v) for v in deductions} # SPC Regs reg 17(11) substitutes CER notional deductions for the tax diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 14a618bbf7..13099d61c8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -105,8 +105,9 @@ def hb_income_before_deductions(benunit, period, parameters): * p.pension_contribution_deduction_rate ) # Tax only on the income taken into account (HB Regs 2006 Sch 5 para 1; - # HB (SPC) Regs 2006 reg 33(12)), before the winter fuel payment charge. - TAX_COMPONENTS = ["legacy_means_test_income_tax", "national_insurance"] + # HB (SPC) Regs 2006 reg 33(12)), before the winter fuel payment charge, + # with earnings carrying the extra tax they cause. + TAX_COMPONENTS = ["legacy_means_test_deducted_income_tax", "national_insurance"] tax = add_for_members(benunit, period, TAX_COMPONENTS, members) increased_income_reduced_by_tax_and_pensions = ( increased_income - tax - pension_contributions diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index a5c50ffb3c..445aef1bee 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -71,7 +71,7 @@ def formula(benunit, period, parameters): tax = add_for_members( benunit, period, - ["legacy_means_test_income_tax", "national_insurance"], + ["legacy_means_test_deducted_income_tax", "national_insurance"], members, ) income += add_for_members(benunit, period, ["social_security_income"], members) diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py new file mode 100644 index 0000000000..d632fe3af4 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py @@ -0,0 +1,46 @@ +from policyengine_uk.model_api import * + + +class legacy_means_test_deducted_income_tax(Variable): + value_type = float + entity = Person + label = "Income tax deducted in the legacy means tests" + documentation = ( + "The income tax that Income Support, Housing Benefit, Pension Credit " + "and council tax reduction deduct for the person: their income tax " + "before the winter fuel payment charge, less the tax that the income " + "the tests treat as capital (rent from property, interest and " + "dividends), and the other Step 7 charges, would bear if nobody had " + "employed earnings or self-employment income " + "(legacy_means_test_uncounted_income_tax_without_earnings). The tests " + "disregard tax only on income they take into account (IS Regs 1987 " + "Sch 9 para 1; HB Regs 2006 Sch 5 para 1; HB (SPC) Regs 2006 reg " + "33(12); SPC Regs 2002 reg 17(10)(a)) and deduct the income tax " + "deducted from earnings (IS Regs 1987 reg 36(3)(a); HB Regs 2006 reg " + "36(3)(a)). The model has no PAYE codes and no provision says how to " + "split one liability, so earnings carry the extra tax they cause, " + "including what they add on savings, dividends and rent and any Step " + "7 charge they bring in, as Pension Credit measures the tax on " + "earnings. Without earnings it equals legacy_means_test_income_tax. " + "It is not floored: where one partner's earnings move a Step 7 " + "charge or the marriage allowance off the other partner, that " + "partner's value can be negative, and the claimant's and partner's " + "values together give the extra tax the earnings cause. Pension " + "Credit replaces the tax on self-employed profit with notional tax " + "(pension_credit_income); the other tests deduct this tax as it is." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/36", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/1", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/36", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/1", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/33", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + ) + + def formula(person, period, parameters): + return person("income_tax_before_winter_fuel_payment_charge", period) - person( + "legacy_means_test_uncounted_income_tax_without_earnings", period + ) diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py index bafd2b5a79..906eab1282 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_income_tax.py @@ -4,45 +4,48 @@ class legacy_means_test_income_tax(Variable): value_type = float entity = Person - label = "Income Tax deducted in the legacy means tests" + label = "Income tax on income the legacy means tests count, by Step 4 component" documentation = ( - "Income Tax on the income the legacy means tests take into account. The " - "tests disregard tax only on income they count (Income Support Schedule 9 " - "paragraph 1; Housing Benefit Schedule 5 paragraph 1; pension-age Housing " - "Benefit regulation 33(12); Pension Credit regulation 17(10)(a); the " - "council tax reduction schemes' equivalents), and they treat savings " - "interest, dividends and property income as capital, not income. So this " - "is the tax calculated on the person's other income (earned_income_tax, " - "which covers earnings, pensions and taxable benefits) at Step 4 of " - "Income Tax Act 2007 s.23, less that income's share of the Step 6 tax " - "reductions (the married couple's allowance, other tax credits and " - "the residential finance-cost reduction and the Pension Tax " - "Allowance). Most reductions are not " - "tied to any income: s.27 orders them only to give the greatest " - "reduction in liability. Two are: foreign tax credit relief reduces " - "the tax on the income the foreign tax was paid on (Taxation " - "(International and Other Provisions) Act 2010 ss.18(2) and 36), and " - "the residential finance-cost reduction is given on at most the " - "property business's profits, plus any current-year estate amounts " - "and within the adjusted total income limit (Income Tax (Trading and " - "Other Income) Act 2005 ss.274A and 274AA). The finance-cost " - "reduction (property_finance_cost_relief) therefore comes off the tax " - "on property income first. The model's other_tax_credits input mixes " - "foreign tax credit relief with other reductions (venture capital " - "trust, enterprise investment and maintenance relief, among others) " - "and records no source, so those reductions are shared in proportion " + "The part of the person's income tax that Step 4 of Income Tax Act " + "2007 s.23 calculates on the income the legacy means tests take into " + "account, less that income's share of the Step 6 tax reductions. The " + "tests do not deduct it directly: " + "legacy_means_test_uncounted_income_tax_without_earnings reads it on " + "a branch where nobody has employed earnings or self-employment " + "income, and takes the rest of the pre-charge tax there as the tax on " + "uncounted income, which legacy_means_test_deducted_income_tax leaves " + "out. Income Support, Housing Benefit, Pension Credit and council tax " + "reduction treat savings interest, dividends and property income as " + "capital, not income, so the counted income here is the person's " + "other income (earned_income_tax, which covers earnings, pensions and " + "taxable benefits). Its share of the Step 6 reductions (the married " + "couple's allowance, other tax credits, the residential finance-cost " + "reduction and the Pension Tax Allowance) follows two rules. Most " + "reductions are not tied to any income: s.27 orders them only to " + "give the greatest reduction in liability. Two are: foreign tax " + "credit relief reduces the tax on the income the foreign tax was paid " + "on (Taxation (International and Other Provisions) Act 2010 ss.18(2) " + "and 36), and the residential finance-cost reduction is given on at " + "most the property business's profits, plus any current-year estate " + "amounts and within the adjusted total income limit (Income Tax " + "(Trading and Other Income) Act 2005 ss.274A and 274AA). The " + "finance-cost reduction (property_finance_cost_relief) therefore " + "comes off the tax on property income first. The model's " + "other_tax_credits input mixes foreign tax credit relief with other " + "reductions (venture capital trust, enterprise investment and " + "maintenance relief, among others) and records no source, so those " + "reductions, and the Pension Tax Allowance, are shared in proportion " "to the Step 4 tax on each kind of income, as a stated convention. A " "credit for foreign tax on rent is therefore partly attributed to " - "counted income, and the tax deducted is too low. Step 7 charges, " - "such as the High Income " - "Child Benefit Charge, the pension annual allowance charge (s.30) " - "and the winter fuel payment charge, are not tax calculated on any " - "of this income and are not deducted. The result is " - "income_tax_before_winter_fuel_payment_charge, the tax every means " - "test starts from, less the tax attributed to capital and the other " - "charges, floored at nil, so a supplied value of it flows through; " - "if it differs from the tax the components imply, the difference " - "falls on the tax on counted income." + "counted income. Step 7 charges, such as the High Income Child " + "Benefit Charge, the pension annual allowance charge (s.30) and the " + "winter fuel payment charge, are not tax calculated on any of this " + "income, so none is included. The result is " + "income_tax_before_winter_fuel_payment_charge less the tax attributed " + "to capital and the other charges, floored at nil, so a supplied " + "value of the pre-charge tax flows through; if it differs from the " + "tax the components imply, the difference falls on the tax on " + "counted income." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py new file mode 100644 index 0000000000..b982e91bd2 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_uncounted_income_tax_without_earnings.py @@ -0,0 +1,58 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.pension_credit import employed_earnings_sources +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit_earnings_income_tax import ( + DECISION, + person_variable_with_inputs_zeroed, +) + + +class legacy_means_test_uncounted_income_tax_without_earnings(Variable): + value_type = float + entity = Person + label = "Income tax on income the legacy means tests do not count, without earnings" + documentation = ( + "The part of the person's income tax before the winter fuel payment " + "charge, if nobody in the simulation had employed earnings (employment " + "income and statutory sick, maternity and paternity pay) or " + "self-employment income, that is not tax on income the legacy means " + "tests take into account: the tax on rent from property, interest and " + "dividends, and the other Step 7 charges. It is that pre-charge tax " + "less legacy_means_test_income_tax, both on the branch without " + "earnings, floored at nil. Income Support, Housing Benefit, Pension " + "Credit and council tax reduction treat income derived from capital " + "as capital and disregard tax only on income they take into account " + "(IS Regs 1987 Sch 9 paras 1 and 22; HB Regs 2006 Sch 5 paras 1 and " + "17; HB (SPC) Regs 2006 reg 33(12) and Sch 5 para 22; SPC Regs 2002 " + "reg 17(10)(a) and Sch IV para 18), so none of them deducts this tax " + "(legacy_means_test_deducted_income_tax). It is measured without " + "earnings because the tax on earnings is the extra tax the earnings " + "cause, including on savings, dividends and rent, and the tests do " + "deduct that. " + DECISION + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/33", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV", + ) + + def formula(person, period, parameters): + income_tax = person("pension_credit_income_tax_without_earnings", period) + # Zero the same earnings as pension_credit_income_tax_without_earnings, + # so both taxes describe the same world without earnings. + sources = employed_earnings_sources(parameters, period) + [ + "self_employment_income" + ] + if not any((person(source, period) != 0).any() for source in sources): + counted = person("legacy_means_test_income_tax", period) + else: + counted = person_variable_with_inputs_zeroed( + person.simulation, + period, + sources, + "legacy_means_test_income_tax", + ) + return max_(0, income_tax - counted) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py index 9f7358eb97..3ddf9dcb24 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py @@ -147,50 +147,3 @@ def formula(benunit, period, parameters): ) - person("pension_credit_income_tax_without_earnings", period) claimant_or_partner = person("is_claimant_or_partner", period) return max_(0, benunit.sum(added * claimant_or_partner)) - - -class pension_credit_uncounted_income_tax_without_earnings(Variable): - value_type = float - entity = Person - label = "Income tax on income Pension Credit does not count, without earnings" - documentation = ( - "The part of the person's income tax before the winter fuel payment " - "charge, if nobody in the simulation had employed earnings (employment " - "income and statutory sick, maternity and paternity pay) or " - "self-employment income, that is not tax on income " - "Pension Credit takes into account: the tax on rent from property, " - "interest and dividends, and the other Step 7 charges " - "(legacy_means_test_income_tax). SPC Regs 2002 reg. 17(10)(a) " - "disregards tax only on income taken into account, and Schedule IV " - "paragraph 18 disregards actual income from capital, so Pension " - "Credit income does not deduct this tax. It is measured without " - "earnings because the tax on earnings is the extra tax the earnings " - "cause, including on savings and dividends " - "(pension_credit_employment_income_tax and " - "pension_credit_self_employment_income_tax), which Pension Credit " - "income does deduct. " + DECISION - ) - definition_period = YEAR - unit = GBP - reference = ( - "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", - "https://www.legislation.gov.uk/uksi/2002/1792/schedule/IV", - ) - - def formula(person, period, parameters): - income_tax = person("pension_credit_income_tax_without_earnings", period) - # Zero the same earnings as pension_credit_income_tax_without_earnings, - # so both taxes describe the same world without earnings. - sources = employed_earnings_sources(parameters, period) + [ - "self_employment_income" - ] - if not any((person(source, period) != 0).any() for source in sources): - counted = person("legacy_means_test_income_tax", period) - else: - counted = person_variable_with_inputs_zeroed( - person.simulation, - period, - sources, - "legacy_means_test_income_tax", - ) - return max_(0, income_tax - counted) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index 8857ff3b19..7405a0ed62 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -47,16 +47,23 @@ def formula(benunit, period, parameters): pension_contributions = benunit.sum( person("pension_contributions", period) * is_claimant_or_partner ) - # Self-employed earnings are net of the notional tax and contributions - # of the Computation of Earnings Regulations 1996 reg. 14, in place of - # those actually payable on the profit (reg. 17(11)). + # Reg 17(10)(a) disregards tax only on income taken into account, and + # Schedule IV paragraph 18 disregards actual income from capital. The + # tax on earnings is the extra tax the earnings cause, so the income + # tax deducted leaves out the tax on uncounted income, and the Step 7 + # charges, that the claimant and partner would pay without earnings + # (legacy_means_test_deducted_income_tax, which the other legacy + # means tests deduct too). Self-employed earnings are net of the + # notional tax and contributions of the Computation of Earnings + # Regulations 1996 reg. 14, in place of those actually payable on the + # profit (reg. 17(11)). tax = benunit.sum( ( add( person, period, [ - "income_tax_before_winter_fuel_payment_charge", + "legacy_means_test_deducted_income_tax", "national_insurance", "pension_credit_self_employment_deductions", ], @@ -65,15 +72,6 @@ def formula(benunit, period, parameters): ) * is_claimant_or_partner ) - benunit("pension_credit_self_employment_income_tax", period) - # Reg 17(10)(a) disregards tax only on income taken into account, and - # Schedule IV paragraph 18 disregards actual income from capital. The - # tax on earnings is the extra tax the earnings cause, so the tax not - # deducted is the tax on uncounted income, and the Step 7 charges, - # that the claimant and partner would pay without earnings. - tax -= benunit.sum( - person("pension_credit_uncounted_income_tax_without_earnings", period) - * is_claimant_or_partner - ) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate # Schedule VI earnings disregards (reg. 17(9)). diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py index 81cea515be..d04efdbf02 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_receives_pension_credit.py @@ -34,7 +34,9 @@ def formula(benunit, period, parameters): # the earnings counterfactuals on the parent so this branch and # ordinary Pension Credit share them whichever is requested first. benunit.members("pension_credit_income_tax_without_earnings", period) - benunit.members("pension_credit_uncounted_income_tax_without_earnings", period) + benunit.members( + "legacy_means_test_uncounted_income_tax_without_earnings", period + ) name = "uc_non_dep_pension_credit" while name in simulation.branches or name == simulation.branch_name: name += "_" diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index cddae83277..96b9b22eef 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -352,7 +352,7 @@ def formula(benunit, period, parameters): tax = add_for_members( benunit, period, - ["legacy_means_test_income_tax", "national_insurance"], + ["legacy_means_test_deducted_income_tax", "national_insurance"], members, ) # SI 2012/2885 Sch 1 para 17(9) and Sch 4; WSI 2013/3029 Sch 1 para From 7381a46c771861ca8a17f1053053a8a75a587d7e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 19:44:08 +0000 Subject: [PATCH 18/24] Restate the legacy means test tax invariants for the shared measure Invariant 1 now holds without earnings and, with earnings, each means-test income moves by exactly minus the change in the tax that test deducts and in its earnings disregard. Invariant 4 bounds the shared deduction, and a differential test checks that IS, HB and the CTR line deduct per person what Pension Credit's tax line does. A YAML case pins the unfloored per-person deduction for a couple whose pay moves the High Income Child Benefit Charge between partners. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6GU2xd3vfduwbp9kHh --- .../income_tax_caused_by_earnings.yaml | 39 +++++++++++++++++++ ...ans_test_income_from_capital_properties.py | 11 +++--- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml index dc28bfc670..220386d294 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml @@ -203,3 +203,42 @@ legacy_means_test_income_tax: 6_486 legacy_means_test_uncounted_income_tax_without_earnings: 2_000 legacy_means_test_deducted_income_tax: 8_032 + +- name: When one partner's pay moves the High Income Child Benefit Charge off the other, the couple's deduction nets it + # A couple with one child. Partner a has interest of 70,000 and partner b + # pay of 80,000. Child Benefit 1,406 in whole pounds. The charge falls on + # the partner with the higher adjusted net income (ITEPA 2003 s.681B): b, + # at 80,000, the whole 1,406. a's interest: 57,430 taxable, 5,000 at the + # starting rate and 500 savings allowance at nil, 32,200 at 20% = 6,440, + # 19,730 at 40% = 7,892; 14,332. b's pay: 37,700 at 20% = 7,540 and 29,730 + # at 40% = 11,892; 19,432, plus the charge, 20,838. + # Without the pay, a has the higher income (70,000) and pays 50% x 1,406 = + # 703 of charge: 15,035 in all, none on counted income. So a's deduction is + # 14,332 - 15,035 = -703, and b's is 20,838. Together they deduct 20,135: + # the 19,432 on the pay and the 703 of charge the pay adds. The deduction is + # not floored per person, so the couple's total is the extra tax the pay + # causes. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + a: + age: 40 + savings_interest_income: 70_000 + b: + age: 40 + employment_income: 80_000 + child: + age: 5 + benunits: + benunit: + members: [a, b, child] + households: + household: + members: [a, b, child] + region: NORTH_EAST + output: + CB_HITC: [0, 1_406, 0] + income_tax_before_winter_fuel_payment_charge: [14_332, 20_838, 0] + legacy_means_test_uncounted_income_tax_without_earnings: [15_035, 0, 0] + legacy_means_test_deducted_income_tax: [-703, 20_838, 0] diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index ba5f4981a6..0a2751e0b0 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -233,7 +233,6 @@ def calculate_with_taxes(units, year, variables, **kwargs): def test_income_from_capital_changes_no_legacy_means_test(units, year): disregards = sorted(set(EARNINGS_DISREGARDS.values())) routes = [ - "in_receipt_of_pension_credit", "council_tax_reduction_universal_credit_award", "housing_benefit_on_passporting_benefit", ] @@ -264,8 +263,8 @@ def test_income_from_capital_changes_no_legacy_means_test(units, year): # Intended exception (d1424): where earnings make it move, each # means-test income moves by exactly minus that change, wherever # neither run is floored and the test uses its own income rules. - pension_credit_route = (base["in_receipt_of_pension_credit"] > 0) | ( - scaled["in_receipt_of_pension_credit"] > 0 + pension_credit_route = (base["pension_credit"] > 0) | ( + scaled["pension_credit"] > 0 ) own_rules = { "income_support_applicable_income": np.ones_like(steady), @@ -456,9 +455,9 @@ def test_the_four_tests_deduct_the_same_income_tax(units, year): ) moved = np.asarray(bumped.calculate(income, year), dtype=float) unfloored = (base > 1) & (moved > 0) - own_rules = ( - np.asarray(sim.calculate("in_receipt_of_pension_credit", year)) == 0 - ) | (income == "pension_credit_income") + own_rules = (np.asarray(sim.calculate("pension_credit", year)) == 0) | ( + income == "pension_credit_income" + ) if income == "housing_benefit_applicable_income": own_rules &= ( np.asarray( From 0865daac63ffb117f17a1bd84bef48b7abf60572 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 06:52:53 +0000 Subject: [PATCH 19/24] Count the council tax reduction applicant's own letting rent and tariff income A household head who applies alone is the applicant, so council tax reduction now reads her own rent from letting part of the home (council_tax_reduction_home_letting_income) rather than the benefit unit's claimant's and partner's, and keeps her tariff income when the benefit unit's claimant and partner receive Pension Credit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KuHbGxemvogNVtTue --- .../income_from_capital.yaml | 198 ++++++++++++++++++ .../legacy_benefits_home_letting_income.py | 21 +- ...council_tax_reduction_applicable_income.py | 12 +- ...uncil_tax_reduction_home_letting_income.py | 39 ++++ .../council_tax_reduction_tariff_income.py | 11 +- 5 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_home_letting_income.py diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index 70f0cdb437..a4dcba374b 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -1883,3 +1883,201 @@ output: council_tax_reduction_assessable_capital: 10_000 council_tax_reduction_tariff_income: 0 + +# A household head who applies alone for council tax reduction is the +# applicant (SI 2012/2885 reg 2(1)), so her own income counts (Sch 1 para 11), +# including rent for letting part of her home (Sch 1 para 16(1)(v), less £20 a +# week under Sch 5 para 10), and the benefit unit's claimant's and partner's +# does not. In each case below the head, aged 70, reached State Pension age +# after 1 April 2021, so her 2026-27 personal allowance is £238 a week: +# 238 x 52 = 12,376. Maidstone runs England's pensioner scheme: maximum 100% +# of the £2,000 council tax, withdrawn at 20% of income above the applicable +# amount. Her £12,000 private pension falls within the £12,570 personal +# allowance, and the model taxes none of the rent: income tax nil. + +- name: A household head applying alone counts her own rent from letting part of the home in council tax reduction + # Rent 5,200 / 52 = 100 a week, less 20 = 80; 80 x 52 = 4,160. + # Income 12,000 + 4,160 = 16,160. CTR 2,000 - 20% x (16,160 - 12,376) + # = 2,000 - 756.80 = 1,243.20. The young couple's legacy measure + # (legacy_benefits_home_letting_income, their claimant and partner) holds + # none of it. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + private_pension_income: 12_000 + sublet_income: 5_200 + mother: + age: 16 + is_parent: true + is_household_head: false + father: + age: 16 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [head, mother, father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [head, mother, father, baby] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_head_applies_alone: [true] + is_council_tax_reduction_applicant_or_partner: [true, false, false, false] + income_tax: [0, 0, 0, 0] + legacy_benefits_home_letting_income: [0] + council_tax_reduction_home_letting_income: [4_160] + council_tax_reduction_applicable_income: [16_160] + council_tax_reduction_applicable_amount: [12_376] + council_tax_reduction: 1_243.20 + +- name: The same head in her own benefit unit gets the same council tax reduction + # The control: the head is her own benefit unit's claimant, so both + # measures count her rent: 4,160, income 16,160 and CTR 1,243.20 as above. + # The couple's unit counts the baby's Child Benefit, 27.05 x 52 = 1,406.60; + # neither parent is liable for the council tax, so they get no reduction. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + private_pension_income: 12_000 + sublet_income: 5_200 + mother: + age: 16 + is_parent: true + is_household_head: false + father: + age: 16 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + head_unit: + members: [head] + claims_all_entitled_benefits: true + would_claim_uc: false + couple_unit: + members: [mother, father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [head, mother, father, baby] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_head_applies_alone: [false, false] + legacy_benefits_home_letting_income: [4_160, 0] + council_tax_reduction_home_letting_income: [4_160, 0] + council_tax_reduction_applicable_income: [16_160, 1_406.60] + council_tax_reduction: 1_243.20 + +- name: A household head applying alone does not count the young couple's rent from letting part of the home + # The converse: the mother receives the 5,200, and the head has none. The + # couple's own measure counts it, 4,160, but the head's CTR income is her + # pension alone: 12,000, below 12,376, so CTR pays the whole 2,000. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + private_pension_income: 12_000 + mother: + age: 16 + is_parent: true + is_household_head: false + sublet_income: 5_200 + father: + age: 16 + is_parent: true + is_household_head: false + baby: + age: 0 + is_household_head: false + benunits: + family: + members: [head, mother, father, baby] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [head, mother, father, baby] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_head_applies_alone: [true] + legacy_benefits_home_letting_income: [4_160] + council_tax_reduction_home_letting_income: [0] + council_tax_reduction_applicable_income: [12_000] + council_tax_reduction: 2_000 + +- name: A household head applying alone keeps her tariff income when the couple she lives with gets Pension Credit + # The head, aged 85, reached State Pension age before 1 April 2021, so her + # allowance is 256 x 52 = 13,312. A pensioner couple flagged as parents + # forms the benefit unit's claimant and partner, and gets guarantee credit. + # That award is theirs, so the head's capital and tariff income follow the + # general rule: household savings 14,000 stand in for her capital. Tariff + # (Sch 1 para 37): £1 a week for each £500, or part, over £10,000: + # 4,000 / 500 = 8; 8 x 52 = 416. Tax on her pension: (13,000 - 12,570) + # x 20% = 86. Income 13,000 - 86 + 416 = 13,330. CTR 2,000 - 20% x + # (13,330 - 13,312) = 1,996.40. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 85 + is_household_head: true + private_pension_income: 13_000 + older_partner: + age: 67 + is_parent: true + is_household_head: false + younger_partner: + age: 66 + is_parent: true + is_household_head: false + benunits: + family: + members: [head, older_partner, younger_partner] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [head, older_partner, younger_partner] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 14_000 + output: + is_claimant_or_partner: [false, true, true] + council_tax_reduction_head_applies_alone: [true] + in_receipt_of_guarantee_credit: [true] + council_tax_reduction_assessable_capital: [14_000] + council_tax_reduction_tariff_income: [416] + council_tax_reduction_applicable_income: [13_330] + council_tax_reduction_applicable_amount: [13_312] + council_tax_reduction: 1_996.40 diff --git a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py index 2cf125e315..c8f89f0af7 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py +++ b/policyengine_uk/variables/gov/dwp/legacy_benefits_home_letting_income.py @@ -1,6 +1,17 @@ from policyengine_uk.model_api import * +def home_letting_income(benunit, period, parameters, counted, pension_age): + """Annual rent the `counted` members receive for letting part of their + home, less the weekly sub-tenant disregard for one occupier: the + pension-age amount where `pension_age` holds, else the working-age one.""" + p = parameters(period).gov.dwp.legacy_means_tests.sub_tenant_rent_disregard + person = benunit.members + weekly_rent = benunit.sum(person("sublet_income", period) * counted) / WEEKS_IN_YEAR + disregard = where(pension_age, p.pension_age, p.working_age) + return max_(0, weekly_rent - disregard) * WEEKS_IN_YEAR + + class legacy_benefits_home_letting_income(Variable): value_type = float entity = BenUnit @@ -35,13 +46,9 @@ class legacy_benefits_home_letting_income(Variable): ] def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.legacy_means_tests.sub_tenant_rent_disregard person = benunit.members claimant_or_partner = person("is_claimant_or_partner", period) - weekly_rent = ( - benunit.sum(person("sublet_income", period) * claimant_or_partner) - / WEEKS_IN_YEAR - ) pension_age = benunit.any(person("is_SP_age", period) & claimant_or_partner) - disregard = where(pension_age, p.pension_age, p.working_age) - return max_(0, weekly_rent - disregard) * WEEKS_IN_YEAR + return home_letting_income( + benunit, period, parameters, claimant_or_partner, pension_age + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 96b9b22eef..9101704ce4 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -54,8 +54,9 @@ class council_tax_reduction_applicable_income(Variable): "for pensioners, Wales and Scotland) count the tariff income they " "treat capital as yielding (council_tax_reduction_tariff_income); the " "English local schemes for people under pension age add their own. " - "Rent for letting part of the home counts, less the sub-tenant " - "disregard, on the schemes' own income rules. It does not count " + "Rent the applicant and partner receive for letting part of the home " + "counts, less the sub-tenant disregard, on the schemes' own income " + "rules (council_tax_reduction_home_letting_income). It does not count " "where a working-age applicant has an award of Universal Credit, " "whose income the schemes take from the Secretary of State's " "Universal Credit assessment (which does not count it), or in the " @@ -304,8 +305,9 @@ def formula(benunit, period, parameters): - maintenance_disregard + widowed_parents_allowance ) - # Rent for letting part of the home counts under the schemes' own - # income rules, less the sub-tenant disregard. Two exceptions: + # Rent the applicant and partner receive for letting part of the home + # counts under the schemes' own income rules, less the sub-tenant + # disregard (SI 2012/2885 Sch 1 paras 11 and 16(1)(v), Sch 5 para 10). Two exceptions: # - A working-age applicant with an award of Universal Credit: the # schemes use the Secretary of State's assessment of income for the # award (SI 2012/2886 Sch para 37(1); WSI 2013/3029 Sch 6 para 9(1); @@ -333,7 +335,7 @@ def formula(benunit, period, parameters): ) increased_income += where( counts_home_letting, - benunit("legacy_benefits_home_letting_income", period), + benunit("council_tax_reduction_home_letting_income", period), 0, ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_home_letting_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_home_letting_income.py new file mode 100644 index 0000000000..07fb3c23c4 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_home_letting_income.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.legacy_benefits_home_letting_income import ( + home_letting_income, +) + + +class council_tax_reduction_home_letting_income(Variable): + value_type = float + entity = BenUnit + label = "Rent from letting part of the home counted for Council Tax Reduction" + documentation = ( + "Rent the Council Tax Reduction applicant and any partner " + "(is_council_tax_reduction_applicant_or_partner) receive for letting " + "part of the home they live in (sublet_income), after the weekly " + "sub-tenant disregard for one occupier. Where the household head " + "applies alone, the head is the applicant, so the head's own rent " + "counts and the benefit unit's claimant's and partner's does not. The " + "pension-age disregard applies where the applicant falls under the " + "pension-age scheme (council_tax_reduction_pensioner). " + "council_tax_reduction_applicable_income decides on which routes it " + "counts." + ) + definition_period = YEAR + unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/11", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/16", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/10", + ] + + def formula(benunit, period, parameters): + applicant_or_partner = benunit.members( + "is_council_tax_reduction_applicant_or_partner", period + ) + pensioner = benunit("council_tax_reduction_pensioner", period) + return home_letting_income( + benunit, period, parameters, applicant_or_partner, pensioner + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py index 8b76864719..f69ff77239 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_tariff_income.py @@ -132,7 +132,12 @@ def formula(benunit, period, parameters): ], default=working_age_weekly * WEEKS_IN_YEAR, ) - pension_credit_route = benunit( - "in_receipt_of_guarantee_credit", period - ) | benunit("in_receipt_of_savings_credit_only", period) + # The benefit unit's Pension Credit is its claimant's and partner's, not + # a household head's who applies alone + # (council_tax_reduction_head_applies_alone). + applicant_family = ~benunit("council_tax_reduction_head_applies_alone", period) + pension_credit_route = applicant_family & ( + benunit("in_receipt_of_guarantee_credit", period) + | benunit("in_receipt_of_savings_credit_only", period) + ) return where(pension_credit_route, 0, tariff_income) From d574b4291c56b0b146f848e67d43ef0a510ef28f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 07:07:44 +0000 Subject: [PATCH 20/24] Re-derive the landlord election cases, floor each test's tax deduction at nil, and test the Step 4 split with taxed capital Pension Credit no longer counts property profits, so the winter fuel payment charge landlord cases now take wages that carry the tax on the profits, and keep their boundaries. Each legacy means test floors the sum of the deducted income tax over the members it counts at nil. The no-earnings property draws capital income that bears tax without earnings. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KuHbGxemvogNVtTue --- .../income_tax_caused_by_earnings.yaml | 83 +++++++ .../charges/winter_fuel_payment_charge.yaml | 189 +++++++++++----- ...ans_test_income_from_capital_properties.py | 206 ++++++++++++++++-- .../tests/test_property_income_properties.py | 48 ++-- .../housing_benefit_applicable_income.py | 11 +- .../dwp/income_support_applicable_income.py | 15 +- .../legacy_means_test_deducted_income_tax.py | 6 +- .../pension_credit_earnings_income_tax.py | 6 +- .../pension_credit/pension_credit_income.py | 39 ++-- .../allowances/uses_property_allowance.py | 12 +- ...council_tax_reduction_applicable_income.py | 15 +- 11 files changed, 505 insertions(+), 125 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml index 220386d294..b10e7830a0 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml @@ -242,3 +242,86 @@ income_tax_before_winter_fuel_payment_charge: [14_332, 20_838, 0] legacy_means_test_uncounted_income_tax_without_earnings: [15_035, 0, 0] legacy_means_test_deducted_income_tax: [-703, 20_838, 0] + +- name: Where the test counts only the member whose deduction is negative, it floors the sum at nil + # The couple's incomes above, with a aged 64 and b aged 44: the model + # presumes b, 20 years younger, to be a's child, so each test counts a + # alone (is_claimant_or_partner). The taxes stay as above: a's deduction is + # 14,332 - 15,035 = -703, b's 20,838. Each test sums the deduction over the + # members it counts, here -703, and floors that sum at nil, so it deducts + # nothing. Without the floor each would add 703 of tax a never paid on + # counted income. + # - Income Support counts none of a's interest (reg 48(4)): income 0. + # - Pension Credit counts none of it either (Sch IV para 18): income 0. + # - Council tax reduction counts the benefit unit's Child Benefit, + # 27.05 x 52 = 1,406.60, and nothing else of a's: income 1,406.60. + # The household rents at 9,000 and pays council tax of 1,500; the family + # claims no Universal Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + a: + age: 64 + savings_interest_income: 70_000 + b: + age: 44 + employment_income: 80_000 + child: + age: 3 + benunits: + benunit: + members: [a, b, child] + would_claim_uc: false + households: + household: + members: [a, b, child] + country: ENGLAND + tenure_type: RENT_PRIVATELY + rent: 9_000 + council_tax: 1_500 + output: + is_claimant_or_partner: [true, false, false] + is_council_tax_reduction_applicant_or_partner: [true, false, false] + legacy_means_test_deducted_income_tax: [-703, 20_838, 0] + income_support_applicable_income: [0] + pension_credit_income: [0] + council_tax_reduction_applicable_income: [1_406.60] + +- name: Housing Benefit's means test floors the counted member's negative deduction at nil + # The same household with a child aged 5, so no test passports it: it + # receives no Income Support (income_support 0), and Housing Benefit takes + # income through its own means test. Housing Benefit counts none of a's + # interest (reg 46(4)) and excludes Child Benefit, so a's income is nil; the + # sum of the deduction over the members it counts, -703, floors at nil: + # income 0, not 703. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + a: + age: 64 + savings_interest_income: 70_000 + b: + age: 44 + employment_income: 80_000 + child: + age: 5 + benunits: + benunit: + members: [a, b, child] + would_claim_uc: false + claims_all_entitled_benefits: true + households: + household: + members: [a, b, child] + country: ENGLAND + tenure_type: RENT_PRIVATELY + rent: 9_000 + council_tax: 1_500 + output: + is_claimant_or_partner: [true, false, false] + legacy_means_test_deducted_income_tax: [-703, 20_838, 0] + income_support: [0] + housing_benefit_pension_age_regulations_apply: [false] + housing_benefit_applicable_income: [0] diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml index e1a3cfe812..50ba903fac 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml @@ -758,24 +758,34 @@ income_tax: [4_086] - name: 2025 a landlord's property allowance election counts the charge - # Private pension £32,520; property receipts £3,500, expenses £200 (profit - # £3,300), finance costs £1,500; five children. With expenses: (35,820 - - # 12,570) x 20% = £4,650, less the finance-cost reduction 20% x £1,500 = - # £300, so £4,350. With partial relief (£1,000 allowance, no reduction): - # (35,020 - 12,570) x 20% = £4,490. Total income is over £35,000 either - # way (£35,820 and £35,020), so Pension Credit decides the charge. Pension - # Credit income is £29,970 with - # expenses and £29,830 with the allowance, against the model's minimum - # guarantee of £29,893.76 (with the amounts for the children), so only the - # allowance route gets Pension Credit, which exempts the pensioner from the - # charge (s.681I(4)). Total tax: £4,350 + £200 = £4,550 with expenses, - # £4,490 with the allowance, so the pensioner elects for partial relief. + # Wages £27,000; private pension £9,070; property receipts £3,500, expenses + # £200 (profit £3,300), finance costs £1,500; five children. A pensioner + # pays no employee National Insurance. + # With expenses: (39,370 - 12,570) x 20% = £5,360, less the finance-cost + # reduction 20% x £1,500 = £300, so £5,060. With partial relief (£1,000 + # allowance, no reduction): (38,570 - 12,570) x 20% = £5,200. Total income + # is over £35,000 either way (£39,370 and £38,570), so Pension Credit + # decides the charge. + # Pension Credit no longer counts the property profits (SPC Sch IV para + # 18), but it deducts the tax the wages cause. Without the wages, pension + # and profits (£12,370 or £11,570) fall within the personal allowance and + # bear no tax, so the whole tax is the wages', and Pension Credit deducts + # it all: wages and pension less tax less the £20 weekly lone parent + # earnings disregard (£1,040). Pension Credit income is 36,070 - 5,060 - + # 1,040 = £29,970 with expenses and 36,070 - 5,200 - 1,040 = £29,830 with + # the allowance, against the model's minimum guarantee of £29,893.76 (with + # the amounts for the children). Only the allowance route gets Pension + # Credit (£63.76), which exempts the pensioner from the charge + # (s.681I(4)). Total tax: £5,060 + £200 = £5,260 with expenses, £5,200 + # with the allowance, so the pensioner elects for partial relief. period: 2025 + absolute_error_margin: 0.01 input: people: pensioner: age: 70 - private_pension_income: 32_520 + employment_income: 27_000 + private_pension_income: 9_070 property_rental_income: 3_500 property_income: 3_300 property_finance_costs: 1_500 @@ -800,35 +810,47 @@ country: ENGLAND output: uses_property_allowance: [true, false, false, false, false, false] + legacy_means_test_uncounted_income_tax_without_earnings: [0, 0, 0, 0, 0, 0] pension_credit_income: 29_830 + pension_credit: 63.76 winter_fuel_payment: [200, 0, 0, 0, 0, 0] winter_fuel_payment_charge: [0, 0, 0, 0, 0, 0] - income_tax_before_winter_fuel_payment_charge: [4_490, 0, 0, 0, 0, 0] - income_tax: [4_490, 0, 0, 0, 0, 0] + income_tax_before_winter_fuel_payment_charge: [5_200, 0, 0, 0, 0, 0] + income_tax: [5_200, 0, 0, 0, 0, 0] - name: 2025 a landlord without finance costs takes the allowance even where it ends Pension Credit (a model limit) - # Private pension £35,207; property receipts £3,500, expenses £200 (profit - # £3,300), no finance costs; six children. With partial relief (the £1,000 - # allowance in place of £200 of expenses): total income £37,707, tax - # (37,707 - 12,570) x 20% = £5,027.40. With expenses: total income £38,507, - # tax (38,507 - 12,570) x 20% = £5,187.40. Total income is over £35,000 - # either way, so Pension Credit decides the charge. Pension Credit income - # (pension and property profits, less tax before the charge) is £33,479.60 - # with the allowance and £33,319.60 with expenses, against the model's - # minimum guarantee of £33,399.60 (with the amounts for the children), so - # only the expenses route gets Pension Credit (£80), which exempts the - # pensioner from the £200 charge (s.681I(4)). Total tax: £5,227.40 with the - # allowance, £5,187.40 with expenses, so a pensioner choosing the lower tax - # would not elect for partial relief. The model does not compare routes for - # landlords without finance costs (uses_property_allowance), so it takes the - # allowance: no Pension Credit, charged the £200. This case pins that limit. + # Wages £31,562; private pension £9,070; property receipts £3,500, expenses + # £200 (profit £3,300), no finance costs; six children. A pensioner pays no + # employee National Insurance. With partial relief (the £1,000 allowance + # in place of £200 of expenses): total income £43,132, tax (43,132 - + # 12,570) x 20% = £6,112.40. With expenses: total income £43,932, tax + # (43,932 - 12,570) x 20% = £6,272.40. Total income is over £35,000 either + # way, so Pension Credit decides the charge. + # Pension Credit no longer counts the property profits (SPC Sch IV para + # 18), but without the wages, pension and profits (£11,570 or £12,370) + # fall within the personal allowance, so the whole tax is the wages', and + # Pension Credit deducts it all. Pension Credit income (wages and pension, + # less that tax, less the £20 weekly lone parent earnings disregard of + # £1,040) is 40,632 - 6,112.40 - 1,040 = £33,479.60 with the allowance and + # 40,632 - 6,272.40 - 1,040 = £33,319.60 with expenses, against the + # model's minimum guarantee of £33,399.60 (with the amounts for the + # children). Only the expenses route gets Pension Credit (£80), which + # exempts the pensioner from the £200 charge (s.681I(4)). Total tax: + # £6,312.40 with the allowance, £6,272.40 with expenses, so a pensioner + # choosing the lower tax would not elect for partial relief. The model + # does not compare routes for landlords without finance costs + # (uses_property_allowance), so it takes the allowance: no Pension Credit, + # charged the £200. This case pins that limit, which survives only where + # the pensioner has earnings: without them, the property route does not + # move the tax Pension Credit deducts. period: 2025 absolute_error_margin: 0.01 input: people: pensioner: age: 70 - private_pension_income: 35_207 + employment_income: 31_562 + private_pension_income: 9_070 property_rental_income: 3_500 property_income: 3_300 child_1: @@ -854,29 +876,91 @@ country: ENGLAND output: uses_property_allowance: [true, false, false, false, false, false, false] - total_income_charged_to_tax: [37_707, 0, 0, 0, 0, 0, 0] + total_income_charged_to_tax: [43_132, 0, 0, 0, 0, 0, 0] pension_credit_income: 33_479.60 pension_credit: 0 winter_fuel_payment: [200, 0, 0, 0, 0, 0, 0] winter_fuel_payment_charge: [200, 0, 0, 0, 0, 0, 0] - income_tax: [5_227.40, 0, 0, 0, 0, 0, 0] + income_tax: [6_312.40, 0, 0, 0, 0, 0, 0] + +- name: 2025 without earnings the property route does not move a landlord's Pension Credit + # The same pensioner and six children with no wages and a private pension + # of £40,632. Without earnings, Pension Credit deducts only the Step 4 tax + # on the pension, (40,632 - 12,570) x 20% = £5,612.40, whatever the + # property route: the profits sit on top of the pension and bear their own + # tax. Pension Credit income is 40,632 - 5,612.40 = £35,019.60 on both + # routes, above the minimum guarantee of £33,399.60, so no Pension Credit + # either way. Total income exceeds £35,000 on both routes, so the £200 + # charge applies either way, and the allowance, which gives the lower tax + # before the charge, gives the lower income tax: (43,132 - 12,570) x 20% + + # 200 = £6,312.40. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 40_632 + property_rental_income: 3_500 + property_income: 3_300 + child_1: + age: 10 + child_2: + age: 10 + child_3: + age: 10 + child_4: + age: 10 + child_5: + age: 10 + child_6: + age: 10 + benunits: + unit: + members: [pensioner, child_1, child_2, child_3, child_4, child_5, child_6] + would_claim_pc: true + would_claim_uc: false + households: + household: + members: [pensioner, child_1, child_2, child_3, child_4, child_5, child_6] + country: ENGLAND + output: + uses_property_allowance: [true, false, false, false, false, false, false] + legacy_means_test_deducted_income_tax: [5_612.40, 0, 0, 0, 0, 0, 0] + pension_credit_income: 35_019.60 + pension_credit: 0 + winter_fuel_payment_charge: [200, 0, 0, 0, 0, 0, 0] + income_tax: [6_312.40, 0, 0, 0, 0, 0, 0] - name: 2025 landlords in two benefit units of one household each elect with the other's route fixed - # One household. Unit A: a claimant aged 70 with private pension - # £42,772.55, a partner aged 85 with no income, and five children. Unit B: - # an unrelated person aged 85 with private pension £40,000. Each landlord - # has property receipts £3,500 and expenses £200 (profit £3,300); finance - # costs are £1,200 (A) and £675 (B). Unit A claims Pension Credit; B does - # not. - # A: with partial relief, total income £45,272.55 and tax before the charge - # (45,272.55 - 12,570) x 20% = £6,540.51, less the Marriage Allowance - # reduction 20% x £1,260 = £252, so £6,288.51. Unit A then gets Pension - # Credit (a savings credit of £16 in the model), which exempts A from the - # charge and makes A the person paid for the couple, so A's partner is not - # entitled (SI 2025/969 reg 4(2)(a)); A is paid the couple's full £300. - # With expenses, A's tax before the charge is £6,700.51 - £252 - 20% x - # £1,200 = £6,208.51, unit A gets no Pension Credit, and A's £100 shared - # payment is charged: £6,308.51. A elects for partial relief. + # One household. Unit A: a claimant aged 70 with wages £37,047.55 and + # private pension £9,000, a partner aged 85 with no income, and five + # children. Unit B: an unrelated person aged 85 with private pension + # £40,000. Each landlord has property receipts £3,500 and expenses £200 + # (profit £3,300); finance costs are £1,200 (A) and £675 (B). Unit A claims + # Pension Credit; B does not. Pensioners pay no employee National + # Insurance. + # A: with partial relief, total income £48,547.55 and tax before the charge + # (48,547.55 - 12,570) x 20% = £7,195.51, less the Marriage Allowance + # reduction 20% x £1,260 = £252, so £6,943.51. With expenses, total income + # £49,347.55 and tax (49,347.55 - 12,570) x 20% = £7,355.51, less £252 and + # the finance-cost reduction 20% x £1,200 = £240, so £6,863.51. + # Pension Credit no longer counts the property profits (SPC Sch IV para + # 18). Without the wages, A's pension and profits (£11,500 or £12,300) + # fall within the personal allowance and bear no tax, so the whole tax is + # the wages', and Pension Credit deducts it all. Its income, less the £10 + # weekly couple earnings disregard (£520), is 46,047.55 - 6,943.51 - 520 = + # £38,584.04 with the allowance and 46,047.55 - 6,863.51 - 520 = + # £38,664.04 with expenses. Both exceed the model's minimum guarantee of + # £36,107.76 (with the amounts for the children), so only savings credit + # can be paid: the model's maximum of £1,006.51 less 40% of income above + # the guarantee. With the allowance that gives 1,006.51 - 40% x 2,476.28 = + # £16.00; with expenses 1,006.51 - 40% x 2,556.28, below nil. + # So with the allowance unit A gets Pension Credit, which exempts A from + # the charge and makes A the person paid for the couple, so A's partner is + # not entitled (SI 2025/969 reg 4(2)(a)); A is paid the couple's full + # £300. With expenses, unit A gets no Pension Credit, and A's £100 shared + # payment is charged: £6,963.51. A elects for partial relief. # B: tax before the charge is (42,500 - 12,570) x 20% = £5,986 with the # allowance and (43,300 - 12,570) x 20% - 20% x £675 = £6,011 with # expenses. B's total income is over £35,000 on both routes, so B is @@ -894,7 +978,8 @@ claimant: age: 70 is_benunit_head: true - private_pension_income: 42_772.55 + employment_income: 37_047.55 + private_pension_income: 9_000 property_rental_income: 3_500 property_income: 3_300 property_finance_costs: 1_200 @@ -932,11 +1017,13 @@ country: ENGLAND output: uses_property_allowance: [true, false, true, false, false, false, false, false] - income_tax_before_winter_fuel_payment_charge: [6_288.51, 0, 5_986, 0, 0, 0, 0, 0] + income_tax_before_winter_fuel_payment_charge: [6_943.51, 0, 5_986, 0, 0, 0, 0, 0] + # Unit B: pension less its Step 4 tax, 40,000 - (40,000 - 12,570) x 20%. + pension_credit_income: [38_584.04, 34_514] pension_credit: [16, 0] winter_fuel_payment: [300, 0, 200, 0, 0, 0, 0, 0] winter_fuel_payment_charge: [0, 0, 200, 0, 0, 0, 0, 0] - income_tax: [6_288.51, 0, 6_186, 0, 0, 0, 0, 0] + income_tax: [6_943.51, 0, 6_186, 0, 0, 0, 0, 0] - name: 2025 net pay pension contributions come off total income # The model takes employee contributions as paid under a net pay diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 0a2751e0b0..40ddbe2ce9 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -22,7 +22,7 @@ legitimate way for income from capital to change the tax on counted income. Intended exception: earnings carry the extra tax they cause, including on - savings, dividends and rent (d1424, applied to all four tests). With + savings, dividends and rent, in all four tests. With earnings, income from capital can move that tax: earnings under the personal allowance can leave rent or dividends taxable. Where someone has earnings, each means-test income changes by exactly minus the change in @@ -47,12 +47,8 @@ legacy_means_test_income_tax, except where the nil floor on the tax on uncounted income binds. Within the basic rate band it is never below legacy_means_test_income_tax: earnings never lower the tax on income from - capital there. -6. Differential: for every generated family without self-employment income, - Income Support, Housing Benefit and the council tax reduction line deduct, - for each person they count, the same income tax as Pension Credit's tax - line: the pre-charge income tax less - legacy_means_test_uncounted_income_tax_without_earnings. + capital there. Each test floors the sum of the deduction over the members + it counts at nil. 5. Tariff: the council tax reduction tariff income that replaces the actual income from capital is never negative, never more than the yield at the 16,000 capital limit (40 steps of 250 at 4.35 a month, 2,088 a year), nil @@ -60,6 +56,16 @@ pension age, whose local schemes set their own), and never falls when savings rise while the family stays on the same route (pensioner, UC award, passport, guarantee credit, savings credit only). +6. Differential: for every generated family without self-employment income, + Income Support, Housing Benefit and the council tax reduction line deduct, + for each person they count, the same income tax as Pension Credit's tax + line: the pre-charge income tax less + legacy_means_test_uncounted_income_tax_without_earnings. +7. Applicant: where the household head does not apply alone for council tax + reduction, council_tax_reduction_home_letting_income equals + legacy_benefits_home_letting_income; where the head applies alone, her + council tax reduction income moves with her own rent from letting part of + the home and not with anyone else's. """ import numpy as np @@ -135,6 +141,36 @@ def families(draw): populations = st.lists(families(), min_size=1, max_size=4) +@st.composite +def taxed_adults(draw, age): + """Adults whose pensions use up the personal allowance and whose income + from capital reaches about 20,000, so that it bears tax without + earnings.""" + return dict( + age=age, + employment_income=0.0, + state_pension=draw(st.floats(0, 10_000)) if age >= 67 else 0.0, + private_pension_income=draw(st.floats(12_570, 20_000)), + property_income=draw(st.floats(0, 20_000)), + savings_interest_income=draw(st.floats(2_000, 20_000)), + dividend_income=draw(st.floats(0, 20_000)), + sublet_income=0.0, + ) + + +@st.composite +def taxed_families(draw): + family = draw(families()) + members = [ + (role, draw(taxed_adults(attrs["age"])) if role == "adult" else attrs) + for role, attrs in family["members"] + ] + return dict(family, members=members) + + +taxed_populations = st.lists(taxed_families(), min_size=1, max_size=4) + + def situation( units, year, capital_income_scale=1.0, sublet_extra=0.0, savings_extra=0.0 ): @@ -364,27 +400,23 @@ def test_legacy_means_test_income_tax_bounds(units, year): @PROPERTY_SETTINGS -@given(populations, st.sampled_from(YEARS)) +@given(taxed_populations, st.sampled_from(YEARS)) def test_deducted_tax_without_earnings_is_the_step_4_split(units, year): - no_earnings = [ - dict( - unit, - members=[ - (role, dict(attrs, employment_income=0.0) if role == "adult" else attrs) - for role, attrs in unit["members"] - ], - ) - for unit in units - ] + """Nobody has earnings, and every adult's interest exceeds the personal + savings allowance on top of a pension that uses up the personal + allowance, so the tax on uncounted income is positive and the deduction + must leave all of it out.""" v = calculate( - no_earnings, + units, year, [ "legacy_means_test_income_tax", "legacy_means_test_deducted_income_tax", + "legacy_means_test_uncounted_income_tax_without_earnings", "income_tax_before_winter_fuel_payment_charge", ], ) + assert (v["legacy_means_test_uncounted_income_tax_without_earnings"] > 0).any() # Equal unless the nil floor on the tax on uncounted income binds, where # the Step 4 split exceeds the pre-charge tax and the deduction is the # pre-charge tax. @@ -537,3 +569,139 @@ def route(values): more["council_tax_reduction_tariff_income"][same_route] >= tariff[same_route] - 0.01 ) + + +@PROPERTY_SETTINGS +@given(populations, st.sampled_from(YEARS)) +def test_council_tax_reduction_letting_income_matches_the_legacy_measure(units, year): + """7. Applicant. Where the household head does not apply alone, the + council tax reduction applicant and partner are the benefit unit's + claimant and partner, so council_tax_reduction_home_letting_income equals + legacy_benefits_home_letting_income. (The two use different pension-age + tests, but the working-age and pension-age disregards have been equal + since April 2008.)""" + v = calculate( + units, + year, + [ + "council_tax_reduction_head_applies_alone", + "council_tax_reduction_home_letting_income", + "legacy_benefits_home_letting_income", + ], + ) + family = v["council_tax_reduction_head_applies_alone"] == 0 + np.testing.assert_allclose( + v["council_tax_reduction_home_letting_income"][family], + v["legacy_benefits_home_letting_income"][family], + atol=0.01, + ) + + +# A household head who applies alone: a liable head sharing a benefit unit +# with a young couple flagged as parents and their baby, so the couple are the +# benefit unit's claimant and partner. +head_alone_household = st.fixed_dictionaries( + dict( + head_age=st.integers(40, 95), + parent_ages=st.tuples(st.integers(16, 17), st.integers(16, 17)), + head_pension=st.floats(0, 20_000), + head_wages=st.one_of(st.just(0.0), st.floats(0, 20_000)), + head_rent=st.one_of(st.just(0.0), st.floats(0, 10_000)), + parent_rent=st.one_of(st.just(0.0), st.floats(0, 10_000)), + region=st.sampled_from(REGIONS), + ) +) + + +def head_alone_situation(households, year, head_extra=0.0, parent_extra=0.0): + people, benunits, homes = {}, {}, {} + for i, h in enumerate(households): + head, mother, father, baby = (f"{r}{i}" for r in "hmfb") + people[head] = { + "age": {year: h["head_age"]}, + "is_household_head": {year: True}, + "private_pension_income": {year: h["head_pension"]}, + "employment_income": {year: h["head_wages"]}, + "sublet_income": {year: h["head_rent"] + head_extra}, + } + people[mother] = { + "age": {year: h["parent_ages"][0]}, + "is_parent": {year: True}, + "is_household_head": {year: False}, + "sublet_income": {year: h["parent_rent"] + parent_extra}, + } + people[father] = { + "age": {year: h["parent_ages"][1]}, + "is_parent": {year: True}, + "is_household_head": {year: False}, + } + people[baby] = {"age": {year: 0}, "is_household_head": {year: False}} + members = [head, mother, father, baby] + benunits[f"b{i}"] = { + "members": members, + "would_claim_uc": {year: False}, + "universal_credit": {year: 0.0}, + "tax_credits": {year: 0.0}, + } + homes[f"h{i}"] = { + "members": members, + "region": {year: h["region"]}, + "council_tax": {year: 1_500.0}, + "savings": {year: 0.0}, + } + return {"people": people, "benunits": benunits, "households": homes} + + +HEAD_ALONE_VARIABLES = [ + "council_tax_reduction_head_applies_alone", + "council_tax_reduction_pensioner", + "council_tax_reduction_home_letting_income", + "legacy_benefits_home_letting_income", + "council_tax_reduction_applicable_income", +] + + +def head_alone_values(households, year, **kwargs): + sim = Simulation(situation=head_alone_situation(households, year, **kwargs)) + return { + v: np.asarray(sim.calculate(v, year), dtype=float) for v in HEAD_ALONE_VARIABLES + } + + +@PROPERTY_SETTINGS +@given( + st.lists(head_alone_household, min_size=1, max_size=4), + st.sampled_from(YEARS), + st.floats(1, 5_000), +) +def test_a_head_applying_alone_counts_only_her_own_letting_income( + households, year, extra +): + """7. Applicant. Where the household head applies alone, she is the + council tax reduction applicant (SI 2012/2885 reg 2(1), Sch 1 para 11): + her income moves with her own rent from letting part of the home, by the + change in council_tax_reduction_home_letting_income wherever the scheme + counts it, and not with the benefit unit's claimant's or partner's.""" + base = head_alone_values(households, year) + assert np.all(base["council_tax_reduction_head_applies_alone"] == 1) + more_own = head_alone_values(households, year, head_extra=extra) + more_others = head_alone_values(households, year, parent_extra=extra) + # Scotland's scheme for people under pension age lists no such income + # (SSI 2021/249 reg 57(1)). + region = np.array([h["region"] for h in households]) + counts = ~((region == "SCOTLAND") & (base["council_tax_reduction_pensioner"] == 0)) + income = "council_tax_reduction_applicable_income" + letting = "council_tax_reduction_home_letting_income" + np.testing.assert_allclose( + more_own[income] - base[income], + np.where(counts, more_own[letting] - base[letting], 0), + atol=0.02, + ) + assert np.all(more_own[letting] >= base[letting] - 0.01) + np.testing.assert_allclose(more_others[income], base[income], atol=0.01) + np.testing.assert_allclose(more_others[letting], base[letting], atol=0.01) + # The couple's own legacy measure does count it. + assert np.all( + more_others["legacy_benefits_home_letting_income"] + >= base["legacy_benefits_home_letting_income"] - 0.01 + ) diff --git a/policyengine_uk/tests/test_property_income_properties.py b/policyengine_uk/tests/test_property_income_properties.py index 4fa3853c96..870d0524ed 100644 --- a/policyengine_uk/tests/test_property_income_properties.py +++ b/policyengine_uk/tests/test_property_income_properties.py @@ -381,8 +381,8 @@ def test_no_one_lowers_their_tax_by_switching_route_alone(couple_sim, drawn): # household. Each landlord with finance costs to relieve must still choose # the route that gives them the lower income tax including the charge, with # every other route held fixed. Landlords without finance costs take the -# allowance without comparing (uses_property_allowance); review r7's lone -# pensioner below shows that can cost more, an intended limit pinned by its +# allowance without comparing (uses_property_allowance); the lone pensioner +# with wages below shows that can cost more, an intended limit pinned by its # own test. PENSIONER_YEAR = 2025 PENSIONER_HOUSEHOLDS = 6 @@ -399,6 +399,7 @@ def test_no_one_lowers_their_tax_by_switching_route_alone(couple_sim, drawn): st.one_of(amount, st.sampled_from([3_300.0])), # profit st.floats(min_value=0, max_value=950), # expenses st.sampled_from([0.0, 0.0, 675.0, 1_200.0, 1_500.0]), # finance costs + st.sampled_from([0.0, 0.0, 27_000.0, 31_562.0, 37_047.55]), # wages ) pensioner_household = st.tuples( st.sampled_from([66, 70, 80, 85]), # claimant's age @@ -414,29 +415,33 @@ def test_no_one_lowers_their_tax_by_switching_route_alone(couple_sim, drawn): min_size=PENSIONER_HOUSEHOLDS, max_size=PENSIONER_HOUSEHOLDS, ) -NO_INCOME = (0.0, 0.0, 0.0, 0.0, 0.0) -# Review r7's cases (2026-10-16). A pensioner with six children and no -# finance costs: the allowance lowers tax before the charge by £160, which -# ends Pension Credit of £80 and so the exemption from the £200 charge. +NO_INCOME = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) +# A pensioner with wages and six children and no finance costs. Pension +# Credit counts no property profits (SPC Sch IV para 18), but without the +# wages the pension and profits fall within the personal allowance, so all +# the tax is the wages' and Pension Credit deducts it. The allowance lowers +# that tax by £160, which ends Pension Credit of £80 and so the exemption +# from the £200 charge. LONE_PENSIONER = ( 70, 10, 40, - (35_207.0, 0.0, 3_300.0, 200.0, 0.0), + (9_070.0, 0.0, 3_300.0, 200.0, 0.0, 31_562.0), NO_INCOME, NO_INCOME, False, ) -# A couple's Pension Credit, through the claimant's election, takes the +# A couple's Pension Credit (savings credit), through the claimant's +# election and the tax the claimant's wages cause on the profits, takes the # partner out of entitlement and raises the lodger's shared payment and # charge from £150 to £200; the lodger's own election must hold that fixed. SHARED_PAYMENT = ( 70, 85, 85, - (42_772.55, 0.0, 3_300.0, 200.0, 1_200.0), + (9_000.0, 0.0, 3_300.0, 200.0, 1_200.0, 37_047.55), NO_INCOME, - (40_000.0, 0.0, 3_300.0, 200.0, 675.0), + (40_000.0, 0.0, 3_300.0, 200.0, 675.0, 0.0), False, ) PENSIONER_SETTINGS = settings( @@ -458,6 +463,7 @@ def person(age): "property_income": {PENSIONER_YEAR: 0}, "property_rental_income": {PENSIONER_YEAR: 0}, "property_finance_costs": {PENSIONER_YEAR: 0}, + "employment_income": {PENSIONER_YEAR: 0}, } people, benunits, households = {}, {}, {} @@ -499,13 +505,14 @@ def set_pensioner_inputs(sim, drawn, uses_allowance=None): for age, landlord in zip(household_ages, landlords) ] + [NO_INCOME] * CHILDREN claims_pc += [True, claims] - pension, dividends, profit, expenses, costs = map(np.array, zip(*incomes)) + pension, dividends, profit, expenses, costs, wages = map(np.array, zip(*incomes)) sim.set_input("age", PENSIONER_YEAR, np.array(ages, dtype=float)) sim.set_input("private_pension_income", PENSIONER_YEAR, pension) sim.set_input("dividend_income", PENSIONER_YEAR, dividends) sim.set_input("property_income", PENSIONER_YEAR, profit) sim.set_input("property_rental_income", PENSIONER_YEAR, profit + expenses) sim.set_input("property_finance_costs", PENSIONER_YEAR, costs) + sim.set_input("employment_income", PENSIONER_YEAR, wages) sim.set_input("would_claim_pc", PENSIONER_YEAR, np.array(claims_pc)) if uses_allowance is None: sim.delete_arrays("uses_property_allowance", PENSIONER_YEAR) @@ -552,13 +559,14 @@ def test_no_pensioner_lowers_their_tax_and_charge_by_switching_alone( def test_a_landlord_without_finance_costs_takes_the_allowance_even_where_it_costs_more( pensioner_sim, ): - """Intended violation: review r7's lone pensioner (no finance costs). - - The allowance lowers tax before the charge by £160 but ends £80 of Pension - Credit and with it the exemption from the £200 charge, so expenses would - give the lower income tax (£5,187.40 against £5,227.40). The model does - not compare routes for landlords without finance costs, so it takes the - allowance. + """Intended violation: a lone pensioner with wages (no finance costs). + + Without the wages, the pension and profits fall within the personal + allowance, so Pension Credit deducts the whole tax as the wages'. The + allowance lowers that tax by £160 but ends £80 of Pension Credit and with + it the exemption from the £200 charge, so expenses would give the lower + income tax (£6,272.40 against £6,312.40). The model does not compare + routes for landlords without finance costs, so it takes the allowance. """ sim = pensioner_sim drawn = [LONE_PENSIONER] * PENSIONER_HOUSEHOLDS @@ -573,10 +581,10 @@ def test_a_landlord_without_finance_costs_takes_the_allowance_even_where_it_cost routes = sim.calculate("uses_property_allowance", PENSIONER_YEAR) income_tax = sim.calculate("income_tax", PENSIONER_YEAR).astype(float) assert routes[claimants].all() - assert income_tax[pinned] == pytest.approx(5_227.40, abs=0.01) + assert income_tax[pinned] == pytest.approx(6_312.40, abs=0.01) set_pensioner_inputs(sim, drawn, routes & ~claimants) with_expenses = sim.calculate("income_tax", PENSIONER_YEAR).astype(float) - assert with_expenses[pinned] == pytest.approx(5_187.40, abs=0.01) + assert with_expenses[pinned] == pytest.approx(6_272.40, abs=0.01) # Reviews r1 and r2 of #2695. winter_fuel_payment_charge_ceiling bounds the diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 13099d61c8..f4edde7864 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -107,8 +107,15 @@ def hb_income_before_deductions(benunit, period, parameters): # Tax only on the income taken into account (HB Regs 2006 Sch 5 para 1; # HB (SPC) Regs 2006 reg 33(12)), before the winter fuel payment charge, # with earnings carrying the extra tax they cause. - TAX_COMPONENTS = ["legacy_means_test_deducted_income_tax", "national_insurance"] - tax = add_for_members(benunit, period, TAX_COMPONENTS, members) + # The deduction can be negative for one person, where another + # member's earnings move a Step 7 charge off them; the test deducts the + # counted members' sum, floored at nil, so it never adds tax to income. + tax = max_( + 0, + add_for_members( + benunit, period, ["legacy_means_test_deducted_income_tax"], members + ), + ) + add_for_members(benunit, period, ["national_insurance"], members) increased_income_reduced_by_tax_and_pensions = ( increased_income - tax - pension_contributions ) diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index 445aef1bee..2a2783a629 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -68,12 +68,15 @@ def formula(benunit, period, parameters): if bi.interactions.include_in_means_tests: INCOME_COMPONENTS.append("basic_income") income = add_for_members(benunit, period, INCOME_COMPONENTS, members) - tax = add_for_members( - benunit, - period, - ["legacy_means_test_deducted_income_tax", "national_insurance"], - members, - ) + # The deduction can be negative for one person, where another + # member's earnings move a Step 7 charge off them; the test deducts the + # counted members' sum, floored at nil, so it never adds tax to income. + tax = max_( + 0, + add_for_members( + benunit, period, ["legacy_means_test_deducted_income_tax"], members + ), + ) + add_for_members(benunit, period, ["national_insurance"], members) income += add_for_members(benunit, period, ["social_security_income"], members) # IS Regs 1987 Sch 9 para 16(h): £10 a week of widowed parent's # allowance is disregarded; social_security_income counts it in full. diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py index d632fe3af4..14d4324a0c 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py @@ -25,7 +25,11 @@ class legacy_means_test_deducted_income_tax(Variable): "It is not floored: where one partner's earnings move a Step 7 " "charge or the marriage allowance off the other partner, that " "partner's value can be negative, and the claimant's and partner's " - "values together give the extra tax the earnings cause. Pension " + "values together give the extra tax the earnings cause. Each test " + "sums it over the members it counts and floors that sum at nil, so " + "where the earner is a member the test does not count (for example " + "one the model presumes to be the claimant's child), the counted " + "member's negative value adds nothing to income. Pension " "Credit replaces the tax on self-employed profit with notional tax " "(pension_credit_income); the other tests deduct this tax as it is." ) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py index 3ddf9dcb24..b357b84891 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_income_tax.py @@ -3,9 +3,9 @@ DECISION = ( "PolicyEngine does not model PAYE codes, so which part of a person's " - "income tax is payable on their earnings is a methodology choice, put to " - "Max on PolicyEngine/policyengine-uk#2019 and settled as option (a): treat " - "earnings as the top slice of the person's income. " + "income tax is payable on their earnings is a methodology choice " + "(PolicyEngine/policyengine-uk#2019): earnings are treated as the top " + "slice of the person's income. " ) # Income tax here is before the winter fuel payment charge, as in every means diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index 7405a0ed62..85a9c3c740 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -57,21 +57,34 @@ def formula(benunit, period, parameters): # notional tax and contributions of the Computation of Earnings # Regulations 1996 reg. 14, in place of those actually payable on the # profit (reg. 17(11)). - tax = benunit.sum( - ( - add( - person, - period, - [ - "legacy_means_test_deducted_income_tax", - "national_insurance", - "pension_credit_self_employment_deductions", - ], + # The deduction can be negative for one person, where another + # member's earnings move a Step 7 charge off them; the test deducts the + # counted members' sum, floored at nil, so it never adds tax to income. + income_tax = max_( + 0, + benunit.sum( + person("legacy_means_test_deducted_income_tax", period) + * is_claimant_or_partner + ), + ) + tax = ( + income_tax + + benunit.sum( + ( + add( + person, + period, + [ + "national_insurance", + "pension_credit_self_employment_deductions", + ], + ) + - add(person, period, ["ni_class_2", "ni_class_4"]) ) - - add(person, period, ["ni_class_2", "ni_class_4"]) + * is_claimant_or_partner ) - * is_claimant_or_partner - ) - benunit("pension_credit_self_employment_income_tax", period) + - benunit("pension_credit_self_employment_income_tax", period) + ) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate # Schedule VI earnings disregards (reg. 17(9)). diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py index d7c264d120..d0ac6c8708 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py @@ -137,8 +137,11 @@ class uses_property_allowance(Variable): "allowance is used whenever it lowers taxable property income, " "without comparing tax. That is always the lower tax before the " "winter fuel payment charge. While the charge has effect it can " - "rarely cost more: the lower tax can raise the income Pension Credit " - "counts and end an award that exempts the person from the charge. " + "rarely cost more: where the person has earnings, Pension Credit " + "deducts the tax they cause, including tax on property profits that " + "would fall within the allowances without them, so the lower tax can " + "raise the income Pension Credit counts and end an award that " + "exempts the person from the charge. " "The model does not compare routes for these landlords, because " "that works out the benefits on both routes for the whole " "population and made simulations several times slower; on the " @@ -177,8 +180,9 @@ def formula(person, period, parameters): # Without a reduction at stake, lower taxable property income means # lower tax before the winter fuel payment charge, so the allowance # is used without comparing. The charge can rarely reverse that - # (lower tax can end Pension Credit and with it the exemption, - # s.681I(4)); see the documentation for why that is not compared. + # (where earnings carry the tax on the profits, lower tax can end + # Pension Credit and with it the exemption, s.681I(4)); see the + # documentation for why that is not compared. uses = lowers_income & ~reduction_at_stake choosing = lowers_income & reduction_at_stake if not choosing.any(): diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 9101704ce4..90d36dd6c3 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -351,12 +351,15 @@ def formula(benunit, period, parameters): pension_contributions = add_for_members( benunit, period, ["pension_contributions"], members ) * benunit("council_tax_reduction_pension_contribution_deduction_rate", period) - tax = add_for_members( - benunit, - period, - ["legacy_means_test_deducted_income_tax", "national_insurance"], - members, - ) + # The deduction can be negative for one person, where another + # member's earnings move a Step 7 charge off them; the test deducts the + # counted members' sum, floored at nil, so it never adds tax to income. + tax = max_( + 0, + add_for_members( + benunit, period, ["legacy_means_test_deducted_income_tax"], members + ), + ) + add_for_members(benunit, period, ["national_insurance"], members) # SI 2012/2885 Sch 1 para 17(9) and Sch 4; WSI 2013/3029 Sch 1 para # 11(9) and Sch 3; SSI 2012/319 reg 31(8) and Sch 2: the pensioner # earnings disregards, capped at net earnings. Zero for working-age From 0346ada0df1c92c5c535dda4faf116b594f7759a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 07:22:26 +0000 Subject: [PATCH 21/24] Draw interest above the savings starting rate in the no-earnings property Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KuHbGxemvogNVtTue --- ...egacy_means_test_income_from_capital_properties.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 40ddbe2ce9..0075c07626 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -152,7 +152,7 @@ def taxed_adults(draw, age): state_pension=draw(st.floats(0, 10_000)) if age >= 67 else 0.0, private_pension_income=draw(st.floats(12_570, 20_000)), property_income=draw(st.floats(0, 20_000)), - savings_interest_income=draw(st.floats(2_000, 20_000)), + savings_interest_income=draw(st.floats(7_000, 20_000)), dividend_income=draw(st.floats(0, 20_000)), sublet_income=0.0, ) @@ -402,10 +402,11 @@ def test_legacy_means_test_income_tax_bounds(units, year): @PROPERTY_SETTINGS @given(taxed_populations, st.sampled_from(YEARS)) def test_deducted_tax_without_earnings_is_the_step_4_split(units, year): - """Nobody has earnings, and every adult's interest exceeds the personal - savings allowance on top of a pension that uses up the personal - allowance, so the tax on uncounted income is positive and the deduction - must leave all of it out.""" + """Nobody has earnings, and every adult's interest of at least 7,000 + exceeds the 5,000 starting rate for savings and the 1,000 personal savings + allowance on top of a pension that uses up the personal allowance, so the + tax on uncounted income is positive and the deduction must leave all of + it out.""" v = calculate( units, year, From 1919788159e18ce20e29d815c1d8af85d4894b5e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 07:39:33 +0000 Subject: [PATCH 22/24] Say in the changelog that council tax reduction reads its applicant's letting rent Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KuHbGxemvogNVtTue --- changelog.d/legacy-capital-derived-income.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/legacy-capital-derived-income.fixed.md b/changelog.d/legacy-capital-derived-income.fixed.md index 2cfe6e50cd..45ca3775f3 100644 --- a/changelog.d/legacy-capital-derived-income.fixed.md +++ b/changelog.d/legacy-capital-derived-income.fixed.md @@ -1 +1 @@ -Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard, except where a working-age council tax reduction applicant's income is the Universal Credit assessment. The residential finance-cost tax reduction now comes off the tax on property income. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale and less any debt secured on it; adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland); uses the Universal Credit assessment of capital for a working-age applicant with an award of Universal Credit, including one the benefit cap or deductions reduce to nil, where the scheme says so; and disregards the whole capital of an applicant on Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance in Wales, in Scotland before April 2022, and in the Kingston, Merton, Westminster and Oxford schemes. All four tests deduct the same income tax: the tax before the winter fuel payment charge, less the tax that rent, interest and dividends, and any Step 7 charge, would bear if nobody had earnings, so earnings carry the extra tax they cause. +Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard, except where a working-age council tax reduction applicant's income is the Universal Credit assessment. The residential finance-cost tax reduction now comes off the tax on property income. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale and less any debt secured on it; adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland); uses the Universal Credit assessment of capital for a working-age applicant with an award of Universal Credit, including one the benefit cap or deductions reduce to nil, where the scheme says so; and disregards the whole capital of an applicant on Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance in Wales, in Scotland before April 2022, and in the Kingston, Merton, Westminster and Oxford schemes. All four tests deduct the same income tax: the tax before the winter fuel payment charge, less the tax that rent, interest and dividends, and any Step 7 charge, would bear if nobody had earnings, so earnings carry the extra tax they cause. Council tax reduction counts the rent its own applicant and partner receive for letting part of the home, so a household head who applies alone counts her own rent and not the benefit unit's claimant's, and keeps her tariff income when they receive Pension Credit; each test floors the sum of the deducted income tax over the members it counts at nil. From 05939be58a9ee3deb6ecd9f3cb12a7a7cc0c91f9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 14:07:35 +0000 Subject: [PATCH 23/24] Floor Pension Credit's counted tax after taking off the actual tax on self-employed profit Pension Credit floored the counted members' deduction at nil before it took off the tax actually payable on self-employed profit, which its notional tax replaces, so its tax line could still turn negative where an uncounted member's pay moved a Child Benefit charge off a counted member with profit. The formula now takes that tax out of the sum before the floor. A YAML case pins the household: Pension Credit income 960, not 1,803. The documentation and changelog say so, and the documentation says that the floor binds only where the sum turns negative. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HUt6Xxx55oAoWZeeW --- .../legacy-capital-derived-income.fixed.md | 2 +- .../income_tax_caused_by_earnings.yaml | 41 +++++++++++++++++++ .../legacy_means_test_deducted_income_tax.py | 11 +++-- .../pension_credit/pension_credit_income.py | 35 ++++++++-------- 4 files changed, 67 insertions(+), 22 deletions(-) diff --git a/changelog.d/legacy-capital-derived-income.fixed.md b/changelog.d/legacy-capital-derived-income.fixed.md index 45ca3775f3..ceb3702b10 100644 --- a/changelog.d/legacy-capital-derived-income.fixed.md +++ b/changelog.d/legacy-capital-derived-income.fixed.md @@ -1 +1 @@ -Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard, except where a working-age council tax reduction applicant's income is the Universal Credit assessment. The residential finance-cost tax reduction now comes off the tax on property income. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale and less any debt secured on it; adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland); uses the Universal Credit assessment of capital for a working-age applicant with an award of Universal Credit, including one the benefit cap or deductions reduce to nil, where the scheme says so; and disregards the whole capital of an applicant on Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance in Wales, in Scotland before April 2022, and in the Kingston, Merton, Westminster and Oxford schemes. All four tests deduct the same income tax: the tax before the winter fuel payment charge, less the tax that rent, interest and dividends, and any Step 7 charge, would bear if nobody had earnings, so earnings carry the extra tax they cause. Council tax reduction counts the rent its own applicant and partner receive for letting part of the home, so a household head who applies alone counts her own rent and not the benefit unit's claimant's, and keeps her tariff income when they receive Pension Credit; each test floors the sum of the deducted income tax over the members it counts at nil. +Stop counting rent from property, and deducting tax on rent, interest and dividends, in the Income Support, Housing Benefit, Pension Credit and council tax reduction means tests, which treat income derived from capital as capital; count rent for letting part of the home less the £20 weekly sub-tenant disregard, except where a working-age council tax reduction applicant's income is the Universal Credit assessment. The residential finance-cost tax reduction now comes off the tax on property income. Council tax reduction now counts land and property other than the home as capital, at market value less 10% for the expenses of sale and less any debt secured on it; adds the national schemes' tariff income from capital (England for pensioners, Wales and Scotland); uses the Universal Credit assessment of capital for a working-age applicant with an award of Universal Credit, including one the benefit cap or deductions reduce to nil, where the scheme says so; and disregards the whole capital of an applicant on Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance in Wales, in Scotland before April 2022, and in the Kingston, Merton, Westminster and Oxford schemes. All four tests deduct the same income tax: the tax before the winter fuel payment charge, less the tax that rent, interest and dividends, and any Step 7 charge, would bear if nobody had earnings, so earnings carry the extra tax they cause. Council tax reduction counts the rent its own applicant and partner receive for letting part of the home: where a household head applies alone, it counts her rent and not the benefit unit's claimant's and partner's, and it counts her tariff income even when the claimant and partner receive Pension Credit. Each test floors at nil the sum of the deducted income tax over the members it counts, and Pension Credit first takes out the tax on self-employed profit that its notional tax replaces. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml index b10e7830a0..9366386aa4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml @@ -325,3 +325,44 @@ income_support: [0] housing_benefit_pension_age_regulations_apply: [false] housing_benefit_applicable_income: [0] + +- name: Pension Credit floors the counted tax after taking off the actual tax on self-employed profit + # The floor cases' household, with a aged 70 and a self-employed profit of + # 2,000. The model presumes b, 26 years younger, to be a's child, so + # Pension Credit counts a alone. b's pay moves the 703 Child Benefit charge + # off a, so a's deduction is the 800 of tax the profit adds less 703: 97. + # The profit's actual tax (pension_credit_self_employment_income_tax) is + # 940: the 800, and the 140 it adds to the charge when b has no pay (843, + # 60% of 1,406 rounded down, against 703). The formula replaces that tax + # with notional tax, nil on 2,000, and deducts no other tax for a: + # max(0, 97 - 940) = 0. Income is the profit less the £20 weekly lone + # parent disregard: 2,000 - 1,040 = 960. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + a: + age: 70 + savings_interest_income: 70_000 + self_employment_income: 2_000 + b: + age: 44 + employment_income: 80_000 + child: + age: 3 + benunits: + benunit: + members: [a, b, child] + would_claim_uc: false + households: + household: + members: [a, b, child] + country: ENGLAND + tenure_type: RENT_PRIVATELY + rent: 9_000 + council_tax: 1_500 + output: + is_claimant_or_partner: [true, false, false] + legacy_means_test_deducted_income_tax: [97, 20_838, 0] + pension_credit_self_employment_income_tax: [940] + pension_credit_income: [960] diff --git a/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py b/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py index 14d4324a0c..258e08e62b 100644 --- a/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py +++ b/policyengine_uk/variables/gov/dwp/legacy_means_test_deducted_income_tax.py @@ -29,9 +29,14 @@ class legacy_means_test_deducted_income_tax(Variable): "sums it over the members it counts and floors that sum at nil, so " "where the earner is a member the test does not count (for example " "one the model presumes to be the claimant's child), the counted " - "member's negative value adds nothing to income. Pension " - "Credit replaces the tax on self-employed profit with notional tax " - "(pension_credit_income); the other tests deduct this tax as it is." + "member's negative value adds nothing to income. The floor binds " + "only where the sum turns negative. Where the counted member also " + "pays tax on counted income, the charge that the uncounted member's " + "earnings move off them still lowers the deduction, and the test's " + "income rises by up to that charge. Pension Credit takes the tax on " + "self-employed profit out of the sum before the floor and adds " + "notional tax in its place (pension_credit_income); the other tests " + "deduct this tax as it is." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index 85a9c3c740..6580a3a3fa 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -58,32 +58,31 @@ def formula(benunit, period, parameters): # Regulations 1996 reg. 14, in place of those actually payable on the # profit (reg. 17(11)). # The deduction can be negative for one person, where another - # member's earnings move a Step 7 charge off them; the test deducts the - # counted members' sum, floored at nil, so it never adds tax to income. + # member's earnings move a Step 7 charge off them. The test takes the + # counted members' sum less the tax actually payable on self-employed + # profit, which the notional tax replaces, and floors that at nil, so + # it never adds tax to income. income_tax = max_( 0, benunit.sum( person("legacy_means_test_deducted_income_tax", period) * is_claimant_or_partner - ), + ) + - benunit("pension_credit_self_employment_income_tax", period), ) - tax = ( - income_tax - + benunit.sum( - ( - add( - person, - period, - [ - "national_insurance", - "pension_credit_self_employment_deductions", - ], - ) - - add(person, period, ["ni_class_2", "ni_class_4"]) + tax = income_tax + benunit.sum( + ( + add( + person, + period, + [ + "national_insurance", + "pension_credit_self_employment_deductions", + ], ) - * is_claimant_or_partner + - add(person, period, ["ni_class_2", "ni_class_4"]) ) - - benunit("pension_credit_self_employment_income_tax", period) + * is_claimant_or_partner ) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate From 7ba0ea7afd9352778c3bc0bf060eee84c784aa7a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 14:17:23 +0000 Subject: [PATCH 24/24] Correct the landlord limit, mask the bounds property, and pin the head-alone cases - winter_fuel_payment_charge.yaml and uses_property_allowance: the property route moves the tax Pension Credit deducts without earnings too, through a shared tax reduction, the Marriage Allowance or the personal allowance taper. The no-earnings case names what it holds, and a twin pins the expenses route. - The bounds property counts only the claimant's and partner's rent and ages, and pins a couple 23 years apart, where the younger lets. - The head-alone property pins a Scottish working-age head, checks her letting income turns positive above the disregard, and checks the couple's legacy measure moves by the parent's extra rent. - A YAML case pins a head's tariff income beside a couple on savings credit only. - Wording: actors in the landlord cases and tests, and no fix history. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HUt6Xxx55oAoWZeeW --- .../income_from_capital.yaml | 60 ++++++- .../income_tax_caused_by_earnings.yaml | 13 +- .../charges/winter_fuel_payment_charge.yaml | 160 ++++++++++++------ ...ans_test_income_from_capital_properties.py | 100 +++++++++-- .../tests/test_property_income_properties.py | 4 +- .../allowances/uses_property_allowance.py | 21 ++- 6 files changed, 265 insertions(+), 93 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml index a4dcba374b..e080bdba36 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_from_capital.yaml @@ -1888,12 +1888,12 @@ # applicant (SI 2012/2885 reg 2(1)), so her own income counts (Sch 1 para 11), # including rent for letting part of her home (Sch 1 para 16(1)(v), less £20 a # week under Sch 5 para 10), and the benefit unit's claimant's and partner's -# does not. In each case below the head, aged 70, reached State Pension age -# after 1 April 2021, so her 2026-27 personal allowance is £238 a week: -# 238 x 52 = 12,376. Maidstone runs England's pensioner scheme: maximum 100% -# of the £2,000 council tax, withdrawn at 20% of income above the applicable -# amount. Her £12,000 private pension falls within the £12,570 personal -# allowance, and the model taxes none of the rent: income tax nil. +# does not. In the first three cases below the head, aged 70, reached State +# Pension age after 1 April 2021, so her 2026-27 personal allowance is £238 a +# week: 238 x 52 = 12,376. Maidstone runs England's pensioner scheme: +# maximum 100% of the £2,000 council tax, withdrawn at 20% of income above the +# applicable amount. Her £12,000 private pension falls within the £12,570 +# personal allowance, and the model taxes none of the rent: income tax nil. - name: A household head applying alone counts her own rent from letting part of the home in council tax reduction # Rent 5,200 / 52 = 100 a week, less 20 = 80; 80 x 52 = 4,160. @@ -2081,3 +2081,51 @@ council_tax_reduction_applicable_income: [13_330] council_tax_reduction_applicable_amount: [13_312] council_tax_reduction: 1_996.40 + +- name: A household head applying alone keeps her tariff income when the couple she lives with gets savings credit only + # As the guarantee credit case, but the couple are 80 and 78 and the elder + # has a £20,000 private pension, so the model gives them savings credit of + # £928 a year and no guarantee credit. That award is theirs, so the head + # keeps her tariff income. The head, aged 90, reached State Pension age + # before 1 April 2021, so her allowance is 256 x 52 = 13,312. Tariff + # 8 x 52 = 416; tax (13,000 - 12,570) x 20% = 86; income 13,000 - 86 + 416 + # = 13,330. CTR 2,000 - 20% x (13,330 - 13,312) = 1,996.40. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 90 + is_household_head: true + private_pension_income: 13_000 + older_partner: + age: 80 + is_parent: true + is_household_head: false + private_pension_income: 20_000 + younger_partner: + age: 78 + is_parent: true + is_household_head: false + benunits: + family: + members: [head, older_partner, younger_partner] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + home: + members: [head, older_partner, younger_partner] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 14_000 + output: + is_claimant_or_partner: [false, true, true] + council_tax_reduction_head_applies_alone: [true] + in_receipt_of_guarantee_credit: [false] + in_receipt_of_savings_credit_only: [true] + council_tax_reduction_assessable_capital: [14_000] + council_tax_reduction_tariff_income: [416] + council_tax_reduction_applicable_income: [13_330] + council_tax_reduction_applicable_amount: [13_312] + council_tax_reduction: 1_996.40 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml index 9366386aa4..9a50ebbd02 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/legacy_means_tests/income_tax_caused_by_earnings.yaml @@ -289,12 +289,13 @@ council_tax_reduction_applicable_income: [1_406.60] - name: Housing Benefit's means test floors the counted member's negative deduction at nil - # The same household with a child aged 5, so no test passports it: it - # receives no Income Support (income_support 0), and Housing Benefit takes - # income through its own means test. Housing Benefit counts none of a's - # interest (reg 46(4)) and excludes Child Benefit, so a's income is nil; the - # sum of the deduction over the members it counts, -703, floors at nil: - # income 0, not 703. + # The same household with a child aged 5. It reports no Income Support + # award, which income_support_eligible requires, so it gets none + # (income_support 0), and it claims no Universal Credit, so Housing Benefit + # takes income through its own means test. Housing Benefit counts none of + # a's interest (reg 46(4)) and excludes Child Benefit, so a's income is + # nil; the sum of the deduction over the members it counts, -703, floors at + # nil: income 0, not 703. period: 2026 absolute_error_margin: 0.01 input: diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml index 50ba903fac..35bb1e0ff4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml @@ -764,20 +764,20 @@ # With expenses: (39,370 - 12,570) x 20% = £5,360, less the finance-cost # reduction 20% x £1,500 = £300, so £5,060. With partial relief (£1,000 # allowance, no reduction): (38,570 - 12,570) x 20% = £5,200. Total income - # is over £35,000 either way (£39,370 and £38,570), so Pension Credit + # exceeds £35,000 on both routes (£39,370 and £38,570), so Pension Credit # decides the charge. - # Pension Credit no longer counts the property profits (SPC Sch IV para - # 18), but it deducts the tax the wages cause. Without the wages, pension - # and profits (£12,370 or £11,570) fall within the personal allowance and - # bear no tax, so the whole tax is the wages', and Pension Credit deducts - # it all: wages and pension less tax less the £20 weekly lone parent - # earnings disregard (£1,040). Pension Credit income is 36,070 - 5,060 - - # 1,040 = £29,970 with expenses and 36,070 - 5,200 - 1,040 = £29,830 with - # the allowance, against the model's minimum guarantee of £29,893.76 (with - # the amounts for the children). Only the allowance route gets Pension - # Credit (£63.76), which exempts the pensioner from the charge - # (s.681I(4)). Total tax: £5,060 + £200 = £5,260 with expenses, £5,200 - # with the allowance, so the pensioner elects for partial relief. + # The formula counts no property profits in Pension Credit income (SPC Sch + # IV para 18), but it deducts the tax the wages cause. Without the wages, + # pension and profits (£12,370 or £11,570) fall within the personal + # allowance and bear no tax, so the wages cause the whole tax, and Pension + # Credit deducts it all: wages and pension less tax less the £20 weekly + # lone parent earnings disregard (£1,040). Pension Credit income is + # 36,070 - 5,060 - 1,040 = £29,970 with expenses and 36,070 - 5,200 - + # 1,040 = £29,830 with the allowance, against the model's minimum guarantee of + # £29,893.76 (with the amounts for the children). Only the allowance route + # gets Pension Credit (£63.76), which exempts the pensioner from the charge + # (s.681I(4)). Total tax: £5,060 + £200 = £5,260 with expenses, £5,200 with + # the allowance, so the pensioner elects for partial relief. period: 2025 absolute_error_margin: 0.01 input: @@ -821,28 +821,31 @@ - name: 2025 a landlord without finance costs takes the allowance even where it ends Pension Credit (a model limit) # Wages £31,562; private pension £9,070; property receipts £3,500, expenses # £200 (profit £3,300), no finance costs; six children. A pensioner pays no - # employee National Insurance. With partial relief (the £1,000 allowance - # in place of £200 of expenses): total income £43,132, tax (43,132 - - # 12,570) x 20% = £6,112.40. With expenses: total income £43,932, tax - # (43,932 - 12,570) x 20% = £6,272.40. Total income is over £35,000 either - # way, so Pension Credit decides the charge. - # Pension Credit no longer counts the property profits (SPC Sch IV para - # 18), but without the wages, pension and profits (£11,570 or £12,370) - # fall within the personal allowance, so the whole tax is the wages', and - # Pension Credit deducts it all. Pension Credit income (wages and pension, - # less that tax, less the £20 weekly lone parent earnings disregard of - # £1,040) is 40,632 - 6,112.40 - 1,040 = £33,479.60 with the allowance and - # 40,632 - 6,272.40 - 1,040 = £33,319.60 with expenses, against the - # model's minimum guarantee of £33,399.60 (with the amounts for the - # children). Only the expenses route gets Pension Credit (£80), which + # employee National Insurance. With partial relief (the £1,000 allowance in + # place of £200 of expenses): total income £43,132, tax (43,132 - 12,570) x + # 20% = £6,112.40. With expenses: total income £43,932, tax (43,932 - + # 12,570) x 20% = £6,272.40. Total income exceeds £35,000 on both routes, + # so Pension Credit decides the charge. + # The formula counts no property profits in Pension Credit income (SPC Sch + # IV para 18), but without the wages, pension and profits (£11,570 or + # £12,370) fall within the personal allowance, so the wages cause the whole + # tax, and Pension Credit deducts it all. Pension Credit income (wages and + # pension, less that tax, less the £20 weekly lone parent earnings + # disregard of £1,040) is 40,632 - 6,112.40 - 1,040 = £33,479.60 with the + # allowance and 40,632 - 6,272.40 - 1,040 = £33,319.60 with expenses, + # against the model's minimum guarantee of £33,399.60 (with the amounts for + # the children). Only the expenses route gets Pension Credit (£80), which # exempts the pensioner from the £200 charge (s.681I(4)). Total tax: # £6,312.40 with the allowance, £6,272.40 with expenses, so a pensioner - # choosing the lower tax would not elect for partial relief. The model - # does not compare routes for landlords without finance costs + # choosing the lower tax would not elect for partial relief. The model does + # not compare routes for landlords without finance costs # (uses_property_allowance), so it takes the allowance: no Pension Credit, - # charged the £200. This case pins that limit, which survives only where - # the pensioner has earnings: without them, the property route does not - # move the tax Pension Credit deducts. + # charged the £200. This case pins that limit. The wages carry the whole + # tax on the profits here, so the route moves the tax the formula deducts + # by the full £160. Without earnings the route still moves that tax where + # the profits change the tax on counted income: through its share of a + # general tax reduction, through the Marriage Allowance or through the + # personal allowance taper. period: 2025 absolute_error_margin: 0.01 input: @@ -883,17 +886,18 @@ winter_fuel_payment_charge: [200, 0, 0, 0, 0, 0, 0] income_tax: [6_312.40, 0, 0, 0, 0, 0, 0] -- name: 2025 without earnings the property route does not move a landlord's Pension Credit - # The same pensioner and six children with no wages and a private pension - # of £40,632. Without earnings, Pension Credit deducts only the Step 4 tax - # on the pension, (40,632 - 12,570) x 20% = £5,612.40, whatever the - # property route: the profits sit on top of the pension and bear their own - # tax. Pension Credit income is 40,632 - 5,612.40 = £35,019.60 on both - # routes, above the minimum guarantee of £33,399.60, so no Pension Credit - # either way. Total income exceeds £35,000 on both routes, so the £200 - # charge applies either way, and the allowance, which gives the lower tax - # before the charge, gives the lower income tax: (43,132 - 12,570) x 20% + - # 200 = £6,312.40. +- name: 2025 a single landlord without earnings, tax reductions or a tapered personal allowance has the same Pension Credit income on both routes + # The same pensioner and six children with no wages, no tax reduction and a + # private pension of £40,632. Without earnings, the formula deducts only + # the Step 4 tax on the pension, (40,632 - 12,570) x 20% = £5,612.40, on + # both property routes: the profits sit on top of the pension and bear + # their own tax, and no tax reduction, Marriage Allowance or allowance + # taper ties them to the tax on the pension. Pension Credit income is + # 40,632 - 5,612.40 = £35,019.60 on both routes, which exceeds the minimum + # guarantee of £33,399.60, so neither route gets Pension Credit. Total + # income exceeds £35,000 on both routes, so the £200 charge applies either + # way, and the allowance, which gives the lower tax before the charge, + # gives the lower income tax: (43,132 - 12,570) x 20% + 200 = £6,312.40. period: 2025 absolute_error_margin: 0.01 input: @@ -932,6 +936,50 @@ winter_fuel_payment_charge: [200, 0, 0, 0, 0, 0, 0] income_tax: [6_312.40, 0, 0, 0, 0, 0, 0] +- name: 2025 the same single landlord on expenses has the same Pension Credit income + # The case above with the expenses route as an input: total income £43,932, + # tax before the charge (43,932 - 12,570) x 20% = £6,272.40. The formula + # still deducts only the £5,612.40 on the pension, so Pension Credit income + # is £35,019.60 on both routes, and income tax with the £200 charge is + # £6,472.40. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + private_pension_income: 40_632 + property_rental_income: 3_500 + property_income: 3_300 + uses_property_allowance: false + child_1: + age: 10 + child_2: + age: 10 + child_3: + age: 10 + child_4: + age: 10 + child_5: + age: 10 + child_6: + age: 10 + benunits: + unit: + members: [pensioner, child_1, child_2, child_3, child_4, child_5, child_6] + would_claim_pc: true + would_claim_uc: false + households: + household: + members: [pensioner, child_1, child_2, child_3, child_4, child_5, child_6] + country: ENGLAND + output: + legacy_means_test_deducted_income_tax: [5_612.40, 0, 0, 0, 0, 0, 0] + pension_credit_income: 35_019.60 + pension_credit: 0 + winter_fuel_payment_charge: [200, 0, 0, 0, 0, 0, 0] + income_tax: [6_472.40, 0, 0, 0, 0, 0, 0] + - name: 2025 landlords in two benefit units of one household each elect with the other's route fixed # One household. Unit A: a claimant aged 70 with wages £37,047.55 and # private pension £9,000, a partner aged 85 with no income, and five @@ -945,22 +993,22 @@ # reduction 20% x £1,260 = £252, so £6,943.51. With expenses, total income # £49,347.55 and tax (49,347.55 - 12,570) x 20% = £7,355.51, less £252 and # the finance-cost reduction 20% x £1,200 = £240, so £6,863.51. - # Pension Credit no longer counts the property profits (SPC Sch IV para - # 18). Without the wages, A's pension and profits (£11,500 or £12,300) - # fall within the personal allowance and bear no tax, so the whole tax is - # the wages', and Pension Credit deducts it all. Its income, less the £10 - # weekly couple earnings disregard (£520), is 46,047.55 - 6,943.51 - 520 = - # £38,584.04 with the allowance and 46,047.55 - 6,863.51 - 520 = + # The formula counts no property profits in Pension Credit income (SPC Sch + # IV para 18). Without the wages, A's pension and profits (£11,500 or + # £12,300) fall within the personal allowance and bear no tax, so the wages + # cause the whole tax, and Pension Credit deducts it all. Its income, less + # the £10 weekly couple earnings disregard (£520), is 46,047.55 - + # 6,943.51 - 520 = £38,584.04 with the allowance and 46,047.55 - 6,863.51 - 520 = # £38,664.04 with expenses. Both exceed the model's minimum guarantee of - # £36,107.76 (with the amounts for the children), so only savings credit - # can be paid: the model's maximum of £1,006.51 less 40% of income above - # the guarantee. With the allowance that gives 1,006.51 - 40% x 2,476.28 = - # £16.00; with expenses 1,006.51 - 40% x 2,556.28, below nil. + # £36,107.76 (with the amounts for the children), so the model can pay + # savings credit only: the model's maximum of £1,006.51 less 40% of income + # above the guarantee. With the allowance that gives 1,006.51 - 40% x + # 2,476.28 = £16.00; with expenses 1,006.51 - 40% x 2,556.28, below nil. # So with the allowance unit A gets Pension Credit, which exempts A from # the charge and makes A the person paid for the couple, so A's partner is - # not entitled (SI 2025/969 reg 4(2)(a)); A is paid the couple's full - # £300. With expenses, unit A gets no Pension Credit, and A's £100 shared - # payment is charged: £6,963.51. A elects for partial relief. + # not entitled (SI 2025/969 reg 4(2)(a)); A receives the couple's full + # £300. With expenses, unit A gets no Pension Credit, and HMRC charges A's + # £100 shared payment: £6,963.51. A elects for partial relief. # B: tax before the charge is (42,500 - 12,570) x 20% = £5,986 with the # allowance and (43,300 - 12,570) x 20% - 20% x £675 = £6,011 with # expenses. B's total income is over £35,000 on both routes, so B is diff --git a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py index 0075c07626..ee4849fb03 100644 --- a/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_legacy_means_test_income_from_capital_properties.py @@ -69,7 +69,7 @@ """ import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -139,13 +139,20 @@ def families(draw): populations = st.lists(families(), min_size=1, max_size=4) +ZERO_INCOME = dict( + employment_income=0.0, + state_pension=0.0, + private_pension_income=0.0, + property_income=0.0, + savings_interest_income=0.0, + dividend_income=0.0, +) @st.composite def taxed_adults(draw, age): - """Adults whose pensions use up the personal allowance and whose income - from capital reaches about 20,000, so that it bears tax without - earnings.""" + """Adults with no earnings whose pensions use up the personal allowance + and whose interest, at least 7,000, bears tax without earnings.""" return dict( age=age, employment_income=0.0, @@ -296,9 +303,9 @@ def test_income_from_capital_changes_no_legacy_means_test(units, year): atol=0.01, err_msg=f"{v} at scale {scale}", ) - # Intended exception (d1424): where earnings make it move, each - # means-test income moves by exactly minus that change, wherever - # neither run is floored and the test uses its own income rules. + # Intended exception: where earnings make it move, each means-test + # income moves by minus that change, wherever neither run hits a + # floor and the test uses its own income rules. pension_credit_route = (base["pension_credit"] > 0) | ( scaled["pension_credit"] > 0 ) @@ -324,11 +331,46 @@ def test_income_from_capital_changes_no_legacy_means_test(units, year): @PROPERTY_SETTINGS @given(populations, st.sampled_from(YEARS + [2007])) +# A couple 23 years apart: the model presumes the younger, who has the rent, +# to be the elder's child, so the measure counts none of it. +@example( + [ + dict( + shape="couple_working", + members=[ + ("adult", dict(ZERO_INCOME, age=56, sublet_income=0.0)), + ("adult", dict(ZERO_INCOME, age=33, sublet_income=5_000.0)), + ], + region="NORTH_EAST", + tenure="RENT_PRIVATELY", + rent=6_000.0, + savings=0.0, + reported=False, + universal_credit=0.0, + ) + ], + 2026, +) def test_home_letting_income_bounds(units, year): sim = Simulation(situation=situation(units, year)) counted = np.asarray(sim.calculate("legacy_benefits_home_letting_income", year)) - rent = np.asarray(sim.calculate("sublet_income", year, map_to="benunit")) - pension_age = np.asarray(sim.calculate("is_SP_age", year, map_to="benunit")) > 0 + # The claimant's and partner's rent and ages, which the measure counts. + counted_member = np.asarray( + sim.calculate("is_claimant_or_partner", year), dtype=float + ) + rent = sim.map_result( + np.asarray(sim.calculate("sublet_income", year)) * counted_member, + "person", + "benunit", + ) + pension_age = ( + sim.map_result( + np.asarray(sim.calculate("is_SP_age", year), dtype=float) * counted_member, + "person", + "benunit", + ) + > 0 + ) disregard = 20 if year >= 2009 else np.where(pension_age, 20, 4) assert np.all(counted >= 0) assert np.all(counted <= rent + 0.01) @@ -579,8 +621,8 @@ def test_council_tax_reduction_letting_income_matches_the_legacy_measure(units, council tax reduction applicant and partner are the benefit unit's claimant and partner, so council_tax_reduction_home_letting_income equals legacy_benefits_home_letting_income. (The two use different pension-age - tests, but the working-age and pension-age disregards have been equal - since April 2008.)""" + tests, but the working-age and pension-age disregards have both stood at + £20 since April 2008.)""" v = calculate( units, year, @@ -675,6 +717,22 @@ def head_alone_values(households, year, **kwargs): st.sampled_from(YEARS), st.floats(1, 5_000), ) +# A Scottish working-age head, whose scheme does not count the rent. +@example( + [ + dict( + head_age=50, + parent_ages=(16, 16), + head_pension=12_000.0, + head_wages=0.0, + head_rent=5_200.0, + parent_rent=0.0, + region="SCOTLAND", + ) + ], + 2026, + 1_000.0, +) def test_a_head_applying_alone_counts_only_her_own_letting_income( households, year, extra ): @@ -687,7 +745,7 @@ def test_a_head_applying_alone_counts_only_her_own_letting_income( assert np.all(base["council_tax_reduction_head_applies_alone"] == 1) more_own = head_alone_values(households, year, head_extra=extra) more_others = head_alone_values(households, year, parent_extra=extra) - # Scotland's scheme for people under pension age lists no such income + # Scottish councils count no such income for people under pension age # (SSI 2021/249 reg 57(1)). region = np.array([h["region"] for h in households]) counts = ~((region == "SCOTLAND") & (base["council_tax_reduction_pensioner"] == 0)) @@ -699,10 +757,22 @@ def test_a_head_applying_alone_counts_only_her_own_letting_income( atol=0.02, ) assert np.all(more_own[letting] >= base[letting] - 0.01) + # Where her own rent with the extra exceeds the £20 weekly disregard, with + # a margin for float32 rounding, her letting income is positive. + above = np.array([(h["head_rent"] + extra) / 52 > 20.01 for h in households]) + assert np.all(more_own[letting][above] > 0) np.testing.assert_allclose(more_others[income], base[income], atol=0.01) np.testing.assert_allclose(more_others[letting], base[letting], atol=0.01) - # The couple's own legacy measure does count it. - assert np.all( + # The couple's own legacy measure does count it, less the £20 weekly + # disregard. + parent_rent = np.array([h["parent_rent"] for h in households]) + np.testing.assert_allclose( more_others["legacy_benefits_home_letting_income"] - >= base["legacy_benefits_home_letting_income"] - 0.01 + - base["legacy_benefits_home_letting_income"], + ( + np.maximum(0, (parent_rent + extra) / 52 - 20) + - np.maximum(0, parent_rent / 52 - 20) + ) + * 52, + atol=0.02, ) diff --git a/policyengine_uk/tests/test_property_income_properties.py b/policyengine_uk/tests/test_property_income_properties.py index 870d0524ed..2135a8bb38 100644 --- a/policyengine_uk/tests/test_property_income_properties.py +++ b/policyengine_uk/tests/test_property_income_properties.py @@ -418,8 +418,8 @@ def test_no_one_lowers_their_tax_by_switching_route_alone(couple_sim, drawn): NO_INCOME = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0) # A pensioner with wages and six children and no finance costs. Pension # Credit counts no property profits (SPC Sch IV para 18), but without the -# wages the pension and profits fall within the personal allowance, so all -# the tax is the wages' and Pension Credit deducts it. The allowance lowers +# wages the pension and profits fall within the personal allowance, so the +# wages cause all the tax and Pension Credit deducts it. The allowance lowers # that tax by £160, which ends Pension Credit of £80 and so the exemption # from the £200 charge. LONE_PENSIONER = ( diff --git a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py index d0ac6c8708..3d07f76bb2 100644 --- a/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py +++ b/policyengine_uk/variables/gov/hmrc/income_tax/allowances/uses_property_allowance.py @@ -137,11 +137,15 @@ class uses_property_allowance(Variable): "allowance is used whenever it lowers taxable property income, " "without comparing tax. That is always the lower tax before the " "winter fuel payment charge. While the charge has effect it can " - "rarely cost more: where the person has earnings, Pension Credit " - "deducts the tax they cause, including tax on property profits that " - "would fall within the allowances without them, so the lower tax can " - "raise the income Pension Credit counts and end an award that " - "exempts the person from the charge. " + "rarely cost more: the lower tax can lower the income tax the " + "Pension Credit formula deducts " + "(legacy_means_test_deducted_income_tax), which raises the income " + "Pension Credit counts and can end an award that exempts the person " + "from the charge. The deduction moves with the route where earnings " + "carry tax on the profits, and, without earnings, where the profits " + "change the tax on income Pension Credit counts: through its share " + "of a general tax reduction, the Marriage Allowance or the personal " + "allowance taper. " "The model does not compare routes for these landlords, because " "that works out the benefits on both routes for the whole " "population and made simulations several times slower; on the " @@ -180,9 +184,10 @@ def formula(person, period, parameters): # Without a reduction at stake, lower taxable property income means # lower tax before the winter fuel payment charge, so the allowance # is used without comparing. The charge can rarely reverse that - # (where earnings carry the tax on the profits, lower tax can end - # Pension Credit and with it the exemption, s.681I(4)); see the - # documentation for why that is not compared. + # (lower tax can lower the tax the Pension Credit formula deducts, + # and so end Pension Credit and with it the exemption, s.681I(4)); + # see the documentation for when that happens and why it is not + # compared. uses = lowers_income & ~reduction_at_stake choosing = lowers_income & reduction_at_stake if not choosing.any():