From ae2196a3e0d6f2cd8a50c5082143b7819c733022 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 15:32:44 -0400 Subject: [PATCH 1/4] Count spousal maintenance in Income Support --- changelog.d/fix-2573-income-support.fixed.md | 1 + ...test_income_support_spousal_maintenance.py | 111 ++++++++++++++++++ .../dwp/income_support_applicable_income.py | 11 ++ 3 files changed, 123 insertions(+) create mode 100644 changelog.d/fix-2573-income-support.fixed.md create mode 100644 policyengine_uk/tests/test_income_support_spousal_maintenance.py diff --git a/changelog.d/fix-2573-income-support.fixed.md b/changelog.d/fix-2573-income-support.fixed.md new file mode 100644 index 0000000000..d6b077a67a --- /dev/null +++ b/changelog.d/fix-2573-income-support.fixed.md @@ -0,0 +1 @@ +- Count the claimant's and partner's spousal maintenance in Income Support while continuing to disregard child maintenance. diff --git a/policyengine_uk/tests/test_income_support_spousal_maintenance.py b/policyengine_uk/tests/test_income_support_spousal_maintenance.py new file mode 100644 index 0000000000..e8a5d2a47d --- /dev/null +++ b/policyengine_uk/tests/test_income_support_spousal_maintenance.py @@ -0,0 +1,111 @@ +"""Maintenance in the Income Support means test for continuing awards. + +IS Regs 1987 reg 40 counts non-earnings income. Schedule 9 para 15(3)(a) +excludes former-partner maintenance from the voluntary-payment disregard; +para 73 disregards child maintenance. Reg 23 counts the claimant's and +partner's income, but not a child's or another benefit-unit member's income. + +https://www.legislation.gov.uk/uksi/1987/1967/regulation/40 +https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/15 +https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/73 +https://www.legislation.gov.uk/uksi/1987/1967/schedule/8/paragraph/5 +https://www.legislation.gov.uk/uksi/1987/1967/schedule/8/paragraph/6 +https://www.gov.uk/government/publications/benefit-and-pension-rates-2024-to-2025/benefit-and-pension-rates-2024-to-2025 +""" + +import pytest + +from policyengine_uk import Simulation + +YEAR = 2024 + + +def family(maintenance=None, partner=False, other_adult=False): + people = { + "claimant": { + "age": {YEAR: 40}, + "is_parent": {YEAR: True}, + "is_claimant_or_partner": {YEAR: True}, + "income_support_reported": {YEAR: 100}, + "employment_income": {YEAR: 2_000}, + "hours_worked": {YEAR: 520}, + }, + "child": { + "age": {YEAR: 4}, + "is_claimant_or_partner": {YEAR: False}, + }, + } + if partner: + people["partner"] = { + "age": {YEAR: 38}, + "is_claimant_or_partner": {YEAR: True}, + } + if other_adult: + people["other_adult"] = { + "age": {YEAR: 21}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "NOT_IN_EDUCATION"}, + } + for person, variable, amount in maintenance or []: + people[person][variable] = {YEAR: amount} + members = list(people) + return Simulation( + situation={ + "people": people, + "benunits": {"family": {"members": members}}, + "households": {"home": {"members": members, "region": "LONDON"}}, + } + ) + + +def amount(simulation, variable): + return float(simulation.calculate(variable, YEAR)[0]) + + +def test_spousal_maintenance_reduces_an_existing_income_support_award(): + baseline = family() + maintained = family([("claimant", "spousal_maintenance_income", 1_200)]) + # 2024-25 lone-parent allowance: £90.50 * 52 = £4,706. Earnings of + # £2,000 exceed the £20 * 52 = £1,040 earnings disregard, leaving £960. + # Maintenance is counted in full: £960 + £1,200 = £2,160. The continuing + # award therefore falls from £4,706 - £960 = £3,746 to £2,546. + assert amount(baseline, "income_support_eligible") == 1 + assert amount(maintained, "income_support_eligible") == 1 + assert amount(baseline, "income_support_applicable_income") == pytest.approx(960) + assert amount(maintained, "income_support_applicable_income") == pytest.approx( + 2_160 + ) + assert amount(baseline, "income_support") == pytest.approx(3_746, abs=0.01) + assert amount(maintained, "income_support") == pytest.approx(2_546, abs=0.01) + + +@pytest.mark.parametrize("recipient", ["claimant", "partner"]) +def test_either_partners_spousal_maintenance_counts(recipient): + baseline = family(partner=True) + maintained = family( + [(recipient, "spousal_maintenance_income", 1_200)], partner=True + ) + # Couple earnings disregard: £10 * 52 = £520. The claimant's £2,000 + # earnings leave £1,480; either partner's £1,200 raises it to £2,680. + assert amount(baseline, "income_support_applicable_income") == pytest.approx(1_480) + assert amount(maintained, "income_support_applicable_income") == pytest.approx( + 2_680 + ) + + +def test_child_maintenance_and_other_members_income_remain_disregarded(): + baseline = family(other_adult=True) + maintained = family( + [ + ("claimant", "maintenance_income", 1_200), + ("other_adult", "spousal_maintenance_income", 3_000), + ("child", "spousal_maintenance_income", 2_000), + ], + other_adult=True, + ) + # Only the claimant's £2,000 earnings count: £2,000 - £1,040 = £960. + for simulation in (baseline, maintained): + assert amount(simulation, "income_support_applicable_income") == pytest.approx( + 960 + ) + assert amount(simulation, "income_support") == pytest.approx(3_746, abs=0.01) 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 476682206b..301eea37d3 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -5,13 +5,20 @@ class income_support_applicable_income(Variable): value_type = float entity = BenUnit label = "Relevant income for Income Support means test" + documentation = ( + "The claimant's and partner's income for Income Support, including " + "spousal maintenance. Child maintenance is disregarded." + ) definition_period = YEAR unit = GBP reference = ( "https://www.legislation.gov.uk/uksi/1987/1967/schedule/8", "https://www.legislation.gov.uk/uksi/1987/1967/regulation/36", "https://www.legislation.gov.uk/uksi/1987/1967/regulation/38", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/40", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/15", "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/16", + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/9/paragraph/73", ) def formula(benunit, period, parameters): @@ -28,6 +35,10 @@ def formula(benunit, period, parameters): "self_employment_income", "property_income_after_finance_costs", "private_pension_income", + # Reg 40 and Sch 9 para 15(3)(a): spousal maintenance is + # non-earnings income, even when paid voluntarily. Child + # maintenance (maintenance_income) is disregarded by para 73. + "spousal_maintenance_income", ] bi = parameters(period).gov.contrib.ubi_center.basic_income if bi.interactions.include_in_means_tests: From 5e01b4e2a295a92b94ffedca323c8c401d4cdb7f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 15:32:47 -0400 Subject: [PATCH 2/4] Count maintenance and WPA in Council Tax Reduction --- .../fix-2573-council-tax-reduction.fixed.md | 1 + .../income_disregards/maintenance.yaml | 24 +++ .../widowed_parents_allowance.yaml | 24 +++ ...t_council_tax_reduction_maintenance_wpa.py | 184 ++++++++++++++++++ ...council_tax_reduction_applicable_income.py | 75 ++++++- .../input/spousal_maintenance_income.py | 8 +- 6 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 changelog.d/fix-2573-council-tax-reduction.fixed.md create mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/maintenance.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/widowed_parents_allowance.yaml create mode 100644 policyengine_uk/tests/test_council_tax_reduction_maintenance_wpa.py diff --git a/changelog.d/fix-2573-council-tax-reduction.fixed.md b/changelog.d/fix-2573-council-tax-reduction.fixed.md new file mode 100644 index 0000000000..2aadacbc18 --- /dev/null +++ b/changelog.d/fix-2573-council-tax-reduction.fixed.md @@ -0,0 +1 @@ +- Count claimant and partner spousal maintenance and Widowed Parent’s Allowance in Council Tax Reduction income across Great Britain, with the statutory £15 weekly disregards and Pension Credit routing. diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/maintenance.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/maintenance.yaml new file mode 100644 index 0000000000..911a57fa85 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/maintenance.yaml @@ -0,0 +1,24 @@ +description: Council Tax Reduction disregards this weekly amount of the applicant's and partner's combined spousal maintenance where the family includes a child or young person (the family-premium condition in the English default and Welsh schemes). +values: + 2013-04-01: 15 +metadata: + period: week + unit: currency-GBP + label: council tax reduction maintenance disregard + reference: + - title: Council Tax Reduction England pension-age Schedule 5 paragraph 20 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/20 + - title: Council Tax Reduction England default Schedule 8 paragraph 49 + href: https://www.legislation.gov.uk/uksi/2012/2886/appendix/8/paragraph/49 + - title: Council Tax Reduction Wales prescribed pension-age Schedule 4 paragraph 20 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/4/paragraph/20 + - title: Council Tax Reduction Wales prescribed working-age Schedule 9 paragraph 49 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/9/paragraph/49 + - title: Council Tax Reduction Wales default pension-age Schedule 5 paragraph 20 + href: https://www.legislation.gov.uk/wsi/2013/3035/schedule/5/paragraph/20 + - title: Council Tax Reduction Wales default working-age Schedule 7 paragraph 49 + href: https://www.legislation.gov.uk/wsi/2013/3035/schedule/7/paragraph/49 + - title: Council Tax Reduction Scotland pension-age Schedule 3 paragraph 19 + href: https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/19 + - title: Council Tax Reduction Scotland working-age regulation 57(6) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/57 diff --git a/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/widowed_parents_allowance.yaml b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/widowed_parents_allowance.yaml new file mode 100644 index 0000000000..13ba0cc7fe --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/council_tax_reduction/income_disregards/widowed_parents_allowance.yaml @@ -0,0 +1,24 @@ +description: Council Tax Reduction disregards this weekly amount of each applicant's or partner's Widowed Parent's Allowance. +values: + 2013-04-01: 15 +metadata: + period: week + unit: currency-GBP + label: council tax reduction widowed parents allowance disregard + reference: + - title: Council Tax Reduction England pension-age Schedule 5 paragraph 7 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/7 + - title: Council Tax Reduction England default Schedule 8 paragraph 21 + href: https://www.legislation.gov.uk/uksi/2012/2886/appendix/8/paragraph/21 + - title: Council Tax Reduction Wales prescribed pension-age Schedule 4 paragraph 7 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/4/paragraph/7 + - title: Council Tax Reduction Wales prescribed working-age Schedule 9 paragraph 21 + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/9/paragraph/21 + - title: Council Tax Reduction Wales default pension-age Schedule 5 paragraph 7 + href: https://www.legislation.gov.uk/wsi/2013/3035/schedule/5/paragraph/7 + - title: Council Tax Reduction Wales default working-age Schedule 7 paragraph 21 + href: https://www.legislation.gov.uk/wsi/2013/3035/schedule/7/paragraph/21 + - title: Council Tax Reduction Scotland pension-age Schedule 3 paragraph 6 + href: https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/6 + - title: Council Tax Reduction Scotland working-age regulation 57(4) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/57 diff --git a/policyengine_uk/tests/test_council_tax_reduction_maintenance_wpa.py b/policyengine_uk/tests/test_council_tax_reduction_maintenance_wpa.py new file mode 100644 index 0000000000..57b86c8bcb --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_maintenance_wpa.py @@ -0,0 +1,184 @@ +"""Statutory CTR income and disregards, independently calculated at 52 weeks. + +England: SI 2012/2885 Sch 1 para 16, Sch 5 paras 7/20; default SI +2012/2886 Sch para 54, Sch 8 paras 21/49. Wales: WSI 2013/3029 Sch 1 +para 10, Sch 6 para 17, Sch 4 paras 7/20, Sch 9 paras 21/49; default +WSI 2013/3035 Sch 5 paras 7/20, Sch 7 paras 21/49. Scotland: SSI +2021/249 reg 57(1)(b)(vi), (d), (4), (6); SSI 2012/319 reg 27(1)(j), +(o), Sch 3 paras 6/19. Wales's family premium applies to a family with +a child or young person (3029 Sch 1 para 1(1)(c), Sch 7 para 3). +""" + +import pytest + +from policyengine_uk import Simulation + +YEAR = 2026 +AREAS = { + "ENGLAND": ("LONDON", "WESTMINSTER"), + "WALES": ("WALES", "CARDIFF"), + "SCOTLAND": ("SCOTLAND", "GLASGOW_CITY"), +} +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + + +def family( + country, + *, + age=40, + recipient="claimant", + maintenance=0, + wpa=0, + child=False, + child_maintenance=0, + route="general", + head_applies_alone=False, +): + people = { + "claimant": { + "age": age, + "is_claimant_or_partner": True, + "is_household_head": True, + "state_pension": 0, + }, + "partner": { + "age": age, + "is_claimant_or_partner": True, + "is_household_head": False, + "state_pension": 0, + }, + "other": { + "age": 22, + "is_claimant_or_partner": False, + "is_household_head": False, + }, + } + if child: + people["child"] = { + "age": 5, + "is_claimant_or_partner": False, + "is_household_head": False, + } + people[recipient].update( + spousal_maintenance_income=maintenance, + widowed_parents_allowance_reported=wpa, + maintenance_income=child_maintenance, + ) + region, authority = AREAS[country] + # Pin the route and other awards to isolate the statutory income list. + # Reported WPA is deliberate even at pension age: its statutory income + # treatment also applies to an under-pension-age partner's payment. + benunit = { + "members": list(people), + "council_tax_reduction_pensioner": age >= 67, + "council_tax_reduction_head_applies_alone": head_applies_alone, + "child_benefit": 0, + "tax_credits": 0, + "universal_credit": 0, + "income_support": 0, + "in_receipt_of_guarantee_credit": route == "guarantee", + "in_receipt_of_savings_credit_only": route == "savings", + "pension_credit_income": 13_000 if route == "savings" else 0, + "pension_credit": 200 if route == "savings" else 0, + "pension_credit_war_pension_income": 0, + } + return Simulation( + situation={ + "people": { + name: {key: {YEAR: value} for key, value in inputs.items()} + for name, inputs in people.items() + }, + "benunits": {"family": benunit}, + "households": { + "home": { + "members": list(people), + "region": region, + "local_authority": authority, + } + }, + } + ) + + +def income(sim): + return float(sim.calculate("council_tax_reduction_applicable_income", YEAR)[0]) + + +@pytest.mark.parametrize("country", AREAS) +@pytest.mark.parametrize("age", [40, 80]) +@pytest.mark.parametrize("recipient", ["claimant", "partner"]) +def test_general_rules_count_wpa_after_fifteen_pounds_weekly(country, age, recipient): + # £156.65 * 52 = £8,145.80; less £15 * 52 = £780 gives £7,365.80. + sim = family(country, age=age, recipient=recipient, wpa=8_145.80) + assert income(sim) == pytest.approx(7_365.80, abs=0.01) + + +@pytest.mark.parametrize("country", AREAS) +@pytest.mark.parametrize("age", [40, 80]) +@pytest.mark.parametrize("child, expected", [(False, 5_200), (True, 4_420)]) +def test_maintenance_disregard_depends_on_the_family(country, age, child, expected): + # £100/week counts in full without a child; (£100 - £15)*52 otherwise. + sim = family(country, age=age, maintenance=5_200, child=child) + assert income(sim) == pytest.approx(expected, abs=0.01) + + +@pytest.mark.parametrize("country", AREAS) +def test_maintenance_payments_share_one_family_disregard(country): + sim = family(country, maintenance=2_600, child=True) + sim.set_input("spousal_maintenance_income", YEAR, [2_600, 2_600, 0, 0]) + assert income(sim) == pytest.approx(4_420, abs=0.01) + + +@pytest.mark.parametrize("country", AREAS) +def test_small_payments_are_not_over_disregarded(country): + assert income(family(country, maintenance=500, wpa=500, child=True)) == 0 + + +@pytest.mark.parametrize("country", AREAS) +def test_only_applicant_partner_income_counts_and_child_maintenance_is_excluded( + country, +): + assert ( + income(family(country, recipient="other", maintenance=5_200, wpa=8_145.80)) == 0 + ) + assert income(family(country, child_maintenance=5_200, child=True)) == 0 + assert ( + income( + family( + country, recipient="child", child=True, maintenance=5_200, wpa=8_145.80 + ) + ) + == 0 + ) + + +@pytest.mark.parametrize("country", AREAS) +def test_head_applying_alone_has_no_partner_or_child_disregard(country): + assert ( + income( + family( + country, recipient="partner", maintenance=5_200, head_applies_alone=True + ) + ) + == 0 + ) + assert income( + family(country, maintenance=5_200, child=True, head_applies_alone=True) + ) == pytest.approx(5_200) + + +@pytest.mark.parametrize("country", AREAS) +def test_guarantee_credit_still_passports_the_whole_income(country): + sim = family(country, age=80, maintenance=5_200, wpa=8_145.80, route="guarantee") + assert income(sim) == 0 + + +@pytest.mark.parametrize("country", AREAS) +@pytest.mark.parametrize("child, expected", [(False, 13_200), (True, 12_420)]) +def test_savings_credit_uses_assessed_income_without_adding_maintenance_again( + country, child, expected +): + # The Secretary of State's £13,000 already contains £5,200 maintenance. + # Add £200 savings credit and subtract only CTR's child-family disregard. + sim = family(country, age=80, maintenance=5_200, child=child, route="savings") + assert income(sim) == pytest.approx(expected, abs=0.01) 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 321375bd61..6a2209e55f 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,12 +16,18 @@ class council_tax_reduction_applicable_income(Variable): "pensioner earnings disregards. Great Britain's pensioner schemes " "also exclude Child Benefit from general-rule income; statutory sick " "and maternity pay remain counted as earnings. The other adjustments those " - "provisions allow (childcare charges, the £20 earnings disregards, " - "maintenance disregards and the rest) are not modelled. " + "provisions allow (childcare charges, the £20 earnings disregards " + "and the rest) are not modelled. Spousal maintenance counts less one " + "£15 weekly disregard where the applicant's family includes a child " + "or young person (the statutory family-premium condition in England's " + "default and Welsh schemes). " + "Widowed Parent's Allowance counts less £15 weekly per recipient. " + "Child maintenance is excluded. Scotland's working-age maintenance " + "rule, like Universal Credit, requires a court order or agreement; " + "the input is assumed to meet that condition. " "The income counted is the Council Tax Reduction applicant's and " - "partner's (is_council_tax_reduction_applicant_or_partner), with, as " - "the model did before, the income of the family's children or young " - "persons. Where the household head applies alone " + "partner's (is_council_tax_reduction_applicant_or_partner). " + "Where the household head applies alone " "(council_tax_reduction_head_applies_alone), the benefit unit's own " "awards (Child Benefit, Income Support, income-based JSA, " "income-related ESA, Universal Credit, tax credits, Pension Credit) " @@ -47,6 +53,18 @@ class council_tax_reduction_applicable_income(Variable): "https://www.legislation.gov.uk/ssi/2012/319/regulation/25", "https://www.legislation.gov.uk/ssi/2012/319/regulation/27", "https://www.legislation.gov.uk/ssi/2021/249/regulation/57", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/7", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/20", + "https://www.legislation.gov.uk/uksi/2012/2886/appendix/8/paragraph/21", + "https://www.legislation.gov.uk/uksi/2012/2886/appendix/8/paragraph/49", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/4/paragraph/7", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/4/paragraph/20", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/9/paragraph/21", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/9/paragraph/49", + "https://www.legislation.gov.uk/wsi/2013/3035/schedule/5/paragraph/20", + "https://www.legislation.gov.uk/wsi/2013/3035/schedule/7/paragraph/49", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/6", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/19", "https://www.legislation.gov.uk/uksi/2012/2885/schedule/4", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/3", "https://www.legislation.gov.uk/ssi/2012/319/schedule/2", @@ -205,7 +223,46 @@ def formula(benunit, period, parameters): benunit, period, applicant_and_partner_benefits, applicant_and_partner ) credits = applicant_family * add(benunit, period, ["tax_credits"]) - increased_income = income + personal_benefit_income + credits + benefits + disregards = parameters( + period + ).gov.local_authorities.council_tax_reduction.income_disregards + # England's default working-age scheme Sch 8 paras 21 and 49; + # pensioners SI 2012/2885 Sch 5 paras 7 and 20. Wales WSI 2013/3029 + # Sch 9 paras 21/49 and Sch 4 paras 7/20 (default WSI 2013/3035 Schs + # 7 and 5). Scotland SSI 2021/249 reg 57(4), (6) and SSI 2012/319 + # Sch 3 paras 6/19. All aggregate the family's maintenance for one + # £15 disregard. The default English and Welsh family premiums are for families + # with a child or young person (SI 2012/2886 Sch para 26(1)(c); + # WSI 2013/3029 Sch 1 para 1(1)(c), Sch 6 para 1(1)(c)). + maintenance = in_great_britain * benunit.sum( + max_(0, person("spousal_maintenance_income", period)) + * applicant_and_partner + ) + has_child = applicant_family & benunit( + "is_responsible_for_child_or_young_person_for_council_tax_reduction", period + ) + maintenance_disregard = where( + has_child, + min_(maintenance, disregards.maintenance * WEEKS_IN_YEAR), + 0, + ) + widowed_parents_allowance = in_great_britain * benunit.sum( + max_( + 0, + person("widowed_parents_allowance", period) + - disregards.widowed_parents_allowance * WEEKS_IN_YEAR, + ) + * applicant_and_partner + ) + increased_income = ( + income + + personal_benefit_income + + credits + + benefits + + maintenance + - maintenance_disregard + + widowed_parents_allowance + ) if not bi.interactions.include_in_means_tests: increased_income -= add_for_members( @@ -252,7 +309,11 @@ def formula(benunit, period, parameters): savings_credit_only_income = max_( 0, benunit("pension_credit_income", period) - - benunit("pension_credit_war_pension_income", period), + - benunit("pension_credit_war_pension_income", period) + # PC already counts maintenance in full: apply only CTR's + # extra disregard (England Sch 1 para 14(2)(c)(ii); Wales Sch 1 + # para 8(2)(c)(ii); Scotland reg 25(2)(c)(ii)). + - maintenance_disregard, ) + benunit("pension_credit", period) return select( [in_receipt_of_guarantee_credit, has_savings_credit_only_award], diff --git a/policyengine_uk/variables/input/spousal_maintenance_income.py b/policyengine_uk/variables/input/spousal_maintenance_income.py index 3c67952524..4db2bdabad 100644 --- a/policyengine_uk/variables/input/spousal_maintenance_income.py +++ b/policyengine_uk/variables/input/spousal_maintenance_income.py @@ -10,9 +10,9 @@ class spousal_maintenance_income(Variable): "former spouse or former civil partner, under a court order, an " "agreement or voluntarily. Child maintenance is maintenance_income. " "Unlike child maintenance, this counts as income for Universal " - "Credit, Pension Credit (but not as savings credit qualifying income) " - "and Housing Benefit, and it is market and HBAI income as " - "maintenance_income is." + "Credit, Pension Credit (but not as savings credit qualifying income), " + "Income Support, Council Tax Reduction and Housing Benefit, and it is " + "market and HBAI income as maintenance_income is." ) definition_period = YEAR unit = GBP @@ -22,5 +22,7 @@ class spousal_maintenance_income(Variable): "https://www.legislation.gov.uk/uksi/2002/1792/regulation/9", "https://www.legislation.gov.uk/uksi/2006/214/regulation/29", "https://www.legislation.gov.uk/uksi/2006/213/schedule/5", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/40", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/57", ) uprating = "gov.economic_assumptions.indices.obr.per_capita.gdp" From 475682f267760b98a26e8b6c53ec6f6e3fe4e062 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 15:32:48 -0400 Subject: [PATCH 3/4] Respect funding nation for new student loan plans --- changelog.d/fix-2573-student-loans.fixed.md | 1 + docs/book/assumptions/growthfactors.md | 18 +++ policyengine_uk/data/economic_assumptions.py | 58 ++++++--- .../tests/test_student_loan_plan_uprating.py | 114 +++++++++++++++++- 4 files changed, 169 insertions(+), 22 deletions(-) create mode 100644 changelog.d/fix-2573-student-loans.fixed.md diff --git a/changelog.d/fix-2573-student-loans.fixed.md b/changelog.d/fix-2573-student-loans.fixed.md new file mode 100644 index 0000000000..351e7f842f --- /dev/null +++ b/changelog.d/fix-2573-student-loans.fixed.md @@ -0,0 +1 @@ +- Correct newly assigned projected student loan plans by funding nation, using household region as a proxy: Plan 4 for Scotland, Plan 1 for Northern Ireland, Plan 2 for Wales, and Plan 2 or 5 by entry cohort for England. diff --git a/docs/book/assumptions/growthfactors.md b/docs/book/assumptions/growthfactors.md index 2dda55e23a..e6ecd81820 100644 --- a/docs/book/assumptions/growthfactors.md +++ b/docs/book/assumptions/growthfactors.md @@ -46,6 +46,24 @@ We apply the OBR's wage growth forecasts to employment-related variables. We use average earnings for these variables: `employee_pension_contributions`, `employer_pension_contributions`, `employment_income`, `employment_income_before_lsr`, `personal_pension_contributions`, `student_loan_repayments` +### Newly assigned student loan plans + +Dataset projection holds ages fixed, so a person of a given age represents a +later university intake in each projected year. When projection adds a loan +to a tertiary-educated person without one, it uses current household region +as a proxy for the funding nation, because the dataset does not record the +funding agency. This proxy cannot identify people who moved between nations. + +The [official repayment-plan rules](https://www.gov.uk/repaying-your-student-loan/which-repayment-plan-you-are-on) +give SAAS borrowers Plan 4 and Student Finance Northern Ireland borrowers +Plan 1. For undergraduate loans, England uses Plan 1 before the 2012 intake, +Plan 2 for 2012–2022, and Plan 5 from 2023; Wales uses Plan 1 before 2012 and +Plan 2 from 2012. New assignments follow these routes; an unknown region +leaves the person without an assigned loan. Take-up remains a fixed 40% +probability with a draw keyed on person ID. This funding-nation routing +applies to newly assigned loans; the existing-holder relabelling and +write-off approximations are unchanged. + ## Lagged average earnings We lag earnings growth by one year and use that to uprate housing service charges (including ground rent). diff --git a/policyengine_uk/data/economic_assumptions.py b/policyengine_uk/data/economic_assumptions.py index 1f9fb501af..f4449fd90a 100644 --- a/policyengine_uk/data/economic_assumptions.py +++ b/policyengine_uk/data/economic_assumptions.py @@ -544,13 +544,20 @@ def uprate_student_loan_plans( As we project forward, we need to: 1. Re-label existing holders to correct plan based on uni start year - 2. Add Plan 1/2 holders in age bands beyond the base year's coverage - 3. Add Plan 5 holders (new plan starting 2023) + 2. Add holders in age bands beyond the base year's coverage + 3. Add holders from the 2023-and-later intakes For (2) and (3), we use highest_education == TERTIARY as the signal for who is a graduate, then apply a flat take-up probability. Each person's take-up draw is keyed on person_id, so the same people take up a loan in every year and in any row order. + + New holders follow the funding nation's undergraduate repayment plan. + Funding agency is unavailable in the dataset, so current household + region proxies for it: England, Wales, SAAS for Scotland, and Student + Finance NI for Northern Ireland. This cannot identify cross-border + movers. An unknown region leaves the plan unchanged. See + https://www.gov.uk/repaying-your-student-loan/which-repayment-plan-you-are-on. """ # Skip if required columns don't exist yet (e.g., during # initial dataset creation before imputation runs) @@ -579,8 +586,26 @@ def uprate_student_loan_plans( uni_start_year = year - age + 18 is_england = np.isin(region, list(_ENGLAND_REGIONS)) + is_wales = region == "WALES" + is_scotland = region == "SCOTLAND" + is_northern_ireland = region == "NORTHERN_IRELAND" is_tertiary = highest_ed == "TERTIARY" + # England introduced Plan 5 in 2023; Wales kept Plan 2. SAAS and + # Student Finance NI use Plans 4 and 1 respectively for all intakes. + # Current residence is the funding-nation proxy described above. + new_borrower_plan = np.select( + [ + is_scotland, + is_northern_ireland, + is_england & (uni_start_year >= _PLAN5_FIRST_UNI_START), + (is_england | is_wales) & (uni_start_year >= _PLAN2_FIRST_UNI_START), + is_england | is_wales, + ], + ["PLAN_4", "PLAN_1", "PLAN_5", "PLAN_2", "PLAN_1"], + default="NONE", + ) + # Initialize output arrays new_plan = base_plan.copy() repayments = person["student_loan_repayments"].values.copy() @@ -589,10 +614,10 @@ def uprate_student_loan_plans( draws = splitmix64_uniform(person["person_id"].values, salt=_STUDENT_LOAN_DRAW_SALT) # Helper to assign plans to eligible people - def assign_with_probability(mask, plan_value, prob=_GRADUATE_LOAN_TAKE_UP): - """Assign plan_value to the masked people whose draw is below prob.""" - sampled = mask & (draws < prob) - new_plan[sampled] = plan_value + def assign_with_probability(mask, prob=_GRADUATE_LOAN_TAKE_UP): + """Assign the funding-nation plan to sampled new borrowers.""" + sampled = mask & (draws < prob) & (new_borrower_plan != "NONE") + new_plan[sampled] = new_borrower_plan[sampled] repayments[sampled] = 0.0 # === Step 1: Re-label existing loan holders === @@ -611,7 +636,7 @@ def assign_with_probability(mask, plan_value, prob=_GRADUATE_LOAN_TAKE_UP): new_plan[is_plan5_cohort & is_england] = "PLAN_5" new_plan[is_plan5_cohort & ~is_england] = "PLAN_2" - # === Step 2: Add Plan 1 holders in extended age bands === + # === Step 2: Add holders in the extended pre-2012 age bands === # Income-contingent (Plan 1) loans began with the 1998 intake, aged 44 in # 2024. With ages fixed, later years' Plan 1 intakes reach older ages, up # to the write-off. @@ -626,9 +651,9 @@ def assign_with_probability(mask, plan_value, prob=_GRADUATE_LOAN_TAKE_UP): & (uni_start_year < _PLAN2_FIRST_UNI_START) & (uni_start_year + _PLAN1_WRITEOFF_YEARS > year) ) - assign_with_probability(plan1_new_cohort, "PLAN_1") + assign_with_probability(plan1_new_cohort) - # === Step 3: Add Plan 2 holders in extended age bands === + # === Step 3: Add holders in the extended 2012-2022 age bands === # In the base year Plan 2 holders are at most the age of the 2012 intake # (30 in 2024). With ages fixed, by 2029 Plan 2 reaches age 35. # Target: NONE people who are tertiary-educated, older than the base @@ -641,20 +666,19 @@ def assign_with_probability(mask, plan_value, prob=_GRADUATE_LOAN_TAKE_UP): & (uni_start_year >= _PLAN2_FIRST_UNI_START) & (uni_start_year < _PLAN5_FIRST_UNI_START) ) - assign_with_probability(plan2_new_cohort, "PLAN_2") + assign_with_probability(plan2_new_cohort) - # === Step 4: Add Plan 5 holders (new plan from 2023) === - # Plan 5 didn't exist in base year. Eligible: tertiary-educated NONE - # people in England who would have started uni 2023+. + # === Step 4: Add holders from the 2023-and-later intakes === + # Plan 5 is for England only. Other funding nations retain their plans. + # Eligible: tertiary-educated NONE people who would have started uni 2023+. # Age constraint: must be 21+ (finished 3-year degree) to be repaying. - plan5_eligible = ( + new_intake_eligible = ( (new_plan == "NONE") & is_tertiary - & is_england - & (uni_start_year >= 2023) + & (uni_start_year >= _PLAN5_FIRST_UNI_START) & (age >= 21) ) - assign_with_probability(plan5_eligible, "PLAN_5") + assign_with_probability(new_intake_eligible) # Write back to the person table (without the merged region column). person_out = current_year.person.copy() diff --git a/policyengine_uk/tests/test_student_loan_plan_uprating.py b/policyengine_uk/tests/test_student_loan_plan_uprating.py index 0b85d21005..74e83a4712 100644 --- a/policyengine_uk/tests/test_student_loan_plan_uprating.py +++ b/policyengine_uk/tests/test_student_loan_plan_uprating.py @@ -12,7 +12,10 @@ import pytest from policyengine_uk.data import UKSingleYearDataset -from policyengine_uk.data.economic_assumptions import extend_single_year_dataset +from policyengine_uk.data.economic_assumptions import ( + extend_single_year_dataset, + uprate_student_loan_plans, +) from policyengine_uk.system import system AGES = range(18, 51) @@ -20,9 +23,15 @@ TAKE_UP = 0.4 -def mock_dataset(base_year: int) -> UKSingleYearDataset: - """MOCK data: tertiary-educated people in England, none with a loan.""" - ages = np.repeat(list(AGES), PEOPLE_PER_AGE) +def mock_dataset( + base_year: int, + *, + ages=AGES, + region="LONDON", + people_per_age=PEOPLE_PER_AGE, +) -> UKSingleYearDataset: + """MOCK data: tertiary-educated people, none with a loan.""" + ages = np.repeat(list(ages), people_per_age) ids = np.arange(1, len(ages) + 1) person = pd.DataFrame( { @@ -39,7 +48,7 @@ def mock_dataset(base_year: int) -> UKSingleYearDataset: { "household_id": ids, "household_weight": 1.0, - "region": "LONDON", + "region": region, "tenure_type": "OWNED_OUTRIGHT", "rent": 0.0, "council_tax": 0.0, @@ -126,3 +135,98 @@ def test_draws_do_not_depend_on_row_order(from_2024): pd.testing.assert_series_equal( plans.sort_index(), from_2024[2029].student_loan_plan.sort_index() ) + + +@pytest.fixture +def all_graduates_take_up(monkeypatch): + """Make the funding-plan regressions independent of random take-up.""" + monkeypatch.setattr( + "policyengine_uk.data.economic_assumptions.splitmix64_uniform", + lambda ids, salt: np.zeros(len(ids)), + ) + + +@pytest.mark.parametrize( + "region, expected_plan", + [ + ("LONDON", "PLAN_2"), + ("WALES", "PLAN_2"), + ("SCOTLAND", "PLAN_4"), + ("NORTHERN_IRELAND", "PLAN_1"), + ("UNKNOWN", "NONE"), + ], +) +def test_extended_2012_intakes_follow_funding_nation( + all_graduates_take_up, region, expected_plan +): + # GOV.UK: England/Wales 2012-2022 undergraduate intakes use Plan 2; + # SAAS uses Plan 4 and Student Finance NI uses Plan 1. Residence proxies + # for funding nation. In 2024 the 2012 intake was 30; projecting to 2029 + # adds ages 31 (2016 intake) through 35 (2012 intake). Ages 30 and 36 + # remain outside this new band. Unknown residence supplies no proxy. + dataset = mock_dataset(2024, ages=[30, 31, 35, 36], region=region, people_per_age=1) + projected = extend_single_year_dataset(dataset, system.parameters, end_year=2029) + assert projected.datasets[2029].person.student_loan_plan.tolist() == [ + "NONE", + expected_plan, + expected_plan, + "NONE", + ] + + +@pytest.mark.parametrize( + "region, expected_plan", + [ + ("LONDON", "PLAN_1"), + ("WALES", "PLAN_1"), + ("SCOTLAND", "PLAN_4"), + ("NORTHERN_IRELAND", "PLAN_1"), + ], +) +def test_extended_pre_2012_intakes_follow_funding_nation( + all_graduates_take_up, region, expected_plan +): + # The existing extension starts above age 44 (2024 - 1998 + 18). + # In 2029 ages 45/46 represent the 2002/2001 intakes: England/Wales + # use Plan 1 before September 2012; SAAS uses Plan 4; NI uses Plan 1. + previous = mock_dataset(2024, ages=[44, 45, 46], region=region, people_per_age=1) + current = previous.copy() + current.time_period = "2029" + projected = uprate_student_loan_plans(current, previous, system.parameters) + assert projected.person.student_loan_plan.tolist() == [ + "NONE", + expected_plan, + expected_plan, + ] + + +@pytest.mark.parametrize( + "region, plan_from_2023, plan_before_2023", + [ + ("LONDON", "PLAN_5", "PLAN_2"), + ("WALES", "PLAN_2", "PLAN_2"), + ("SCOTLAND", "PLAN_4", "PLAN_4"), + ("NORTHERN_IRELAND", "PLAN_1", "PLAN_1"), + ("UNKNOWN", "NONE", "NONE"), + ], +) +def test_new_intakes_follow_funding_nation_and_entry_year( + all_graduates_take_up, region, plan_from_2023, plan_before_2023 +): + # https://www.gov.uk/repaying-your-student-loan/which-repayment-plan-you-are-on + # Only England switches to Plan 5 in 2023; Wales retains Plan 2. + # In 2029 age 24 represents entry in 2023, age 25 entry in 2022. + # A 2018 base's Plan 2 coverage ends at 24, so both are newly assigned. + # Age 21 (2026 entry) qualifies; age 20 has not finished a degree. + previous = mock_dataset( + 2018, ages=[20, 21, 24, 25], region=region, people_per_age=1 + ) + current = previous.copy() + current.time_period = "2029" + projected = uprate_student_loan_plans(current, previous, system.parameters) + assert projected.person.student_loan_plan.tolist() == [ + "NONE", + plan_from_2023, + plan_from_2023, + plan_before_2023, + ] From e13b6b2693d76568277f67442d52b149a09dff2c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 15:32:49 -0400 Subject: [PATCH 4/4] Count maintenance in HB and CTR non-dependant bands --- changelog.d/fix-2573-non-dependants.fixed.md | 1 + .../test_non_dependant_spousal_maintenance.py | 101 ++++++++++++++++++ ...d_benefits_individual_non_dep_deduction.py | 15 ++- .../council_tax_reduction/_legacy.py | 3 + ..._reduction_individual_non_dep_deduction.py | 9 +- 5 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 changelog.d/fix-2573-non-dependants.fixed.md create mode 100644 policyengine_uk/tests/test_non_dependant_spousal_maintenance.py diff --git a/changelog.d/fix-2573-non-dependants.fixed.md b/changelog.d/fix-2573-non-dependants.fixed.md new file mode 100644 index 0000000000..53eb6f7c6b --- /dev/null +++ b/changelog.d/fix-2573-non-dependants.fixed.md @@ -0,0 +1 @@ +- Included spousal maintenance in Housing Benefit and Council Tax Reduction non-dependant income bands while preserving its income-tax exemption. diff --git a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py new file mode 100644 index 0000000000..e829efd5ef --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py @@ -0,0 +1,101 @@ +"""Spousal maintenance is gross income for non-dependant deductions. + +HB Regs 2006 reg 74(2), (9), pension-age HB reg 55(2), (10), and +SI 2012/2885 Sch 1 para 8(2), (9) do not disregard spousal maintenance. +It remains exempt from income tax under ITTOIA 2005 s 727. +""" + +import numpy as np +import pytest + +from policyengine_uk import Simulation + +YEAR = 2026 + + +@pytest.fixture(scope="module") +def non_dependants(): + # Separate benefit units isolate each non-dependant's own gross income. + cases = [ + ("earnings_only", 7_800, 0), + ("maintenance_100", 7_800, 5_200), + ("maintenance_150", 7_800, 7_800), + ("same_gross_earnings", 13_000, 0), + ] + people, benunits, households = {}, {}, {} + for name, earnings, maintenance in cases: + people[name] = { + "age": {YEAR: 30}, + "weekly_hours": {YEAR: 16}, + "employment_income": {YEAR: earnings}, + "spousal_maintenance_income": {YEAR: maintenance}, + "housing_benefit_individual_non_dep_deduction_eligible": {YEAR: True}, + "council_tax_reduction_individual_non_dep_deduction_eligible": {YEAR: True}, + } + benunits[name] = { + "members": [name], + "universal_credit": {YEAR: 0}, + "pension_credit": {YEAR: 0}, + } + households[name] = { + "members": [name], + "country": {YEAR: "ENGLAND"}, + "local_authority": {YEAR: "MERTON"}, + } + return Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + + +def values(simulation, variable): + return np.asarray(simulation.calculate(variable, YEAR), dtype=float) + + +def test_housing_benefit_non_dependant_band_counts_spousal_maintenance(non_dependants): + # HB reg 74(2): £150/week is below £192; £150 + £100 = £250 is + # in the £192–278.99 band. SI 2026/148 art 23(3) sets £20.40 and + # £46.85 weekly deductions: £1,060.80 and £2,436.20 a year. + np.testing.assert_allclose( + values(non_dependants, "household_benefits_individual_non_dep_deduction")[:2], + [1_060.80, 2_436.20], + atol=0.01, + ) + + +def test_national_ctr_non_dependant_band_counts_spousal_maintenance(non_dependants): + # SI 2012/2885 Sch 1 para 8(2), (9): £150 earnings plus £100 + # maintenance and £250 earnings have the same normal gross weekly income. + # Compare bands without endorsing the pre-existing shared HB rate scale + # in the national CTR formula (#2007). + deductions = values( + non_dependants, "council_tax_reduction_individual_non_dep_deduction" + ) + assert deductions[1] == pytest.approx(deductions[3], abs=0.01) + assert deductions[1] > deductions[0] + + +def test_local_ctr_non_dependant_band_counts_spousal_maintenance(non_dependants): + # Merton's 2026 scheme para 30(1), (2), (9), pp 58–61: + # £150 + £150 = £300/week falls in £279–484.99, with £10.60/week + # deducted (£551.20/year), rather than £5.20 (£270.40/year). + np.testing.assert_allclose( + values( + non_dependants, "merton_council_tax_reduction_individual_non_dep_deduction" + )[:3], + [270.40, 270.40, 551.20], + atol=0.01, + ) + + +def test_non_dependant_spousal_maintenance_stays_exempt_from_income_tax(non_dependants): + # ITTOIA 2005 s 727: maintenance leaves taxable income at £7,800. + # The £13,000 earnings comparator is taxable above the £12,570 allowance: + # (£13,000 - £12,570) × 20% = £86. + np.testing.assert_allclose( + values(non_dependants, "total_income"), + [7_800, 7_800, 7_800, 13_000], + atol=0.01, + ) + np.testing.assert_allclose( + values(non_dependants, "income_tax"), [0, 0, 0, 86], atol=0.01 + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index b23543c89a..9701a9ff4d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -8,6 +8,10 @@ class household_benefits_individual_non_dep_deduction(Variable): definition_period = YEAR unit = GBP defined_for = "housing_benefit_individual_non_dep_deduction_eligible" + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction @@ -15,10 +19,13 @@ def formula(person, period, parameters): # apply only to a non-dependant in remunerative work (HB Regs 2006 reg # 74(2); SPC Regs reg 55(2)), and the model does not yet test that, so # counting it would band non-workers whose only income is investment - # income. #2007 implements reg 74 in full and can then read - # total_income directly. - income = person("total_income", period) - person( - "other_investment_income", period + # income. #2007 implements reg 74 in full and can then include it. + # Spousal maintenance is tax-exempt, but is normal gross income and + # has no disregard under reg 74(9) or pension-age reg 55(10). + income = ( + person("total_income", period) + - person("other_investment_income", period) + + max_(0, person("spousal_maintenance_income", period)) ) weekly_income = income / WEEKS_IN_YEAR deduction = p.amount.calc(weekly_income, right=True) 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 52524b2828..2aab5da833 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 @@ -133,6 +133,9 @@ def normal_gross_income_non_dep_deduction( "savings_interest_income", "dividend_income", "state_pension", + # Normal gross income includes maintenance, although it is exempt + # from income tax (Default Scheme Sch para 30(2), (9)). + "spousal_maintenance_income", ] earned_income_components = [ "employment_income", diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py index 64458e2319..552973d3a9 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py @@ -8,6 +8,7 @@ class council_tax_reduction_individual_non_dep_deduction(Variable): definition_period = YEAR unit = GBP defined_for = "council_tax_reduction_individual_non_dep_deduction_eligible" + reference = "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8" def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction @@ -15,8 +16,12 @@ def formula(person, period, parameters): # household_benefits_individual_non_dep_deduction: the bands apply only # to a non-dependant in remunerative work (SI 2012/2885 Sch 1 para # 8(2)), which this formula does not yet test (#2007). - income = person("total_income", period) - person( - "other_investment_income", period + # Tax-exempt spousal maintenance is normal gross income, with no + # disregard in SI 2012/2885 Sch 1 para 8(9). + income = ( + person("total_income", period) + - person("other_investment_income", period) + + max_(0, person("spousal_maintenance_income", period)) ) weekly_total_income = income / WEEKS_IN_YEAR return p.amount.calc(weekly_total_income, right=True) * WEEKS_IN_YEAR