From a4c2a06082caaa206147857a6a46c536e56b4cad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:09:03 -0400 Subject: [PATCH 01/31] Apply HB and national CTR non-dependant deductions as legislated Housing Benefit (SI 2006/213 reg 74; SI 2006/214 reg 55): - bands include their lower edge, and only a non-dependant in remunerative work (16+ hours, reg 6(1)) is banded, on the couple's joint income; anyone else pays the reg 74(1)(b) amount; - one deduction per couple, the higher (reg 74(3)); - no deduction for full-time students, State Pension Credit recipients, or under-25s on IS, income-based JSA or UC without earned income (reg 74(7), (8), (10)), nor for any non-dependant when the claimant or partner is blind or on AA, DLA care, PIP daily living or AFIP (reg 74(6)). Council Tax Reduction (SI 2012/2885 Sch 1 para 8; WSI 2013/3029 Sch 1 para 3 and Sch 6 para 5; SSI 2012/319 reg 48; SSI 2012/303 reg 67 and SSI 2021/249 reg 90): - each national scheme's own weekly scale for 2013-14 to 2026-27, instead of the Housing Benefit amounts; - the same banding and couple rules, with Wales's working-age exception for couples on UC; - the schemes' any-age exemptions, the UC limb from the year each scheme added it, and the existing applicant exemption. Fixes #2004; the non-dependant half of #1934. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 1 + .../programs/gov/dwp/housing-benefit.ipynb | 2 +- .../non_dep_deduction/age_threshold.yaml | 12 +- .../income_related_benefit_age_limit.yaml | 12 + .../remunerative_work_hours.yaml | 12 + .../pensioners/non_dep_deduction/amount.yaml | 154 ++ ...niversal_credit_without_earned_income.yaml | 11 + .../remunerative_work_hours.yaml | 10 + .../non_dep_deduction/amount.yaml | 160 ++ ...niversal_credit_without_earned_income.yaml | 13 + .../remunerative_work_hours.yaml | 14 + .../non_dep_deduction/amount.yaml | 162 ++ ...niversal_credit_without_earned_income.yaml | 11 + .../remunerative_work_hours.yaml | 12 + policyengine_uk/programs.yaml | 2 +- ...ax_reduction_non_dependant_deductions.yaml | 185 +++ ...benefits_individual_non_dep_deduction.yaml | 190 ++- ...individual_non_dep_deduction_eligible.yaml | 15 + ...using_benefit_non_dep_deduction_chain.yaml | 90 ++ ...sing_benefit_non_dep_deduction_exempt.yaml | 119 ++ .../housing_benefit_non_dep_deductions.yaml | 45 + ...it_non_dep_deductions_claimant_exempt.yaml | 77 + ...ncil_tax_reduction_non_dep_deductions.yaml | 1312 +++++++++++++++++ .../council_tax_reduction_pension_credit.yaml | 14 +- ...test_non_dependant_deduction_properties.py | 545 +++++++ ...d_benefits_individual_non_dep_deduction.py | 24 +- ...t_individual_non_dep_deduction_eligible.py | 7 +- ...ousing_benefit_non_dep_deduction_exempt.py | 57 + .../housing_benefit_non_dep_deductions.py | 22 +- ...efit_non_dep_deductions_claimant_exempt.py | 33 + ..._reduction_individual_non_dep_deduction.py | 77 +- ..._tax_reduction_non_dep_deduction_exempt.py | 80 + ...ouncil_tax_reduction_non_dep_deductions.py | 44 +- uv.lock | 2 +- 34 files changed, 3480 insertions(+), 46 deletions(-) create mode 100644 changelog.d/2004.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml create mode 100644 policyengine_uk/tests/test_non_dependant_deduction_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md new file mode 100644 index 0000000000..f01a97f236 --- /dev/null +++ b/changelog.d/2004.fixed.md @@ -0,0 +1 @@ +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge, only a non-dependant in remunerative work is banded (on a couple's joint income, one deduction per couple), the claimant-side and non-dependant exemptions apply, and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 01ae7d9a50..4a963be413 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**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\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 appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", + "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). Under reg 74 (SI 2006/214 reg 55), a non-dependant aged 18 or over in remunerative work (16 hours a week or more) pays the deduction for the band containing their normal gross weekly income, counting a couple's joint income and including each band's lower edge; one not in remunerative work pays the lowest amount. A couple pays one deduction, the higher. No deduction is made for full-time students, State Pension Credit recipients, or under-25s on Income Support, income-based JSA or Universal Credit without earned income, nor for any non-dependant if the claimant or partner is blind or receives Attendance Allowance, the DLA care component, the PIP daily living component or Armed Forces Independence Payment. The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\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`." diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml index c08cba87a4..d22b71993c 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml @@ -1,11 +1,13 @@ -description: A non dependent deduction is provided under Housing Benefit for filers at or above this age threshold. +description: Housing Benefit makes a non-dependant deduction only for non-dependants at or above this age. metadata: label: Housing benefit non dependent deduction age threshold period: year unit: year - reference: - - title: The Housing Benefit Regulations 2006 sec. 74 (1) - href: https://www.legislation.gov.uk/uksi/2006/213/regulation/74/2024-04-01 + reference: + - title: The Housing Benefit Regulations 2006 reg. 74(1) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/74 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 55(1) + href: https://www.legislation.gov.uk/uksi/2006/214/regulation/55 values: - 2019-04-01: 18 + 2006-03-06: 18 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml new file mode 100644 index 0000000000..643e0c4133 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml @@ -0,0 +1,12 @@ +description: Housing Benefit makes no deduction for a non-dependant under this age who is on Income Support or income-based Jobseeker's Allowance, or entitled to Universal Credit calculated on the basis that they have no earned income. +values: + 2006-03-06: 25 +metadata: + label: Housing Benefit non-dependant income-related benefit exemption age limit + unit: year + period: year + reference: + - title: The Housing Benefit Regulations 2006 reg. 74(8) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/74 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 55(8) + href: https://www.legislation.gov.uk/uksi/2006/214/regulation/55 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..0fecad832e --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,12 @@ +description: Housing Benefit treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2006-03-06: 16 +metadata: + label: Housing Benefit remunerative work hours + unit: hour + period: week + reference: + - title: The Housing Benefit Regulations 2006 reg. 6(1) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/6 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 6(1) + href: https://www.legislation.gov.uk/uksi/2006/214/regulation/6 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml new file mode 100644 index 0000000000..f7d2106bc1 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml @@ -0,0 +1,154 @@ +description: England pensioner Council Tax Reduction deducts this weekly amount for each non-dependant aged 18 or over, by the non-dependant's normal gross weekly income if they are in remunerative work; a non-dependant not in remunerative work pays the lowest amount. + +# Sums from paragraph 8 of Schedule 1 to the Council Tax Reduction Schemes +# (Prescribed Requirements) (England) Regulations 2012 (SI 2012/2885), as +# substituted for each financial year by the amending instruments in the +# references. Each year's sums apply from 1 April. +brackets: +- threshold: + 2013-04-01: 0 + amount: + 2013-04-01: 3.65 + 2014-04-01: 3.7 + 2015-04-01: 3.74 + 2016-04-01: 3.77 + 2017-04-01: 3.8 + 2018-04-01: 3.9 + 2019-04-01: 4 + 2020-04-01: 4.05 + 2021-04-01: 4.05 + 2022-04-01: 4.2 + 2023-04-01: 4.6 + 2024-04-01: 4.9 + 2025-04-01: 5 + 2026-04-01: 5.2 +- threshold: + 2013-04-01: 186 + 2014-04-01: 188 + 2015-04-01: 189 + 2016-04-01: 195 + 2017-04-01: 196.95 + 2018-04-01: 202.85 + 2019-04-01: 207.7 + 2020-04-01: 217 + 2021-04-01: 217 + 2022-04-01: 224 + 2023-04-01: 236 + 2024-04-01: 256 + 2025-04-01: 266 + 2026-04-01: 279 + amount: + 2013-04-01: 7.25 + 2014-04-01: 7.45 + 2015-04-01: 7.52 + 2016-04-01: 7.58 + 2017-04-01: 7.65 + 2018-04-01: 7.9 + 2019-04-01: 8.1 + 2020-04-01: 8.25 + 2021-04-01: 8.3 + 2022-04-01: 8.55 + 2023-04-01: 9.4 + 2024-04-01: 10.05 + 2025-04-01: 10.2 + 2026-04-01: 10.6 +- threshold: + 2013-04-01: 322 + 2014-04-01: 326 + 2015-04-01: 328 + 2016-04-01: 338 + 2017-04-01: 341.4 + 2018-04-01: 351.65 + 2019-04-01: 360.1 + 2020-04-01: 377 + 2021-04-01: 377 + 2022-04-01: 389 + 2023-04-01: 410 + 2024-04-01: 445 + 2025-04-01: 463 + 2026-04-01: 485 + amount: + 2013-04-01: 9.15 + 2014-04-01: 9.4 + 2015-04-01: 9.49 + 2016-04-01: 9.56 + 2017-04-01: 9.65 + 2018-04-01: 9.95 + 2019-04-01: 10.2 + 2020-04-01: 10.35 + 2021-04-01: 10.4 + 2022-04-01: 10.7 + 2023-04-01: 11.8 + 2024-04-01: 12.6 + 2025-04-01: 12.8 + 2026-04-01: 13.3 +- threshold: + 2013-04-01: 401 + 2014-04-01: 406 + 2015-04-01: 408 + 2016-04-01: 420 + 2017-04-01: 424.2 + 2018-04-01: 436.9 + 2019-04-01: 447.4 + 2020-04-01: 469 + 2021-04-01: 469 + 2022-04-01: 484 + 2023-04-01: 511 + 2024-04-01: 554 + 2025-04-01: 577 + 2026-04-01: 605 + amount: + 2013-04-01: 10.95 + 2014-04-01: 11.25 + 2015-04-01: 11.36 + 2016-04-01: 11.45 + 2017-04-01: 11.55 + 2018-04-01: 11.9 + 2019-04-01: 12.2 + 2020-04-01: 12.4 + 2021-04-01: 12.45 + 2022-04-01: 12.85 + 2023-04-01: 14.15 + 2024-04-01: 15.1 + 2025-04-01: 15.35 + 2026-04-01: 15.95 +metadata: + label: England pensioner CTR non-dependant deduction + amount_unit: currency-GBP + threshold_unit: currency-GBP + period: week + type: single_amount + economy: false + propagate_metadata_to_children: true + uprating: gov.benefit_uprating_cpi + reference: + - title: Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012, Sch. 1 para. 8 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8 + - title: SI 2012/3085, sums for 2013-14 + href: https://www.legislation.gov.uk/uksi/2012/3085/made + - title: SI 2013/3181, sums for 2014-15 + href: https://www.legislation.gov.uk/uksi/2013/3181/made + - title: SI 2014/3312, sums for 2015-16 + href: https://www.legislation.gov.uk/uksi/2014/3312/made + - title: SI 2015/2041, sums for 2016-17 + href: https://www.legislation.gov.uk/uksi/2015/2041/made + - title: SI 2016/1262, sums for 2017-18 + href: https://www.legislation.gov.uk/uksi/2016/1262/made + - title: SI 2017/1305, sums for 2018-19 + href: https://www.legislation.gov.uk/uksi/2017/1305/made + - title: SI 2018/1346, sums for 2019-20 + href: https://www.legislation.gov.uk/uksi/2018/1346/made + - title: SI 2020/23, sums for 2020-21 + href: https://www.legislation.gov.uk/uksi/2020/23/made + - title: SI 2021/29, sums for 2021-22 + href: https://www.legislation.gov.uk/uksi/2021/29/made + - title: SI 2022/25, sums for 2022-23 + href: https://www.legislation.gov.uk/uksi/2022/25/made + - title: SI 2023/16, sums for 2023-24 + href: https://www.legislation.gov.uk/uksi/2023/16/made + - title: SI 2024/29, sums for 2024-25 + href: https://www.legislation.gov.uk/uksi/2024/29/made + - title: SI 2025/39, sums for 2025-26 + href: https://www.legislation.gov.uk/uksi/2025/39/made + - title: SI 2026/27, sums for 2026-27 + href: https://www.legislation.gov.uk/uksi/2026/27/made diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml new file mode 100644 index 0000000000..75410b1ece --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml @@ -0,0 +1,11 @@ +description: England pensioner Council Tax Reduction makes no deduction for a non-dependant entitled to Universal Credit calculated on the basis that they have no earned income, from the year this applies (Sch. 1 para. 8(8)(c)). +values: + 2013-04-01: false + 2015-04-01: true +metadata: + label: England pensioner CTR exempts non-dependants on Universal Credit without earned income + unit: bool + period: year + reference: + - title: SI 2012/2885 Sch. 1 para. 8(8)(c), inserted for 2015-16 by SI 2014/3312 reg. 2(5)(a)(vi) + href: https://www.legislation.gov.uk/uksi/2014/3312/regulation/2/made diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..565e049a0b --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,10 @@ +description: England pensioner Council Tax Reduction treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2013-04-01: 16 +metadata: + label: England pensioner CTR remunerative work hours + unit: hour + period: week + reference: + - title: Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012, reg. 10(1) + href: https://www.legislation.gov.uk/uksi/2012/2885/regulation/10 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml new file mode 100644 index 0000000000..3b5c0874b7 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml @@ -0,0 +1,160 @@ +description: Scotland Council Tax Reduction deducts this weekly amount for each non-dependant aged 18 or over, by the non-dependant's normal gross weekly income if they are in remunerative work; a non-dependant not in remunerative work pays the lowest amount. + +# Sums from regulation 48 of the Council Tax Reduction (State Pension Credit) +# (Scotland) Regulations 2012 (SSI 2012/319) and, for people under pension +# age, regulation 67 of SSI 2012/303 to 2021-22 and regulation 90 of the +# Council Tax Reduction (Scotland) Regulations 2021 (SSI 2021/249) from +# 2022-23, as substituted for each financial year by the amending instruments +# in the references. Each year's sums apply from 1 April. +brackets: +- threshold: + 2013-04-01: 0 + amount: + 2013-04-01: 3.65 + 2014-04-01: 3.75 + 2015-04-01: 3.85 + 2016-04-01: 3.9 + 2017-04-01: 3.95 + 2018-04-01: 4.05 + 2019-04-01: 4.15 + 2020-04-01: 4.25 + 2021-04-01: 4.3 + 2022-04-01: 4.45 + 2023-04-01: 4.9 + 2024-04-01: 5.25 + 2025-04-01: 5.35 + 2026-04-01: 5.55 +- threshold: + 2013-04-01: 186 + 2014-04-01: 188 + 2015-04-01: 189 + 2016-04-01: 195 + 2017-04-01: 200 + 2018-04-01: 204 + 2019-04-01: 209 + 2020-04-01: 213 + 2021-04-01: 213 + 2022-04-01: 228 + 2023-04-01: 241 + 2024-04-01: 260 + 2025-04-01: 273 + 2026-04-01: 286 + amount: + 2013-04-01: 7.25 + 2014-04-01: 7.5 + 2015-04-01: 7.7 + 2016-04-01: 7.75 + 2017-04-01: 7.85 + 2018-04-01: 8.1 + 2019-04-01: 8.3 + 2020-04-01: 8.45 + 2021-04-01: 8.5 + 2022-04-01: 8.8 + 2023-04-01: 9.7 + 2024-04-01: 10.35 + 2025-04-01: 10.55 + 2026-04-01: 10.95 +- threshold: + 2013-04-01: 322 + 2014-04-01: 326 + 2015-04-01: 328 + 2016-04-01: 338 + 2017-04-01: 346 + 2018-04-01: 354 + 2019-04-01: 363 + 2020-04-01: 370 + 2021-04-01: 370 + 2022-04-01: 396 + 2023-04-01: 418 + 2024-04-01: 451 + 2025-04-01: 474 + 2026-04-01: 497 + amount: + 2013-04-01: 9.15 + 2014-04-01: 9.5 + 2015-04-01: 9.8 + 2016-04-01: 9.85 + 2017-04-01: 9.95 + 2018-04-01: 10.25 + 2019-04-01: 10.5 + 2020-04-01: 10.7 + 2021-04-01: 10.8 + 2022-04-01: 11.15 + 2023-04-01: 12.3 + 2024-04-01: 13.15 + 2025-04-01: 13.4 + 2026-04-01: 13.9 +- threshold: + 2013-04-01: 401 + 2014-04-01: 406 + 2015-04-01: 408 + 2016-04-01: 420 + 2017-04-01: 430 + 2018-04-01: 439 + 2019-04-01: 451 + 2020-04-01: 458 + 2021-04-01: 458 + 2022-04-01: 490 + 2023-04-01: 517 + 2024-04-01: 558 + 2025-04-01: 586 + 2026-04-01: 614 + amount: + 2013-04-01: 10.95 + 2014-04-01: 11.35 + 2015-04-01: 11.7 + 2016-04-01: 11.8 + 2017-04-01: 11.9 + 2018-04-01: 12.25 + 2019-04-01: 12.55 + 2020-04-01: 12.8 + 2021-04-01: 12.9 + 2022-04-01: 13.3 + 2023-04-01: 14.65 + 2024-04-01: 15.65 + 2025-04-01: 15.95 + 2026-04-01: 16.55 +metadata: + label: Scotland CTR non-dependant deduction + amount_unit: currency-GBP + threshold_unit: currency-GBP + period: week + type: single_amount + economy: false + propagate_metadata_to_children: true + uprating: gov.benefit_uprating_cpi + reference: + - title: Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012, reg. 48 + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/48 + - title: Council Tax Reduction (Scotland) Regulations 2012, reg. 67 (to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/67 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 90 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/90 + - title: SSI 2013/49, sums for 2013-14 + href: https://www.legislation.gov.uk/ssi/2013/49/made + - title: SSI 2014/35, sums for 2014-15 + href: https://www.legislation.gov.uk/ssi/2014/35/made + - title: SSI 2015/46, sums for 2015-16 + href: https://www.legislation.gov.uk/ssi/2015/46/made + - title: SSI 2016/81, sums for 2016-17 + href: https://www.legislation.gov.uk/ssi/2016/81/made + - title: SSI 2017/41, sums for 2017-18 + href: https://www.legislation.gov.uk/ssi/2017/41/made + - title: SSI 2018/69, sums for 2018-19 + href: https://www.legislation.gov.uk/ssi/2018/69/made + - title: SSI 2019/29, sums for 2019-20 + href: https://www.legislation.gov.uk/ssi/2019/29/made + - title: SSI 2020/25, sums for 2020-21 + href: https://www.legislation.gov.uk/ssi/2020/25/made + - title: SSI 2021/51, sums for 2021-22 + href: https://www.legislation.gov.uk/ssi/2021/51/made + - title: SSI 2022/52, sums for 2022-23 + href: https://www.legislation.gov.uk/ssi/2022/52/made + - title: SSI 2023/38, sums for 2023-24 + href: https://www.legislation.gov.uk/ssi/2023/38/made + - title: SSI 2024/35, sums for 2024-25 + href: https://www.legislation.gov.uk/ssi/2024/35/made + - title: SSI 2025/24, sums for 2025-26 + href: https://www.legislation.gov.uk/ssi/2025/24/made + - title: SSI 2026/56, sums for 2026-27 + href: https://www.legislation.gov.uk/ssi/2026/56/made diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml new file mode 100644 index 0000000000..7ee2d4484a --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml @@ -0,0 +1,13 @@ +description: Scotland Council Tax Reduction makes no deduction for a non-dependant entitled to Universal Credit calculated on the basis that they have no earned income, from the year this applies (SSI 2012/319 reg. 48(8)(c), SSI 2012/303 reg. 67(8)(c) and SSI 2021/249 reg. 90(8)(c)). +values: + 2013-04-01: false + 2016-04-01: true +metadata: + label: Scotland CTR exempts non-dependants on Universal Credit without earned income + unit: bool + period: year + reference: + - title: SSI 2012/319 reg. 48(8)(c) and SSI 2012/303 reg. 67(8)(c), inserted from 1 April 2016 by SSI 2016/81 + href: https://www.legislation.gov.uk/ssi/2016/81/made + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 90(8)(c) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/90 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..9bf0f1cdf8 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,14 @@ +description: Scotland Council Tax Reduction treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2013-04-01: 16 +metadata: + label: Scotland CTR remunerative work hours + unit: hour + period: week + reference: + - title: Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012, reg. 6(1) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/6 + - title: Council Tax Reduction (Scotland) Regulations 2012, reg. 6(1) (to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/6 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 10(1) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/10 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml new file mode 100644 index 0000000000..72afa4a599 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml @@ -0,0 +1,162 @@ +description: Wales Council Tax Reduction deducts this weekly amount for each non-dependant aged 18 or over, by the non-dependant's normal gross weekly income if they are in remunerative work; a non-dependant not in remunerative work pays the lowest amount. + +# Sums from paragraph 3 of Schedule 1 (pensioners) and paragraph 5 of Schedule +# 6 (persons who are not pensioners) to the Council Tax Reduction Schemes and +# Prescribed Requirements (Wales) Regulations 2013 (WSI 2013/3029); for +# 2013-14, the same paragraphs of WSI 2012/3144, as substituted for each +# financial year by the amending instruments in the references. Each year's +# sums apply from 1 April. +# 2016-17: the English text sets the first band at £194; the Welsh text has +# £195 (WSI 2017/46 reg. 3(a)(iii)-(v) replaced both). +brackets: +- threshold: + 2013-04-01: 0 + amount: + 2013-04-01: 3.65 + 2014-04-01: 3.75 + 2015-04-01: 3.9 + 2016-04-01: 4.05 + 2017-04-01: 4.2 + 2018-04-01: 4.35 + 2019-04-01: 4.55 + 2020-04-01: 4.85 + 2021-04-01: 5.1 + 2022-04-01: 5.3 + 2023-04-01: 5.45 + 2024-04-01: 5.8 + 2025-04-01: 6.25 + 2026-04-01: 6.7 +- threshold: + 2013-04-01: 186 + 2014-04-01: 188 + 2015-04-01: 189 + 2016-04-01: 194 + 2017-04-01: 200 + 2018-04-01: 205 + 2019-04-01: 210 + 2020-04-01: 217 + 2021-04-01: 217 + 2022-04-01: 224 + 2023-04-01: 236 + 2024-04-01: 256 + 2025-04-01: 266 + 2026-04-01: 279 + amount: + 2013-04-01: 7.25 + 2014-04-01: 7.5 + 2015-04-01: 7.8 + 2016-04-01: 8.1 + 2017-04-01: 8.4 + 2018-04-01: 8.7 + 2019-04-01: 9.15 + 2020-04-01: 9.75 + 2021-04-01: 10.2 + 2022-04-01: 10.6 + 2023-04-01: 10.9 + 2024-04-01: 11.55 + 2025-04-01: 12.45 + 2026-04-01: 13.35 +- threshold: + 2013-04-01: 322 + 2014-04-01: 326 + 2015-04-01: 328 + 2016-04-01: 338 + 2017-04-01: 346 + 2018-04-01: 355 + 2019-04-01: 365 + 2020-04-01: 377 + 2021-04-01: 377 + 2022-04-01: 389 + 2023-04-01: 410 + 2024-04-01: 445 + 2025-04-01: 463 + 2026-04-01: 485 + amount: + 2013-04-01: 9.15 + 2014-04-01: 9.45 + 2015-04-01: 9.85 + 2016-04-01: 10.25 + 2017-04-01: 10.6 + 2018-04-01: 10.95 + 2019-04-01: 11.5 + 2020-04-01: 12.25 + 2021-04-01: 12.85 + 2022-04-01: 13.35 + 2023-04-01: 13.7 + 2024-04-01: 14.5 + 2025-04-01: 15.65 + 2026-04-01: 16.8 +- threshold: + 2013-04-01: 401 + 2014-04-01: 406 + 2015-04-01: 408 + 2016-04-01: 420 + 2017-04-01: 430 + 2018-04-01: 440 + 2019-04-01: 450 + 2020-04-01: 469 + 2021-04-01: 469 + 2022-04-01: 484 + 2023-04-01: 511 + 2024-04-01: 554 + 2025-04-01: 577 + 2026-04-01: 605 + amount: + 2013-04-01: 10.95 + 2014-04-01: 11.3 + 2015-04-01: 11.75 + 2016-04-01: 12.25 + 2017-04-01: 12.7 + 2018-04-01: 13.1 + 2019-04-01: 13.75 + 2020-04-01: 14.65 + 2021-04-01: 15.35 + 2022-04-01: 15.95 + 2023-04-01: 16.4 + 2024-04-01: 17.35 + 2025-04-01: 18.7 + 2026-04-01: 20.05 +metadata: + label: Wales CTR non-dependant deduction + amount_unit: currency-GBP + threshold_unit: currency-GBP + period: week + type: single_amount + economy: false + propagate_metadata_to_children: true + uprating: gov.benefit_uprating_cpi + reference: + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013, Sch. 1 para. 3 (pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3 + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013, Sch. 6 para. 5 (persons who are not pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5 + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (2013-14), Sch. 1 para. 3 and Sch. 6 para. 5 + href: https://www.legislation.gov.uk/wsi/2012/3144/schedule/1/paragraph/3 + - title: WSI 2013/112, sums for 2013-14 + href: https://www.legislation.gov.uk/wsi/2013/112/made + - title: WSI 2014/66, sums for 2014-15 + href: https://www.legislation.gov.uk/wsi/2014/66/made + - title: WSI 2015/44, sums for 2015-16 + href: https://www.legislation.gov.uk/wsi/2015/44/made + - title: WSI 2016/50, sums for 2016-17 + href: https://www.legislation.gov.uk/wsi/2016/50/made + - title: WSI 2017/46, sums for 2017-18 + href: https://www.legislation.gov.uk/wsi/2017/46/made + - title: WSI 2018/14, sums for 2018-19 + href: https://www.legislation.gov.uk/wsi/2018/14/made + - title: WSI 2019/11, sums for 2019-20 + href: https://www.legislation.gov.uk/wsi/2019/11/made + - title: WSI 2020/16, sums for 2020-21 + href: https://www.legislation.gov.uk/wsi/2020/16/made + - title: WSI 2021/34, sums for 2021-22 + href: https://www.legislation.gov.uk/wsi/2021/34/made + - title: WSI 2022/51, sums for 2022-23 + href: https://www.legislation.gov.uk/wsi/2022/51/made + - title: WSI 2023/47, sums for 2023-24 + href: https://www.legislation.gov.uk/wsi/2023/47/made + - title: WSI 2024/56, sums for 2024-25 + href: https://www.legislation.gov.uk/wsi/2024/56/made + - title: WSI 2025/59, sums for 2025-26 + href: https://www.legislation.gov.uk/wsi/2025/59/made + - title: WSI 2026/13, sums for 2026-27 + href: https://www.legislation.gov.uk/wsi/2026/13/made diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml new file mode 100644 index 0000000000..d7ec14e179 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml @@ -0,0 +1,11 @@ +description: Wales Council Tax Reduction makes no deduction for a non-dependant entitled to Universal Credit calculated on the basis that they have no earned income, from the year this applies (Sch. 1 para. 3(8)(c) and Sch. 6 para. 5(8)(c)). +values: + 2013-04-01: false + 2015-04-01: true +metadata: + label: Wales CTR exempts non-dependants on Universal Credit without earned income + unit: bool + period: year + reference: + - title: WSI 2013/3029 Sch. 1 para. 3(8)(c) and Sch. 6 para. 5(8)(c), added for 2015-16 by WSI 2015/44 + href: https://www.legislation.gov.uk/wsi/2015/44/made diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..46775c49e1 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,12 @@ +description: Wales Council Tax Reduction treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2013-04-01: 16 +metadata: + label: Wales CTR remunerative work hours + unit: hour + period: week + reference: + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013, reg. 10(1) + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/10 + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (2013-14), reg. 10(1) + href: https://www.legislation.gov.uk/wsi/2012/3144/regulation/10 diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 46d253f103..61a51cecef 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -521,7 +521,7 @@ programs: variable: council_tax_reduction parameter_prefix: gov.local_authorities verified_start_year: 2013 - notes: First-pass national support for Scotland, Wales, and England pensioner schemes (including the Pension Credit guarantee credit disregard and savings-credit-only rules), with reported CTB/CTR retained for unsupported English working-age local schemes. + notes: First-pass national support for Scotland, Wales, and England pensioner schemes (including the Pension Credit guarantee credit disregard and savings-credit-only rules, and each scheme's own non-dependant deductions and exemptions), with reported CTB/CTR retained for unsupported English working-age local schemes. # --- Treasury programs --- - id: cost_of_living_support diff --git a/policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml b/policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml new file mode 100644 index 0000000000..2aa4334b76 --- /dev/null +++ b/policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml @@ -0,0 +1,185 @@ +# Council Tax Reduction non-dependant deductions as enacted, weekly, for the +# financial year starting 1 April of each year, from each year's amending +# instrument (source) and the point-in-time text of the scheme provision on +# legislation.gov.uk. Bands include their lower edge; the first amount is +# para (1)(b), the last para (1)(a). Wales: WSI 2012/3144 (2013-14), then WSI +# 2013/3029 Sch 1 para 3 and Sch 6 para 5 (identical scales). Scotland: SSI +# 2012/319 reg 48, SSI 2012/303 reg 67 and SSI 2021/249 reg 90 (identical). + +england_pensioners: + parameter: gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.amount + years: + 2013: + thresholds: [0, 186.00, 322.00, 401.00] + amounts: [3.65, 7.25, 9.15, 10.95] + source: https://www.legislation.gov.uk/uksi/2012/3085/made + 2014: + thresholds: [0, 188.00, 326.00, 406.00] + amounts: [3.70, 7.45, 9.40, 11.25] + source: https://www.legislation.gov.uk/uksi/2013/3181/made + 2015: + thresholds: [0, 189.00, 328.00, 408.00] + amounts: [3.74, 7.52, 9.49, 11.36] + source: https://www.legislation.gov.uk/uksi/2014/3312/made + 2016: + thresholds: [0, 195.00, 338.00, 420.00] + amounts: [3.77, 7.58, 9.56, 11.45] + source: https://www.legislation.gov.uk/uksi/2015/2041/made + 2017: + thresholds: [0, 196.95, 341.40, 424.20] + amounts: [3.80, 7.65, 9.65, 11.55] + source: https://www.legislation.gov.uk/uksi/2016/1262/made + 2018: + thresholds: [0, 202.85, 351.65, 436.90] + amounts: [3.90, 7.90, 9.95, 11.90] + source: https://www.legislation.gov.uk/uksi/2017/1305/made + 2019: + thresholds: [0, 207.70, 360.10, 447.40] + amounts: [4.00, 8.10, 10.20, 12.20] + source: https://www.legislation.gov.uk/uksi/2018/1346/made + 2020: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [4.05, 8.25, 10.35, 12.40] + source: https://www.legislation.gov.uk/uksi/2020/23/made + 2021: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [4.05, 8.30, 10.40, 12.45] + source: https://www.legislation.gov.uk/uksi/2021/29/made + 2022: + thresholds: [0, 224.00, 389.00, 484.00] + amounts: [4.20, 8.55, 10.70, 12.85] + source: https://www.legislation.gov.uk/uksi/2022/25/made + 2023: + thresholds: [0, 236.00, 410.00, 511.00] + amounts: [4.60, 9.40, 11.80, 14.15] + source: https://www.legislation.gov.uk/uksi/2023/16/made + 2024: + thresholds: [0, 256.00, 445.00, 554.00] + amounts: [4.90, 10.05, 12.60, 15.10] + source: https://www.legislation.gov.uk/uksi/2024/29/made + 2025: + thresholds: [0, 266.00, 463.00, 577.00] + amounts: [5.00, 10.20, 12.80, 15.35] + source: https://www.legislation.gov.uk/uksi/2025/39/made + 2026: + thresholds: [0, 279.00, 485.00, 605.00] + amounts: [5.20, 10.60, 13.30, 15.95] + source: https://www.legislation.gov.uk/uksi/2026/27/made +wales: + parameter: gov.local_authorities.wales.council_tax_reduction.non_dep_deduction.amount + years: + 2013: + thresholds: [0, 186.00, 322.00, 401.00] + amounts: [3.65, 7.25, 9.15, 10.95] + source: https://www.legislation.gov.uk/wsi/2013/112/made + 2014: + thresholds: [0, 188.00, 326.00, 406.00] + amounts: [3.75, 7.50, 9.45, 11.30] + source: https://www.legislation.gov.uk/wsi/2014/66/made + 2015: + thresholds: [0, 189.00, 328.00, 408.00] + amounts: [3.90, 7.80, 9.85, 11.75] + source: https://www.legislation.gov.uk/wsi/2015/44/made + 2016: + thresholds: [0, 194.00, 338.00, 420.00] + amounts: [4.05, 8.10, 10.25, 12.25] + source: https://www.legislation.gov.uk/wsi/2016/50/made + 2017: + thresholds: [0, 200.00, 346.00, 430.00] + amounts: [4.20, 8.40, 10.60, 12.70] + source: https://www.legislation.gov.uk/wsi/2017/46/made + 2018: + thresholds: [0, 205.00, 355.00, 440.00] + amounts: [4.35, 8.70, 10.95, 13.10] + source: https://www.legislation.gov.uk/wsi/2018/14/made + 2019: + thresholds: [0, 210.00, 365.00, 450.00] + amounts: [4.55, 9.15, 11.50, 13.75] + source: https://www.legislation.gov.uk/wsi/2019/11/made + 2020: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [4.85, 9.75, 12.25, 14.65] + source: https://www.legislation.gov.uk/wsi/2020/16/made + 2021: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [5.10, 10.20, 12.85, 15.35] + source: https://www.legislation.gov.uk/wsi/2021/34/made + 2022: + thresholds: [0, 224.00, 389.00, 484.00] + amounts: [5.30, 10.60, 13.35, 15.95] + source: https://www.legislation.gov.uk/wsi/2022/51/made + 2023: + thresholds: [0, 236.00, 410.00, 511.00] + amounts: [5.45, 10.90, 13.70, 16.40] + source: https://www.legislation.gov.uk/wsi/2023/47/made + 2024: + thresholds: [0, 256.00, 445.00, 554.00] + amounts: [5.80, 11.55, 14.50, 17.35] + source: https://www.legislation.gov.uk/wsi/2024/56/made + 2025: + thresholds: [0, 266.00, 463.00, 577.00] + amounts: [6.25, 12.45, 15.65, 18.70] + source: https://www.legislation.gov.uk/wsi/2025/59/made + 2026: + thresholds: [0, 279.00, 485.00, 605.00] + amounts: [6.70, 13.35, 16.80, 20.05] + source: https://www.legislation.gov.uk/wsi/2026/13/made +scotland: + parameter: gov.local_authorities.scotland.council_tax_reduction.non_dep_deduction.amount + years: + 2013: + thresholds: [0, 186.00, 322.00, 401.00] + amounts: [3.65, 7.25, 9.15, 10.95] + source: https://www.legislation.gov.uk/ssi/2013/49/made + 2014: + thresholds: [0, 188.00, 326.00, 406.00] + amounts: [3.75, 7.50, 9.50, 11.35] + source: https://www.legislation.gov.uk/ssi/2014/35/made + 2015: + thresholds: [0, 189.00, 328.00, 408.00] + amounts: [3.85, 7.70, 9.80, 11.70] + source: https://www.legislation.gov.uk/ssi/2015/46/made + 2016: + thresholds: [0, 195.00, 338.00, 420.00] + amounts: [3.90, 7.75, 9.85, 11.80] + source: https://www.legislation.gov.uk/ssi/2016/81/made + 2017: + thresholds: [0, 200.00, 346.00, 430.00] + amounts: [3.95, 7.85, 9.95, 11.90] + source: https://www.legislation.gov.uk/ssi/2017/41/made + 2018: + thresholds: [0, 204.00, 354.00, 439.00] + amounts: [4.05, 8.10, 10.25, 12.25] + source: https://www.legislation.gov.uk/ssi/2018/69/made + 2019: + thresholds: [0, 209.00, 363.00, 451.00] + amounts: [4.15, 8.30, 10.50, 12.55] + source: https://www.legislation.gov.uk/ssi/2019/29/made + 2020: + thresholds: [0, 213.00, 370.00, 458.00] + amounts: [4.25, 8.45, 10.70, 12.80] + source: https://www.legislation.gov.uk/ssi/2020/25/made + 2021: + thresholds: [0, 213.00, 370.00, 458.00] + amounts: [4.30, 8.50, 10.80, 12.90] + source: https://www.legislation.gov.uk/ssi/2021/51/made + 2022: + thresholds: [0, 228.00, 396.00, 490.00] + amounts: [4.45, 8.80, 11.15, 13.30] + source: https://www.legislation.gov.uk/ssi/2022/52/made + 2023: + thresholds: [0, 241.00, 418.00, 517.00] + amounts: [4.90, 9.70, 12.30, 14.65] + source: https://www.legislation.gov.uk/ssi/2023/38/made + 2024: + thresholds: [0, 260.00, 451.00, 558.00] + amounts: [5.25, 10.35, 13.15, 15.65] + source: https://www.legislation.gov.uk/ssi/2024/35/made + 2025: + thresholds: [0, 273.00, 474.00, 586.00] + amounts: [5.35, 10.55, 13.40, 15.95] + source: https://www.legislation.gov.uk/ssi/2025/24/made + 2026: + thresholds: [0, 286.00, 497.00, 614.00] + amounts: [5.55, 10.95, 13.90, 16.55] + source: https://www.legislation.gov.uk/ssi/2026/56/made diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 49c22bda96..41919b3d7c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -1,31 +1,189 @@ -- name: Reduced rent without LHA - period: 2025 +# 2026-27 deductions (HB Regs 2006 reg 74(1)-(2); SI 2006/214 reg 55(1)-(2); +# up-rated by SI 2026/148 art 23(3)). In remunerative work (16 hours or more, +# reg 6(1)): under £192 a week 20.40; not less than £192 46.85; £279 64.35; +# £365 105.20; £485 119.85; £605 and above 131.45. Not in remunerative work: +# 20.40 (reg 74(1)(b)). Bands include their lower edge ("not less than"). + +- name: In remunerative work with no income pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 0 + weekly_hours: 35 + output: + # reg 74(2)(a) and (1)(b) + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: Just below the second band + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 9983.48 + weekly_hours: 35 + output: + # 191.99 a week + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: Exactly 192 a week is in the band starting at 192 + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 9984 + weekly_hours: 35 + output: + household_benefits_individual_non_dep_deduction: 46.85 * 52 + +- name: Exactly 279 a week is in the band starting at 279 + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 14508 + weekly_hours: 35 + output: + household_benefits_individual_non_dep_deduction: 64.35 * 52 + +- name: Exactly 365 a week is in the band starting at 365 + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 5_200 # 100 weekly + age: 30 + total_income: 18980 + weekly_hours: 35 output: - household_benefits_individual_non_dep_deduction: 19.3 * 52 + household_benefits_individual_non_dep_deduction: 105.2 * 52 -- name: Higher amount - period: 2025 +- name: Exactly 485 a week is in the band starting at 485 + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 28_912 # 556 weekly + age: 30 + total_income: 25220 + weekly_hours: 35 output: - household_benefits_individual_non_dep_deduction: 124.55 * 52 + household_benefits_individual_non_dep_deduction: 119.85 * 52 -- name: Middle amount - period: 2025 +- name: Exactly 605 a week is in the band starting at 605 + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 17_420 # 335 weekly + age: 30 + total_income: 31460 + weekly_hours: 35 output: - household_benefits_individual_non_dep_deduction: 99.65 * 52 + household_benefits_individual_non_dep_deduction: 131.45 * 52 -- name: Exclusive brackets - period: 2025 +- name: Just below the top band + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 9_153 # 176 weekly + age: 30 + total_income: 31459.48 + weekly_hours: 35 + output: + # 604.99 a week + household_benefits_individual_non_dep_deduction: 119.85 * 52 + +- name: Not in remunerative work pays the lowest amount whatever the income + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 40000 + weekly_hours: 0 + output: + # reg 74(1)(b): the bands apply only in remunerative work + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: Just under 16 hours is not remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 40000 + weekly_hours: 15.9 + output: + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: 16 hours is remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 40000 + weekly_hours: 16 + output: + household_benefits_individual_non_dep_deduction: 131.45 * 52 + +- name: A loss counts as no income + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: -5000 + weekly_hours: 35 + output: + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: A couple's joint income sets the band for the member in remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + people: + worker: + age: 30 + total_income: 10_400 # 200 a week + weekly_hours: 35 + housing_benefit_individual_non_dep_deduction_eligible: true + partner: + age: 30 + total_income: 10_400 + weekly_hours: 0 + housing_benefit_individual_non_dep_deduction_eligible: true + benunits: + couple: + members: [worker, partner] + households: + household: + members: [worker, partner] + output: + # reg 74(4): joint income 400 a week is in the 365 band; the partner, not + # in remunerative work, would pay 20.40 (reg 74(1)(b)). + household_benefits_individual_non_dep_deduction: [5_470.40, 1_060.80] + +- name: A dependent child's income does not count towards the couple's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + total_income: 9_360 # 180 a week + weekly_hours: 35 + housing_benefit_individual_non_dep_deduction_eligible: true + child: + age: 17 + total_income: 5_200 + benunits: + family: + members: [parent, child] + households: + household: + members: [parent, child] output: - household_benefits_individual_non_dep_deduction: 44.4 * 52 + household_benefits_individual_non_dep_deduction: [1_060.80, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml index 0307c37986..e27118fd0b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml @@ -3,6 +3,12 @@ input: benunit_is_rent_liable: false age: 18 + # Without this the age-based default puts an 18-year-old in tertiary + # education, and full-time students are exempt (reg 74(7)(c)). + current_education: NOT_IN_EDUCATION + # An 18-year-old with no income is otherwise entitled to UC without + # earned income, which exempts them (reg 74(8)(b)). + would_claim_uc: false output: housing_benefit_individual_non_dep_deduction_eligible: true @@ -21,3 +27,12 @@ age: 18 output: housing_benefit_individual_non_dep_deduction_eligible: false + +- name: Exempt non-dependant is ineligible + period: 2025 + input: + benunit_is_rent_liable: false + age: 20 + current_education: TERTIARY + output: + housing_benefit_individual_non_dep_deduction_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml new file mode 100644 index 0000000000..99aa29abf3 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml @@ -0,0 +1,90 @@ +# Housing Benefit with a non-dependant, end to end through eligibility, +# exemption, banding and aggregation. Means-test inputs are fixed so the +# award is the eligible rent less the non-dependant deductions. + +- name: A zero-income full-time student does not reduce the claimant's HB + # The counterexample from the PolicyEngine/policyengine-uk#1925 review: + # HB Regs 2006 reg 74(7)(c)-(d). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + tenant: + age: 40 + total_income: 0 + student: + age: 20 + total_income: 0 + employment_status: STUDENT + current_education: TERTIARY + benunits: + tenant_unit: + members: [tenant] + benunit_is_rent_liable: true + housing_benefit_eligible: true + would_claim_housing_benefit: true + benunit_rent: 6_000 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + student_unit: + members: [student] + benunit_is_rent_liable: false + housing_benefit_eligible: false + would_claim_housing_benefit: false + benunit_rent: 0 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + households: + home: + members: [tenant, student] + output: + housing_benefit_non_dep_deduction_exempt: [false, true] + household_benefits_individual_non_dep_deduction: [0, 0] + housing_benefit_non_dep_deductions: [0, 0] + housing_benefit: [6_000, 0] + +- name: A zero-income non-student non-dependant pays the lowest deduction + # 2026 rather than 2025 so the amount is the same before and after + # PolicyEngine/policyengine-uk#1925 (which corrects the 2025-26 rates). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + tenant: + age: 40 + total_income: 0 + son: + age: 30 + total_income: 0 + current_education: NOT_IN_EDUCATION + benunits: + tenant_unit: + members: [tenant] + benunit_is_rent_liable: true + housing_benefit_eligible: true + would_claim_housing_benefit: true + benunit_rent: 6_000 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + son_unit: + members: [son] + benunit_is_rent_liable: false + housing_benefit_eligible: false + would_claim_housing_benefit: false + benunit_rent: 0 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + households: + home: + members: [tenant, son] + output: + # reg 74(1)(b): 20.40 a week in 2026-27. + housing_benefit_non_dep_deductions: [1_060.80, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml new file mode 100644 index 0000000000..5699336677 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml @@ -0,0 +1,119 @@ +# HB Regs 2006 (SI 2006/213) reg 74(7)-(8) and (10); SI 2006/214 reg 55(7)-(9). + +- name: Full-time tertiary student is exempt + period: 2026 + input: + age: 20 + would_claim_uc: false + current_education: TERTIARY + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Full-time student in non-advanced education is exempt + period: 2026 + input: + age: 18 + would_claim_uc: false + current_education: UPPER_SECONDARY + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Higher education flag alone makes a student exempt + period: 2026 + input: + age: 30 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + in_HE: true + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: A worker not in education is not exempt + period: 2026 + input: + age: 30 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + employment_income: 20_000 + output: + housing_benefit_non_dep_deduction_exempt: false + +- name: State Pension Credit recipient is exempt at any age + period: 2026 + input: + age: 70 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + pension_credit: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Income Support under 25 is exempt + period: 2026 + input: + age: 24 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + income_support: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Income Support at 25 is not exempt + period: 2026 + input: + age: 25 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + income_support: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: false + +- name: Income-based JSA under 25 is exempt + period: 2026 + input: + age: 24 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + jsa_income: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Income-related ESA under 25 is not exempt without an assessment-phase input + period: 2026 + input: + age: 24 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + esa_income: 1_000 + output: + # reg 74(8)(a) exempts only ESA(IR) without the support component and + # outside the work-related activity group; the model has no ESA group input. + housing_benefit_non_dep_deduction_exempt: false + +- name: Universal Credit without earned income under 25 is exempt + period: 2026 + input: + age: 24 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Universal Credit with earned income under 25 is not exempt + period: 2026 + input: + age: 24 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 1_000 + employment_income: 2_000 + output: + housing_benefit_non_dep_deduction_exempt: false + +- name: Universal Credit without earned income at 25 is not exempt + period: 2026 + input: + age: 25 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 46c2b6ac44..071c6610c8 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -59,3 +59,48 @@ members: [person_1, person_2, person_3] output: housing_benefit_non_dep_deductions: [5, 4, 3] + +- name: One deduction for a non-dependant couple, the higher amount + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + household_benefits_individual_non_dep_deduction: 0 + non_dep_1: + household_benefits_individual_non_dep_deduction: 1_060.80 + non_dep_2: + household_benefits_individual_non_dep_deduction: 5_470.40 + benunits: + claimant_unit: + members: [claimant] + couple: + members: [non_dep_1, non_dep_2] + households: + household: + members: [claimant, non_dep_1, non_dep_2] + output: + # reg 74(3); the couple's own unit has no non-dependants. + housing_benefit_non_dep_deductions: [5_470.40, 0] + +- name: No deductions where the claimant is exempt + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + household_benefits_individual_non_dep_deduction: 0 + non_dep: + household_benefits_individual_non_dep_deduction: 1_060.80 + benunits: + claimant_unit: + members: [claimant] + housing_benefit_non_dep_deductions_claimant_exempt: true + non_dep_unit: + members: [non_dep] + housing_benefit_non_dep_deductions_claimant_exempt: false + households: + household: + members: [claimant, non_dep] + output: + housing_benefit_non_dep_deductions: [0, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml new file mode 100644 index 0000000000..f4de59feff --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml @@ -0,0 +1,77 @@ +# HB Regs 2006 (SI 2006/213) reg 74(6); SI 2006/214 reg 55(6). + +- name: Blind claimant + period: 2026 + input: + age: 40 + is_blind: true + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Claimant on Attendance Allowance + period: 2026 + input: + age: 70 + aa_category: LOWER + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Partner on the DLA care component + period: 2026 + input: + people: + claimant: + age: 40 + partner: + age: 38 + dla_sc_category: LOWER + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Claimant on the PIP daily living component + period: 2026 + input: + age: 40 + pip_dl_category: STANDARD + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Claimant on Armed Forces Independence Payment + period: 2026 + input: + age: 40 + armed_forces_independence_payment: 1_000 + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: A child's DLA care component does not exempt the claimant + period: 2026 + input: + people: + claimant: + age: 40 + child: + age: 10 + dla_sc_category: HIGHER + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + output: + housing_benefit_non_dep_deductions_claimant_exempt: false + +- name: DLA mobility only does not exempt the claimant + period: 2026 + input: + age: 40 + dla_m_category: HIGHER + output: + housing_benefit_non_dep_deductions_claimant_exempt: false diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml new file mode 100644 index 0000000000..31dd42ed94 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml @@ -0,0 +1,1312 @@ +# National Council Tax Reduction schemes' own non-dependant deductions: +# England pensioners SI 2012/2885 Sch 1 para 8; Wales WSI 2013/3029 Sch 1 +# para 3 and Sch 6 para 5; Scotland SSI 2012/319 reg 48 and SSI 2021/249 +# reg 90. 2026-27 weekly sums, with bands including their lower edge: +# England: under 279 5.20; 279 10.60; 485 13.30; 605 and above 15.95. +# Wales: under 279 6.70; 279 13.35; 485 16.80; 605 and above 20.05. +# Scotland: under 286 5.55; 286 10.95; 497 13.90; 614 and above 16.55. +# A non-dependant not in remunerative work (under 16 hours) pays the lowest. +# The applicant is the benefit unit with the household's oldest member, so +# working-age applicants here are 60. + +- name: England in remunerative work with no income pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 0 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: England just below 279 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 14507.48 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # 278.99 a week + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: England exactly 279 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 14508 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: England exactly 485 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 25220 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 691.60] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: England just below 605 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 31459.48 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # 604.99 a week + council_tax_reduction_individual_non_dep_deduction: [0, 691.60] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: England exactly 605 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 31460 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 829.40] + council_tax_reduction_non_dep_deductions: [829.40, 0] + +- name: England not in remunerative work pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 40000 + weekly_hours: 15.9 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: England 2025-26 top amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 30004 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 798.20] + council_tax_reduction_non_dep_deductions: [798.20, 0] + +- name: Scotland pensioner claimant, below 286 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 14871.48 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + # 285.99 a week + council_tax_reduction_individual_non_dep_deduction: [0, 288.60] + council_tax_reduction_non_dep_deductions: [288.60, 0] + +- name: Scotland pensioner claimant, exactly 286 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 14872 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 569.40] + council_tax_reduction_non_dep_deductions: [569.40, 0] + +- name: Scotland working-age claimant, exactly 497 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 25844 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 722.80] + council_tax_reduction_non_dep_deductions: [722.80, 0] + +- name: Scotland working-age claimant, exactly 614 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 31928 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 860.60] + council_tax_reduction_non_dep_deductions: [860.60, 0] + +- name: Scotland 2025-26 lowest amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 40000 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + +- name: Wales pensioner claimant, not in remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 40000 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 348.40] + council_tax_reduction_non_dep_deductions: [348.40, 0] + +- name: Wales pensioner claimant, exactly 279 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 14508 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 694.20] + council_tax_reduction_non_dep_deductions: [694.20, 0] + +- name: Wales working-age claimant, exactly 485 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 25220 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 873.60] + council_tax_reduction_non_dep_deductions: [873.60, 0] + +- name: Wales working-age claimant, exactly 605 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 31460 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 1_042.60] + council_tax_reduction_non_dep_deductions: [1_042.60, 0] + +- name: Wales 2025-26 lowest amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 325.00] + council_tax_reduction_non_dep_deductions: [325.00, 0] + +- name: English working-age households have no national deduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 36400 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # Unsupported English working-age local schemes keep reported CTB. + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A full-time student is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 20 + current_education: TERTIARY + total_income: 20000 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # SI 2012/2885 Sch 1 para 8(7)(c) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A 40-year-old on Income Support is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 1000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(8)(a) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A 40-year-old on income-based JSA is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 1000 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A 40-year-old on income-related ESA is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 1000 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A non-dependant on State Pension Credit is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 70 + total_income: 9000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: Universal Credit without earned income is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(8)(c) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: Universal Credit with earned income is not exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + employment_income: 2000 + weekly_hours: 10 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Scotland Income Support exemption applies at working age too + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 1000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + # SSI 2021/249 reg 90(8)(a) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: "England: no Universal Credit exemption in 2014-15" + period: 2014 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # Parameters before 2015 are read at 1 January (PE-UK converts to fiscal + # years from 2015), so only the exemption is asserted here. + council_tax_reduction_non_dep_deduction_exempt: [false, false] + +- name: "England: Universal Credit exemption from 2015-16" + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # inserted by SI 2014/3312 + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: "Wales: no Universal Credit exemption in 2014-15" + period: 2014 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # Parameters before 2015 are read at 1 January (PE-UK converts to fiscal + # years from 2015), so only the exemption is asserted here. + council_tax_reduction_non_dep_deduction_exempt: [false, false] + +- name: "Wales: Universal Credit exemption from 2015-16" + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # added by WSI 2015/44 + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: "Scotland: no Universal Credit exemption in 2015-16" + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 200.20] + council_tax_reduction_non_dep_deductions: [200.20, 0] + +- name: "Scotland: Universal Credit exemption from 2016-17" + period: 2016 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + # inserted by SSI 2016/81 + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: No deduction where the applicant is blind + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_blind: True + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 36400 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(6)(a); the son's own amount is 15.95 a week, but none is deducted. + council_tax_reduction_individual_non_dep_deduction: [0, 829.40] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: "England: one deduction for a non-dependant couple, on their joint income" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 15600 + weekly_hours: 35 + partner: + age: 38 + current_education: NOT_IN_EDUCATION + total_income: 0 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son, partner] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son, partner] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(3)-(4): joint 300 a week is in the 279 band (10.60); the partner + # not in remunerative work would pay 5.20; only the higher is deducted. + council_tax_reduction_individual_non_dep_deduction: [0, 551.20, 270.40] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: "Wales working age: both members of a couple on Universal Credit are deducted" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + employment_income: 15600 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + partner: + age: 38 + current_education: NOT_IN_EDUCATION + employment_income: 1000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son, partner] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son, partner] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # WSI 2013/3029 Sch 6 para 5(3): one deduction per couple "other than where + # there is an award of universal credit". Joint 319.23 a week: 13.35 and 6.70. + council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 348.40] + council_tax_reduction_non_dep_deductions: [1_042.60, 0] + +- name: "Wales pensioner: one deduction for a couple on Universal Credit" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + employment_income: 15600 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + partner: + age: 38 + current_education: NOT_IN_EDUCATION + employment_income: 1000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son, partner] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son, partner] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # Sch 1 para 3(3) has no Universal Credit exception. + council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 348.40] + council_tax_reduction_non_dep_deductions: [694.20, 0] + +- name: A Pension Credit pensioner living with a working son (issue 1934) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + son: + age: 40 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + son_benunit: + members: [son] + would_claim_uc: false + households: + household: + members: [pensioner, son] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + in_receipt_of_guarantee_credit: [true, false] + # 30,000 / 52 = 576.92 a week, in the 485 band: 13.30 a week, 691.60 a + # year. On the Housing Benefit scale it was 119.85 a week, which took the + # whole 2,000. + council_tax_reduction_non_dep_deductions: [691.60, 0] + council_tax_reduction: 1_308.40 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..b1e4be9ded 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 @@ -245,14 +245,12 @@ guarantee_credit: [3_376, 0] in_receipt_of_guarantee_credit: [true, false] council_tax_reduction_applicable_income: [0, 5_000] - # The national schemes' non-dependant deduction currently uses Housing - # Benefit amounts (20.40 a week here), not the CTR schedules' own amounts - # and exemptions (SI 2012/2885 Sch 1 para 8); this pins that the deduction - # still applies on guarantee credit, not its size. - council_tax_reduction_non_dep_deductions: [1_060.80, 0] - # 2,000 - 1,060.80 - simulated_council_tax_reduction_benunit: [939.20, 0] - council_tax_reduction: 939.20 + # SI 2012/2885 Sch 1 para 8(1)(b): the son has no hours, so he is not in + # remunerative work and pays the 2026-27 lowest amount, 5.20 a week. + council_tax_reduction_non_dep_deductions: [270.40, 0] + # 2,000 - 270.40 + simulated_council_tax_reduction_benunit: [1_729.60, 0] + council_tax_reduction: 1_729.60 - name: Mixed-age couple is not in receipt of guarantee credit, so the capital limit applies period: 2026 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py new file mode 100644 index 0000000000..66fd8dc9d4 --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -0,0 +1,545 @@ +"""Property-based and exhaustive tests for the Housing Benefit and national +Council Tax Reduction non-dependant deductions. + +Law: +- Housing Benefit: SI 2006/213 reg 74 (working age) and SI 2006/214 reg 55 + (pension age). +- Council Tax Reduction: SI 2012/2885 Sch 1 para 8 (England, pensioners); + WSI 2013/3029 Sch 1 para 3 and Sch 6 para 5 (Wales); SSI 2012/319 reg 48 and + SSI 2021/249 reg 90 (Scotland). + +Invariants, for any generated population of households, each a claimant family +and a non-dependant family of one or two adults: + +1. An exempt non-dependant's individual deduction is 0, for Housing Benefit and + for Council Tax Reduction. +2. Bounds: an eligible non-dependant who is not exempt pays between the + scale's lowest and highest weekly amounts. In English working-age + households (local schemes) the national CTR deduction is 0. +3. A non-dependant not in remunerative work (under 16 hours) pays the lowest + amount, whatever their income. +4. If the claimant or partner is exempt, the claimant family's deductions + are 0. +5. Aggregation: a family's deductions are the sum, over the other families in + the household, of their higher member's amount (both members' amounts for a + Welsh working-age applicant's non-dependant couple with a Universal Credit + award), recomputed here from the individual amounts. +6. Monotonicity (metamorphic): raising any non-dependant's income never lowers + an individual deduction or a family's deductions. +7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, + including uprated years, an income at a band's lower edge gets that band's + amount and the next lower representable income gets the band below's. +8. Differential: each CTR scale's parameter equals the enacted sums in + tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml. +9. Differential: each exemption flag equals a reading of the provisions + straight from the generated inputs. Housing Benefit: a full-time student, + a State Pension Credit recipient, or under 25 and on Income Support, + income-based JSA or Universal Credit without earned income. CTR (any age): + a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national + scheme, on Universal Credit without earned income. +""" + +from pathlib import Path + +import numpy as np +import pytest +import yaml +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import CountryTaxBenefitSystem, Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Local authorities without a modelled local scheme. +COUNTRIES = { + "ENGLAND": "MAIDSTONE", + "WALES": "CARDIFF", + "SCOTLAND": "GLASGOW_CITY", +} +CTR_SCALES = { + "ENGLAND": "england.council_tax_reduction.pensioners.non_dep_deduction", + "WALES": "wales.council_tax_reduction.non_dep_deduction", + "SCOTLAND": "scotland.council_tax_reduction.non_dep_deduction", +} +FIXTURE = ( + Path(__file__).parent + / "fixtures" + / "council_tax_reduction_non_dependant_deductions.yaml" +) +WEEKS = 52 +CLAIMANT_DISABILITY = [ + None, + None, + None, + "is_blind", + "attendance_allowance", + "dla_sc", + "pip_dl", + "armed_forces_independence_payment", +] +BENEFITS = [ + "income_support", + "jsa_income", + "esa_income", + "pension_credit", + "universal_credit_pre_benefit_cap", +] +SYSTEM = CountryTaxBenefitSystem() + + +def for_years(situation, years): + """Key every input by year: unkeyed inputs apply only to the default + period.""" + for group in situation.values(): + for entity in group.values(): + for key, value in entity.items(): + if key != "members" and not isinstance(value, dict): + entity[key] = {str(year): value for year in years} + return situation + + +@st.composite +def households(draw): + # The CTR applicant is the family with the household's oldest member, so + # claimants are older than their non-dependants. State Pension age is at + # most 67 in 2025-30. + claimant_age = draw(st.one_of(st.integers(50, 64), st.integers(68, 95))) + non_dependants = [ + dict( + age=draw(st.integers(18, claimant_age - 1)), + education=draw( + st.sampled_from( + ["NOT_IN_EDUCATION"] * 4 + ["TERTIARY", "UPPER_SECONDARY"] + ) + ), + earnings=draw(st.one_of(st.just(0.0), st.floats(0, 60_000))), + other_income=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + hours=draw(st.sampled_from([0.0, 10.0, 15.9, 16.0, 35.0, 45.0])), + income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + ) + for _ in range(draw(st.integers(1, 2))) + ] + return dict( + country=draw(st.sampled_from(sorted(COUNTRIES))), + claimant_age=claimant_age, + claimant_disability=draw(st.sampled_from(CLAIMANT_DISABILITY)), + benefits={ + benefit: draw(st.sampled_from([0.0, 0.0, 0.0, 1_000.0])) + for benefit in BENEFITS + }, + non_dependants=non_dependants, + ) + + +def build(population, year, raise_income=False): + people, benunits, households_, persons = {}, {}, {}, [] + for h, household in enumerate(population): + claimant = f"claimant_{h}" + flag = household["claimant_disability"] + people[claimant] = dict( + age=household["claimant_age"], + current_education="NOT_IN_EDUCATION", + total_income=0.0, + employment_income=0.0, + weekly_hours=0.0, + is_blind=flag == "is_blind", + **{ + name: 1_000.0 if flag == name else 0.0 + for name in CLAIMANT_DISABILITY[3:] + if name != "is_blind" + }, + ) + persons.append(dict(household=h, benunit=2 * h, non_dependant=None)) + members = [] + for n, nd in enumerate(household["non_dependants"]): + name = f"non_dependant_{h}_{n}" + other = nd["other_income"] + (nd["income_rise"] if raise_income else 0) + people[name] = dict( + age=nd["age"], + current_education=nd["education"], + total_income=nd["earnings"] + other, + employment_income=nd["earnings"], + weekly_hours=nd["hours"], + is_blind=False, + **{ + benefit: 0.0 + for benefit in CLAIMANT_DISABILITY[3:] + if benefit != "is_blind" + }, + ) + members.append(name) + persons.append(dict(household=h, benunit=2 * h + 1, non_dependant=nd)) + benunits[f"claimant_unit_{h}"] = dict( + members=[claimant], + benunit_is_rent_liable=True, + **{benefit: 0.0 for benefit in BENEFITS}, + ) + benunits[f"non_dependant_unit_{h}"] = dict( + members=members, + benunit_is_rent_liable=False, + **household["benefits"], + ) + households_[f"household_{h}"] = dict( + members=[claimant, *members], + country=household["country"], + local_authority=COUNTRIES[household["country"]], + ) + situation = dict(people=people, benunits=benunits, households=households_) + return Simulation(situation=for_years(situation, [year])), persons + + +def calculate(sim, year): + names = dict( + person=[ + "housing_benefit_non_dep_deduction_exempt", + "housing_benefit_individual_non_dep_deduction_eligible", + "household_benefits_individual_non_dep_deduction", + "council_tax_reduction_non_dep_deduction_exempt", + "council_tax_reduction_individual_non_dep_deduction_eligible", + "council_tax_reduction_individual_non_dep_deduction", + ], + benunit=[ + "housing_benefit_non_dep_deductions", + "housing_benefit_non_dep_deductions_claimant_exempt", + "council_tax_reduction_non_dep_deductions", + "universal_credit_pre_benefit_cap", + ], + household=[ + "council_tax_reduction_household_has_non_dep_exemption", + "council_tax_reduction_household_has_pensioner", + ], + ) + return { + name: np.asarray(sim.calculate(name, year)) + for entity in names.values() + for name in entity + } + + +def aggregate(individual, persons, benunit_count, each_member): + """Recompute family deductions from individual amounts (invariant 5).""" + by_benunit = {} + for i, person in enumerate(persons): + by_benunit.setdefault(person["benunit"], []).append(individual[i]) + own = np.array( + [ + (sum if each_member[b] else max)(by_benunit.get(b, [0.0])) + for b in range(benunit_count) + ] + ) + household_of = {p["benunit"]: p["household"] for p in persons} + totals = {} + for b in range(benunit_count): + totals[household_of[b]] = totals.get(household_of[b], 0.0) + own[b] + return np.array([totals[household_of[b]] - own[b] for b in range(benunit_count)]) + + +@PROPERTY_SETTINGS +@given( + population=st.lists(households(), min_size=1, max_size=12), + year=st.integers(2025, 2030), +) +@example( + # Rare under random draws: a Welsh working-age applicant's non-dependant + # couple on Universal Credit, both with earnings (Sch 6 para 5(3)); and + # the Housing Benefit under-25 boundary on Income Support (reg 74(8)). + population=[ + dict( + country="WALES", + claimant_age=60, + claimant_disability=None, + benefits={ + **dict.fromkeys(BENEFITS, 0.0), + "universal_credit_pre_benefit_cap": 1_000.0, + }, + non_dependants=[ + dict( + age=30, + education="NOT_IN_EDUCATION", + earnings=15_600.0, + other_income=0.0, + hours=35.0, + income_rise=0.0, + ), + dict( + age=28, + education="NOT_IN_EDUCATION", + earnings=1_000.0, + other_income=0.0, + hours=0.0, + income_rise=5_000.0, + ), + ], + ), + *[ + dict( + country="ENGLAND", + claimant_age=70, + claimant_disability=None, + benefits={**dict.fromkeys(BENEFITS, 0.0), "income_support": 1_000.0}, + non_dependants=[ + dict( + age=age, + education="NOT_IN_EDUCATION", + earnings=0.0, + other_income=0.0, + hours=0.0, + income_rise=0.0, + ) + ], + ) + for age in [24, 25] + ], + ], + year=2026, +) +def test_non_dependant_deduction_invariants(population, year): + sim, persons = build(population, year) + r = calculate(sim, year) + p = SYSTEM.parameters(str(year)) + hb = p.gov.dwp.housing_benefit.non_dep_deduction + hb_lowest, hb_top = hb.amount.amounts[0] * WEEKS, hb.amount.amounts[-1] * WEEKS + pension_age = np.repeat( + r["council_tax_reduction_household_has_pensioner"], + [1 + len(h["non_dependants"]) for h in population], + ) + + for i, person in enumerate(persons): + household = population[person["household"]] + hb_deduction = r["household_benefits_individual_non_dep_deduction"][i] + ctr_deduction = r["council_tax_reduction_individual_non_dep_deduction"][i] + # 1. Exempt => 0. + if r["housing_benefit_non_dep_deduction_exempt"][i]: + assert hb_deduction == 0 + if r["council_tax_reduction_non_dep_deduction_exempt"][i]: + assert ctr_deduction == 0 + nd = person["non_dependant"] + if nd is None: + continue + # 9. The exemptions, read straight from the inputs. + benefits = household["benefits"] + student = nd["education"] != "NOT_IN_EDUCATION" + uc_without_earnings = ( + benefits["universal_credit_pre_benefit_cap"] > 0 and nd["earnings"] <= 0 + ) + hb_exempt = ( + student + or benefits["pension_credit"] > 0 + or ( + nd["age"] < 25 + and ( + benefits["income_support"] > 0 + or benefits["jsa_income"] > 0 + or uc_without_earnings + ) + ) + ) + assert bool(r["housing_benefit_non_dep_deduction_exempt"][i]) == hb_exempt + national = household["country"] != "ENGLAND" or pension_age[i] + ctr_exempt = ( + student + or any( + benefits[b] > 0 + for b in [ + "income_support", + "jsa_income", + "esa_income", + "pension_credit", + ] + ) + or (national and uc_without_earnings) + ) + assert ( + bool(r["council_tax_reduction_non_dep_deduction_exempt"][i]) == ctr_exempt + ) + # 2-3. Housing Benefit bounds and the not-in-work amount. + if ( + r["housing_benefit_individual_non_dep_deduction_eligible"][i] + and not r["housing_benefit_non_dep_deduction_exempt"][i] + ): + assert hb_lowest - 0.01 <= hb_deduction <= hb_top + 0.01 + if nd["hours"] < hb.remunerative_work_hours: + assert hb_deduction == pytest.approx(hb_lowest, abs=0.01) + # 2-3. Council Tax Reduction bounds and the not-in-work amount. + country = household["country"] + if not national: + assert ctr_deduction == 0 + elif ( + r["council_tax_reduction_individual_non_dep_deduction_eligible"][i] + and not r["council_tax_reduction_non_dep_deduction_exempt"][i] + ): + scale = p.gov.local_authorities + for part in CTR_SCALES[country].split("."): + scale = getattr(scale, part) + lowest = scale.amount.amounts[0] * WEEKS + top = scale.amount.amounts[-1] * WEEKS + assert lowest - 0.01 <= ctr_deduction <= top + 0.01 + if nd["hours"] < scale.remunerative_work_hours: + assert ctr_deduction == pytest.approx(lowest, abs=0.01) + + benunit_count = 2 * len(population) + # 4-5. Housing Benefit aggregation and the claimant exemption. + expected_hb = aggregate( + r["household_benefits_individual_non_dep_deduction"], + persons, + benunit_count, + [False] * benunit_count, + ) + expected_hb = np.where( + r["housing_benefit_non_dep_deductions_claimant_exempt"], 0, expected_hb + ) + np.testing.assert_allclose( + r["housing_benefit_non_dep_deductions"], expected_hb, atol=0.01 + ) + # 4-5. Council Tax Reduction aggregation and the applicant exemption. + welsh_working_age = [ + population[b // 2]["country"] == "WALES" + and not r["council_tax_reduction_household_has_pensioner"][b // 2] + and r["universal_credit_pre_benefit_cap"][b] > 0 + for b in range(benunit_count) + ] + expected_ctr = aggregate( + r["council_tax_reduction_individual_non_dep_deduction"], + persons, + benunit_count, + welsh_working_age, + ) + household_exempt = np.repeat( + r["council_tax_reduction_household_has_non_dep_exemption"], 2 + ) + expected_ctr = np.where(household_exempt, 0, expected_ctr) + np.testing.assert_allclose( + r["council_tax_reduction_non_dep_deductions"], expected_ctr, atol=0.01 + ) + + # 6. Raising non-dependants' incomes never lowers a deduction. + raised, _ = build(population, year, raise_income=True) + s = calculate(raised, year) + for name in [ + "household_benefits_individual_non_dep_deduction", + "council_tax_reduction_individual_non_dep_deduction", + "housing_benefit_non_dep_deductions", + "council_tax_reduction_non_dep_deductions", + ]: + assert np.all(s[name] >= r[name] - 1e-3), name + + +def first_income_at_or_above(threshold): + """The smallest float32 annual income (the input's storage type) whose + weekly value, computed as the model does (the benefit-unit sum returns + float64, then divided by 52), is at least the threshold.""" + + def weekly(annual): + return float(np.float32(annual)) / WEEKS + + income = np.float32(threshold * WEEKS) + while weekly(income) < threshold: + income = np.nextafter(income, np.float32(np.inf)) + while weekly(np.nextafter(income, np.float32(-np.inf))) >= threshold: + income = np.nextafter(income, np.float32(-np.inf)) + return income + + +SCALES = [ + ("HOUSING_BENEFIT", range(2019, 2031)), + ("ENGLAND", range(2015, 2031)), + ("WALES", range(2015, 2031)), + ("SCOTLAND", range(2015, 2031)), +] + + +@pytest.mark.parametrize("scale_name, years", SCALES, ids=[s for s, _ in SCALES]) +def test_band_edges_are_inclusive(scale_name, years): + """Invariant 7: every lower edge, every year, both schemes.""" + country = "ENGLAND" if scale_name == "HOUSING_BENEFIT" else scale_name + edges_by_year = {} + for year in years: + p = SYSTEM.parameters(str(year)) + if scale_name == "HOUSING_BENEFIT": + scale = p.gov.dwp.housing_benefit.non_dep_deduction.amount + else: + scale = p.gov.local_authorities + for part in CTR_SCALES[scale_name].split("."): + scale = getattr(scale, part) + scale = scale.amount + edges_by_year[year] = list( + zip(scale.thresholds[1:], scale.amounts[1:], scale.amounts[:-1]) + ) + count = max(len(edges) for edges in edges_by_year.values()) + people, benunits, households_ = {}, {}, {} + for k in range(2 * count): + claimant, non_dependant = f"claimant_{k}", f"non_dependant_{k}" + incomes = {} + for year, edges in edges_by_year.items(): + threshold, _, _ = edges[k // 2] + at_edge = first_income_at_or_above(threshold) + incomes[str(year)] = float( + at_edge if k % 2 == 0 else np.nextafter(at_edge, np.float32(-np.inf)) + ) + people[claimant] = dict(age=80, current_education="NOT_IN_EDUCATION") + people[non_dependant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + weekly_hours={str(y): 35.0 for y in years}, + total_income=incomes, + employment_income={str(y): 0.0 for y in years}, + ) + benunits[f"claimant_unit_{k}"] = dict( + members=[claimant], + benunit_is_rent_liable={str(y): True for y in years}, + **{b: {str(y): 0.0 for y in years} for b in BENEFITS}, + ) + benunits[f"non_dependant_unit_{k}"] = dict( + members=[non_dependant], + benunit_is_rent_liable={str(y): False for y in years}, + **{b: {str(y): 0.0 for y in years} for b in BENEFITS}, + ) + households_[f"household_{k}"] = dict( + members=[claimant, non_dependant], + country=country, + local_authority=COUNTRIES[country], + ) + sim = Simulation( + situation=for_years( + dict(people=people, benunits=benunits, households=households_), years + ) + ) + variable = ( + "household_benefits_individual_non_dep_deduction" + if scale_name == "HOUSING_BENEFIT" + else "council_tax_reduction_individual_non_dep_deduction" + ) + for year, edges in edges_by_year.items(): + deductions = np.asarray(sim.calculate(variable, year))[1::2] / WEEKS + for k, (threshold, amount, below) in enumerate(edges): + assert deductions[2 * k] == pytest.approx(amount, abs=1e-4), ( + year, + threshold, + ) + assert deductions[2 * k + 1] == pytest.approx(below, abs=1e-4), ( + year, + threshold, + ) + + +def test_ctr_scales_match_enacted_sums(): + """Invariant 8: the parameters reproduce the enacted table.""" + fixture = yaml.safe_load(FIXTURE.read_text()) + for scheme, entry in fixture.items(): + for year, row in entry["years"].items(): + # PolicyEngine UK reads parameters at 30 April from 2015; earlier + # years are read at that date directly. + instant = str(year) if year >= 2015 else f"{year}-04-30" + node = SYSTEM.parameters(instant) + for part in entry["parameter"].split("."): + node = getattr(node, part) + assert list(node.thresholds) == pytest.approx(row["thresholds"]), ( + scheme, + year, + ) + assert list(node.amounts) == pytest.approx(row["amounts"]), (scheme, year) 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 95b6bcb695..32dc5d9662 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 @@ -5,12 +5,30 @@ class household_benefits_individual_non_dep_deduction(Variable): value_type = float entity = Person label = "Housing Benefit individual non-dependent deduction" + documentation = ( + "A non-dependant in remunerative work pays the deduction for the band " + "containing their normal gross weekly income, counting a couple's " + "joint income; bands include their lower edge. A non-dependant not in " + "remunerative work pays the lowest amount. Gross income is taxable " + "total income, so the disregarded disability benefits are excluded." + ) 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 - weekly_income = person("total_income", period) / WEEKS_IN_YEAR - deduction = p.amount.calc(weekly_income, right=True) - return deduction * WEEKS_IN_YEAR + income = max_(0, person("total_income", period)) + counted = person("age", period) >= p.age_threshold + weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + in_remunerative_work = ( + person("weekly_hours", period) >= p.remunerative_work_hours + ) + banded = p.amount.calc(weekly_income) + not_in_remunerative_work = p.amount.calc(np.zeros_like(weekly_income)) + weekly = where(in_remunerative_work, banded, not_in_remunerative_work) + return weekly * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py index 37ea75a311..a02f8f8da8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py @@ -6,9 +6,14 @@ class housing_benefit_individual_non_dep_deduction_eligible(Variable): entity = Person label = "eligible person for the Housing Benefit non-dependent deduction" definition_period = YEAR + 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): rent_liable = person.benunit("benunit_is_rent_liable", period) p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction age_eligible = person("age", period) >= p.age_threshold - return age_eligible & ~rent_liable + exempt = person("housing_benefit_non_dep_deduction_exempt", period) + return age_eligible & ~rent_liable & ~exempt diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py new file mode 100644 index 0000000000..1c998c6694 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -0,0 +1,57 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + is_full_time_student_non_dep, +) + + +class housing_benefit_non_dep_deduction_exempt(Variable): + value_type = bool + entity = Person + label = "Housing Benefit makes no non-dependant deduction for this person" + documentation = ( + "Modelled exemptions: a full-time student (SI 2006/213 reg 74(7)(c)-(e); " + "SI 2006/214 reg 55(7)(e)); a non-dependant on State Pension Credit " + "(reg 74(10); reg 55(9)); and a non-dependant under 25 on Income " + "Support or income-based JSA, or entitled to Universal Credit " + "calculated on no earned income (reg 74(8); reg 55(8)). Not modelled: " + "a working student's summer vacation (reg 74(7)(d)), under-25s on " + "assessment-phase income-related ESA (no ESA group input), youth " + "training allowances, a normal home elsewhere, and absence in hospital, " + "prison or on armed forces operations." + ) + definition_period = YEAR + 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 + full_time_student = is_full_time_student_non_dep(person, period) + on_pension_credit = person.benunit("pension_credit", period) > 0 + on_legacy_income_related_benefit = ( + person.benunit("income_support", period) > 0 + ) | (person.benunit("jsa_income", period) > 0) + # Entitlement before the benefit cap: universal_credit itself depends on + # Housing Benefit through the cap. + entitled_to_universal_credit = ( + person.benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + earned_income = add( + person, period, ["employment_income", "self_employment_income"] + ) + universal_credit_without_earned_income = entitled_to_universal_credit & ( + earned_income <= 0 + ) + under_age_limit = person("age", period) < p.income_related_benefit_age_limit + return ( + full_time_student + | on_pension_credit + | ( + under_age_limit + & ( + on_legacy_income_related_benefit + | universal_credit_without_earned_income + ) + ) + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 3c35be82d2..86eb32437a 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -5,15 +5,29 @@ class housing_benefit_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "non-dependent deductions" + documentation = ( + "Deductions for the non-dependants in other benefit units of the " + "household: one per couple, the higher of the two members' amounts, " + "and none if the claimant or partner is exempt." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) def formula(benunit, period, parameters): deductions = benunit.members( "household_benefits_individual_non_dep_deduction", period ) - non_dep_deductions_in_hh = benunit.max( - benunit.members.household.sum(deductions) + deduction_for_benunit = benunit.max(deductions) + is_benunit_head = benunit.members("is_benunit_head", period) + counted = is_benunit_head * benunit.project(deduction_for_benunit) + deductions_in_household = benunit.max(benunit.members.household.sum(counted)) + claimant_exempt = benunit( + "housing_benefit_non_dep_deductions_claimant_exempt", period + ) + return where( + claimant_exempt, 0, deductions_in_household - deduction_for_benunit ) - non_dep_deductions_in_bu = benunit.sum(deductions) - return non_dep_deductions_in_hh - non_dep_deductions_in_bu diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py new file mode 100644 index 0000000000..4568e9f714 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_non_dep_deductions_claimant_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Housing Benefit claimant exempt from all non-dependant deductions" + documentation = ( + "No deduction is made for any non-dependant if the claimant or partner " + "is blind or receives Attendance Allowance, the DLA care component, the " + "PIP daily living component or Armed Forces Independence Payment (SI " + "2006/213 reg 74(6); SI 2006/214 reg 55(6)). Scottish disability " + "assistance (pension age and adult disability payments, child " + "disability payment, Scottish adult DLA) has no separate input." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) + + def formula(benunit, period, parameters): + person = benunit.members + # Claimant or partner as in council_tax_reduction_household_has_non_dep_exemption. + claimant_or_partner = person("is_adult", period) + qualifying = ( + person("is_blind", period) + | (person("attendance_allowance", period) > 0) + | (person("dla_sc", period) > 0) + | (person("pip_dl", period) > 0) + | (person("armed_forces_independence_payment", period) > 0) + ) + return benunit.any(claimant_or_partner & qualifying) 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 102c093f63..fea66dd381 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 @@ -1,15 +1,86 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_england_pensioner_scheme, + is_scotland_scheme, + is_wales_scheme, +) class council_tax_reduction_individual_non_dep_deduction(Variable): value_type = float entity = Person label = "CTR individual non-dependent deduction" + documentation = ( + "The England pensioner, Scottish and Welsh schemes' own weekly scales. " + "A non-dependant in remunerative work pays the deduction for the band " + "containing their normal gross weekly income, counting a couple's " + "joint income; bands include their lower edge. A non-dependant not in " + "remunerative work pays the lowest amount; an exempt non-dependant " + "pays nothing. Gross income is taxable total income, so the " + "disregarded disability benefits are excluded. English working-age " + "local schemes have their own variables." + ) 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", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + ) def formula(person, period, parameters): - p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction - weekly_total_income = person("total_income", period) / WEEKS_IN_YEAR - return p.amount.calc(weekly_total_income, right=True) * WEEKS_IN_YEAR + local_authorities = parameters(period).gov.local_authorities + england = ( + local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction + ) + scotland = local_authorities.scotland.council_tax_reduction.non_dep_deduction + wales = local_authorities.wales.council_tax_reduction.non_dep_deduction + country = person.household("country", period) + has_pensioner = person.household( + "council_tax_reduction_household_has_pensioner", period + ) + schemes = [ + is_england_pensioner_scheme(country, has_pensioner), + is_scotland_scheme(country), + is_wales_scheme(country), + ] + income = max_(0, person("total_income", period)) + counted = person("age", period) >= 18 + weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + no_income = np.zeros_like(weekly_income) + remunerative_work_hours = select( + schemes, + [ + england.remunerative_work_hours, + scotland.remunerative_work_hours, + wales.remunerative_work_hours, + ], + default=np.inf, + ) + in_remunerative_work = person("weekly_hours", period) >= remunerative_work_hours + banded = select( + schemes, + [ + england.amount.calc(weekly_income), + scotland.amount.calc(weekly_income), + wales.amount.calc(weekly_income), + ], + default=0, + ) + not_in_remunerative_work = select( + schemes, + [ + england.amount.calc(no_income), + scotland.amount.calc(no_income), + wales.amount.calc(no_income), + ], + default=0, + ) + weekly = where(in_remunerative_work, banded, not_in_remunerative_work) + # The local schemes share the eligibility variable and apply their own + # exemptions, so the national ones apply here. + exempt = person("council_tax_reduction_non_dep_deduction_exempt", period) + return where(exempt, 0, weekly * WEEKS_IN_YEAR) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py new file mode 100644 index 0000000000..004237bc6a --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -0,0 +1,80 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + is_full_time_student_non_dep, +) +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_england_pensioner_scheme, + is_scotland_scheme, + is_wales_scheme, +) + + +class council_tax_reduction_non_dep_deduction_exempt(Variable): + value_type = bool + entity = Person + label = "national Council Tax Reduction makes no non-dependant deduction for this person" + documentation = ( + "Modelled exemptions in the England pensioner, Scottish and Welsh " + "schemes: a full-time student; a non-dependant on Income Support, " + "income-based JSA, income-related ESA or State Pension Credit (any " + "age); and, from the year each scheme added it, one entitled to " + "Universal Credit calculated on no earned income. Wales excludes " + "members of the ESA work-related activity group; with no ESA group " + "input, income-related ESA is treated as exempt. Not modelled: persons " + "disregarded for council tax discounts (LGFA 1992 Sch 1) other than " + "students, youth training allowances, a normal home elsewhere, and " + "absence in hospital or on armed forces operations." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + ) + + def formula(person, period, parameters): + local_authorities = parameters(period).gov.local_authorities + england = local_authorities.england.council_tax_reduction.pensioners + scotland = local_authorities.scotland.council_tax_reduction + wales = local_authorities.wales.council_tax_reduction + country = person.household("country", period) + has_pensioner = person.household( + "council_tax_reduction_household_has_pensioner", period + ) + schemes = [ + is_england_pensioner_scheme(country, has_pensioner), + is_scotland_scheme(country), + is_wales_scheme(country), + ] + full_time_student = is_full_time_student_non_dep(person, period) + on_income_related_benefit = ( + (person.benunit("income_support", period) > 0) + | (person.benunit("jsa_income", period) > 0) + | (person.benunit("esa_income", period) > 0) + | (person.benunit("pension_credit", period) > 0) + ) + universal_credit_limb = select( + schemes, + [ + england.non_dep_deduction.exempt_universal_credit_without_earned_income, + scotland.non_dep_deduction.exempt_universal_credit_without_earned_income, + wales.non_dep_deduction.exempt_universal_credit_without_earned_income, + ], + default=False, + ) + entitled_to_universal_credit = ( + person.benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + earned_income = add( + person, period, ["employment_income", "self_employment_income"] + ) + universal_credit_without_earned_income = ( + universal_credit_limb & entitled_to_universal_credit & (earned_income <= 0) + ) + return ( + full_time_student + | on_income_related_benefit + | universal_credit_without_earned_income + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index c6b5fc1d8d..4384831a88 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -1,17 +1,55 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_wales_scheme, +) class council_tax_reduction_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "CTR non-dependent deductions" + documentation = ( + "Deductions for the non-dependants in other benefit units of the " + "household: one per couple, the higher of the two members' amounts " + "(both, in the Welsh scheme for people who are not pensioners, when " + "the couple has a Universal Credit award), and none if the applicant " + "or partner is exempt." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + ) def formula(benunit, period, parameters): deductions = benunit.members( "council_tax_reduction_individual_non_dep_deduction", period ) - deductions_in_household = benunit.max(benunit.members.household.sum(deductions)) - deductions_in_benunit = benunit.sum(deductions) - return deductions_in_household - deductions_in_benunit + country = benunit.household("country", period) + has_pensioner = benunit.household( + "council_tax_reduction_household_has_pensioner", period + ) + # WSI 2013/3029 Sch 6 para 5(3). + each_member_deducted = ( + is_wales_scheme(country) + & ~has_pensioner + & (benunit("universal_credit_pre_benefit_cap", period) > 0) + ) + deduction_for_benunit = where( + each_member_deducted, + benunit.sum(deductions), + benunit.max(deductions), + ) + is_benunit_head = benunit.members("is_benunit_head", period) + counted = is_benunit_head * benunit.project(deduction_for_benunit) + deductions_in_household = benunit.max(benunit.members.household.sum(counted)) + applicant_exempt = benunit.household( + "council_tax_reduction_household_has_non_dep_exemption", period + ) + return where( + applicant_exempt, 0, deductions_in_household - deduction_for_benunit + ) diff --git a/uv.lock b/uv.lock index 47b04e0136..1b1776e219 100644 --- a/uv.lock +++ b/uv.lock @@ -1471,7 +1471,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.102.3" +version = "2.104.2" source = { editable = "." } dependencies = [ { name = "microdf-python" }, From 86d7ae6e3b86da552610215894c5b31ce1fd4d94 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 02:25:54 -0400 Subject: [PATCH 02/31] Address review: claimant or partner, couple income, reg 6(6), earnings, gross income - Identify the claimant or partner as the benefit unit's head or any member who is not an SSCBA s.142 child or qualifying young person (HB Regs 2006 regs 2(1) and 19; CTR regs 2(1)), replacing is_adult in the HB and CTR claimant exemptions. - Band a couple on the couple's joint income only, give the couple one deduction and any other adult in the family their own, and count benefit receipt (IS, JSA(IB), ESA(IR), SPC, UC) only for the award's claimant or partner. - Treat a non-dependant on IS, JSA(IB) or ESA(IR) as not in remunerative work for HB (reg 6(6)). - Take self-employed losses as nil in the UC no-earned-income test (UC reg 57(2)). - Add the family's UC (before the cap), tax credits and child benefit to gross income. - Tests: the review's counterexamples, a non-vacuous SPC exemption case, and property-test generators and oracles for dependent young people and losses. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- ...benefits_individual_non_dep_deduction.yaml | 126 +++++++++++++ .../non_dependant_deduction_review_cases.yaml | 167 ++++++++++++++++++ ...ncil_tax_reduction_non_dep_deductions.yaml | 40 ++++- ...test_non_dependant_deduction_properties.py | 107 ++++++++--- .../non_dep_deduction/_non_dependants.py | 74 ++++++++ ...d_benefits_individual_non_dep_deduction.py | 25 ++- ...ousing_benefit_non_dep_deduction_exempt.py | 26 +-- .../housing_benefit_non_dep_deductions.py | 18 +- ...efit_non_dep_deductions_claimant_exempt.py | 6 +- ..._reduction_individual_non_dep_deduction.py | 17 +- ..._tax_reduction_non_dep_deduction_exempt.py | 17 +- ...ouncil_tax_reduction_non_dep_deductions.py | 21 +-- 13 files changed, 570 insertions(+), 76 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index f01a97f236..dbfa0aa46e 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge, only a non-dependant in remunerative work is banded (on a couple's joint income, one deduction per couple), the claimant-side and non-dependant exemptions apply, and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner identified by the SSCBA s.142 child and young person definitions; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 41919b3d7c..f21ab85587 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -3,6 +3,8 @@ # reg 6(1)): under £192 a week 20.40; not less than £192 46.85; £279 64.35; # £365 105.20; £485 119.85; £605 and above 131.45. Not in remunerative work: # 20.40 (reg 74(1)(b)). Bands include their lower edge ("not less than"). +# Family benefits (UC, tax credits, child benefit), which count as gross +# income, are held at zero except where a case tests them. - name: In remunerative work with no income pays the lowest amount period: 2026 @@ -12,6 +14,10 @@ age: 30 total_income: 0 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # reg 74(2)(a) and (1)(b) household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -24,6 +30,10 @@ age: 30 total_income: 9983.48 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # 191.99 a week household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -36,6 +46,10 @@ age: 30 total_income: 9984 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 46.85 * 52 @@ -47,6 +61,10 @@ age: 30 total_income: 14508 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 64.35 * 52 @@ -58,6 +76,10 @@ age: 30 total_income: 18980 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 105.2 * 52 @@ -69,6 +91,10 @@ age: 30 total_income: 25220 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 119.85 * 52 @@ -80,6 +106,10 @@ age: 30 total_income: 31460 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 131.45 * 52 @@ -91,6 +121,10 @@ age: 30 total_income: 31459.48 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # 604.99 a week household_benefits_individual_non_dep_deduction: 119.85 * 52 @@ -103,6 +137,10 @@ age: 30 total_income: 40000 weekly_hours: 0 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # reg 74(1)(b): the bands apply only in remunerative work household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -115,6 +153,10 @@ age: 30 total_income: 40000 weekly_hours: 15.9 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -126,6 +168,10 @@ age: 30 total_income: 40000 weekly_hours: 16 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 131.45 * 52 @@ -137,6 +183,10 @@ age: 30 total_income: -5000 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -158,6 +208,10 @@ benunits: couple: members: [worker, partner] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 households: household: members: [worker, partner] @@ -182,8 +236,80 @@ benunits: family: members: [parent, child] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 households: household: members: [parent, child] output: household_benefits_individual_non_dep_deduction: [1_060.80, 0] + +- name: Universal Credit counts towards gross income + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 40 + total_income: 14_040 # 270 a week + weekly_hours: 16 + universal_credit_pre_benefit_cap: 1_040 # 20 a week + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + # reg 74(2): 290 a week gross is in the 279 band (64.35). UC is not among + # the reg 74(9) disregards. + household_benefits_individual_non_dep_deduction: 64.35 * 52 + +- name: On income-related ESA, 16 hours is not remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 40 + total_income: 10_574.72 # 203.36 a week (supported permitted work) + weekly_hours: 16 + esa_income: 5_200 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + # reg 6(6): a person on income-related ESA is treated as not in + # remunerative work, so pays the reg 74(1)(b) amount. + household_benefits_individual_non_dep_deduction: 20.40 * 52 + +- name: A dependent young person in the family is banded on their own income, not the parent's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + total_income: 9_360 # 180 a week + weekly_hours: 16 + housing_benefit_individual_non_dep_deduction_eligible: true + young_person: + age: 19 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 18 + total_income: 5_200 # 100 a week + weekly_hours: 8 + housing_benefit_individual_non_dep_deduction_eligible: true + benunits: + family: + members: [parent, young_person] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + households: + household: + members: [parent, young_person] + output: + # reg 74(4) joins only a couple's income. The parent's 180 a week is + # under the 192 band (20.40); the young person, under 16 hours, pays 20.40 + # if not exempt (eligibility is an input here). + household_benefits_individual_non_dep_deduction: [1_060.80, 1_060.80] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml new file mode 100644 index 0000000000..e91df019b0 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -0,0 +1,167 @@ +# Counterexamples from the independent review of PolicyEngine/policyengine-uk#2007, +# end to end for Housing Benefit (HB Regs 2006 reg 74) and the England pensioner +# Council Tax Reduction scheme (SI 2012/2885 Sch 1 para 8), 2026-27 amounts. +# The claimant family is the rent-liable family with the household's oldest +# member. Family benefits are held at zero except where a case tests them. + +- name: A dependent 18-year-old's PIP does not exempt the claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + dependant: + age: 18 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + pip_dl_category: STANDARD + lodger_son: + age: 40 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant, dependant] + benunit_is_rent_liable: true + son_unit: + members: [lodger_son] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, dependant, lodger_son] + country: ENGLAND + local_authority: MAIDSTONE + output: + # reg 74(6) and para 8(6) protect the claimant or partner, not a young + # person in the family (reg 19; SSCBA s.142). The son's 576.92 a week is + # in the HB 485 band (119.85) and the CTR 485 band (13.30). + housing_benefit_non_dep_deductions_claimant_exempt: [false, false] + council_tax_reduction_household_has_non_dep_exemption: false + housing_benefit_non_dep_deductions: [6_232.20, 0] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: A 17-year-old claimant's PIP exempts their family + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 17 + current_education: NOT_IN_EDUCATION + pip_dl_category: STANDARD + sibling: + age: 16 + employment_income: 20_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + sibling_unit: + members: [sibling] + benunit_is_rent_liable: false + households: + household: + members: [claimant, sibling] + output: + # The benefit unit's head is the claimant whatever their age. + housing_benefit_non_dep_deductions_claimant_exempt: [true, false] + +- name: A parent non-dependant is banded on their own income, not their young person's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + parent: + age: 45 + employment_income: 9_360 # 180 a week + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + young_person: + age: 19 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 18 + employment_income: 5_200 # 100 a week + weekly_hours: 8 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + parent_unit: + members: [parent, young_person] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # reg 74(4) and para 8(4) join a couple's income only. The parent's 180 a + # week is under both lowest bands; the young person is a full-time student + # (reg 74(7)(c); para 8(7)(c)). + housing_benefit_non_dep_deduction_exempt: [false, false, true] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: A self-employed loss does not cancel wages for the Universal Credit exemption + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 24 + employment_income: 5_200 + self_employment_income: -5_200 + weekly_hours: 8 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 1_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 57(2) step 3: a self-employed loss is nil, so the wages + # are earned income and neither exemption applies. Under 16 hours: the + # lowest amounts. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml index 31dd42ed94..a8df69a38e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml @@ -780,7 +780,7 @@ input: people: claimant: - age: 70 + age: 90 son: age: 70 total_income: 9000 @@ -804,9 +804,47 @@ council_tax: 2_000 savings: 0 output: + # The claimant is strictly older, so the son is an eligible non-dependant + # and the zero comes from the SPC exemption (para 8(8)(a)), not from the + # son also counting as the applicant. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] council_tax_reduction_individual_non_dep_deduction: [0, 0.00] council_tax_reduction_non_dep_deductions: [0.00, 0] +- name: The same non-dependant without State Pension Credit pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 90 + son: + age: 70 + total_income: 9000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deductions: [270.40, 0] + - name: Universal Credit without earned income is exempt period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 66fd8dc9d4..4d8112ad06 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -16,14 +16,16 @@ 2. Bounds: an eligible non-dependant who is not exempt pays between the scale's lowest and highest weekly amounts. In English working-age households (local schemes) the national CTR deduction is 0. -3. A non-dependant not in remunerative work (under 16 hours) pays the lowest - amount, whatever their income. +3. A non-dependant not in remunerative work (under 16 hours or, for Housing + Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)) pays the lowest amount, + whatever their income. 4. If the claimant or partner is exempt, the claimant family's deductions are 0. 5. Aggregation: a family's deductions are the sum, over the other families in - the household, of their higher member's amount (both members' amounts for a - Welsh working-age applicant's non-dependant couple with a Universal Credit - award), recomputed here from the individual amounts. + the household, of their claimant and partner's higher amount (both amounts + for a Welsh working-age applicant's non-dependant couple with a Universal + Credit award) plus each other member's, recomputed here from the + individual amounts. 6. Monotonicity (metamorphic): raising any non-dependant's income never lowers an individual deduction or a family's deductions. 7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, @@ -36,7 +38,9 @@ a State Pension Credit recipient, or under 25 and on Income Support, income-based JSA or Universal Credit without earned income. CTR (any age): a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national - scheme, on Universal Credit without earned income. + scheme, on Universal Credit without earned income. Benefit receipt counts + only for the claimant or partner of the award; a self-employed loss counts + as nil earnings (UC reg 57(2)). """ from pathlib import Path @@ -89,6 +93,9 @@ "pension_credit", "universal_credit_pre_benefit_cap", ] +# Family benefits counted in gross income, held at zero so the oracle knows +# each family's gross income. +OTHER_FAMILY_BENEFITS = ["child_tax_credit", "working_tax_credit", "child_benefit"] SYSTEM = CountryTaxBenefitSystem() @@ -119,11 +126,30 @@ def households(draw): ), earnings=draw(st.one_of(st.just(0.0), st.floats(0, 60_000))), other_income=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + self_employment=draw(st.one_of(st.just(0.0), st.floats(-20_000, 20_000))), hours=draw(st.sampled_from([0.0, 10.0, 15.9, 16.0, 35.0, 45.0])), income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + approved_training=False, ) for _ in range(draw(st.integers(1, 2))) ] + # A dependent young person (SSCBA s.142) in the non-dependant family: in + # full-time education (a student) or on approved training (not a student, + # so a separate non-dependant with their own deduction). + if draw(st.booleans()): + training = draw(st.booleans()) + non_dependants.append( + dict( + age=draw(st.integers(16, 19)), + education="NOT_IN_EDUCATION" if training else "UPPER_SECONDARY", + earnings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + other_income=0.0, + self_employment=0.0, + hours=draw(st.sampled_from([0.0, 8.0, 16.0, 35.0])), + income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 10_000))), + approved_training=training, + ) + ) return dict( country=draw(st.sampled_from(sorted(COUNTRIES))), claimant_age=claimant_age, @@ -146,6 +172,9 @@ def build(population, year, raise_income=False): current_education="NOT_IN_EDUCATION", total_income=0.0, employment_income=0.0, + self_employment_income=0.0, + is_in_approved_training=False, + age_started_or_accepted_current_education_or_training=1_000, weekly_hours=0.0, is_blind=flag == "is_blind", **{ @@ -164,6 +193,10 @@ def build(population, year, raise_income=False): current_education=nd["education"], total_income=nd["earnings"] + other, employment_income=nd["earnings"], + self_employment_income=nd["self_employment"], + is_in_approved_training=nd["approved_training"], + # Young people started at 16; adults' start ages don't matter. + age_started_or_accepted_current_education_or_training=16, weekly_hours=nd["hours"], is_blind=False, **{ @@ -177,12 +210,13 @@ def build(population, year, raise_income=False): benunits[f"claimant_unit_{h}"] = dict( members=[claimant], benunit_is_rent_liable=True, - **{benefit: 0.0 for benefit in BENEFITS}, + **{benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS}, ) benunits[f"non_dependant_unit_{h}"] = dict( members=members, benunit_is_rent_liable=False, **household["benefits"], + **{benefit: 0.0 for benefit in OTHER_FAMILY_BENEFITS}, ) households_[f"household_{h}"] = dict( members=[claimant, *members], @@ -202,6 +236,8 @@ def calculate(sim, year): "council_tax_reduction_non_dep_deduction_exempt", "council_tax_reduction_individual_non_dep_deduction_eligible", "council_tax_reduction_individual_non_dep_deduction", + "is_benunit_head", + "is_child_or_qualifying_young_person_for_child_benefit", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -221,14 +257,18 @@ def calculate(sim, year): } -def aggregate(individual, persons, benunit_count, each_member): - """Recompute family deductions from individual amounts (invariant 5).""" - by_benunit = {} +def aggregate(individual, persons, couple, benunit_count, each_member): + """Recompute family deductions from individual amounts (invariant 5): the + claimant and partner's higher amount (both where each_member), plus each + other member's.""" + couple_amounts, others = {}, {} for i, person in enumerate(persons): - by_benunit.setdefault(person["benunit"], []).append(individual[i]) + target = couple_amounts if couple[i] else others + target.setdefault(person["benunit"], []).append(individual[i]) own = np.array( [ - (sum if each_member[b] else max)(by_benunit.get(b, [0.0])) + (sum if each_member[b] else max)(couple_amounts.get(b, [0.0])) + + sum(others.get(b, [])) for b in range(benunit_count) ] ) @@ -265,6 +305,8 @@ def aggregate(individual, persons, benunit_count, each_member): other_income=0.0, hours=35.0, income_rise=0.0, + self_employment=0.0, + approved_training=False, ), dict( age=28, @@ -273,6 +315,8 @@ def aggregate(individual, persons, benunit_count, each_member): other_income=0.0, hours=0.0, income_rise=5_000.0, + self_employment=0.0, + approved_training=False, ), ], ), @@ -290,6 +334,8 @@ def aggregate(individual, persons, benunit_count, each_member): other_income=0.0, hours=0.0, income_rise=0.0, + self_employment=0.0, + approved_training=False, ) ], ) @@ -308,6 +354,11 @@ def test_non_dependant_deduction_invariants(population, year): r["council_tax_reduction_household_has_pensioner"], [1 + len(h["non_dependants"]) for h in population], ) + # Claimant or partner of each person's own family: its head, or a member + # who is not an SSCBA s.142 child or qualifying young person. + couple = r["is_benunit_head"].astype(bool) | ~r[ + "is_child_or_qualifying_young_person_for_child_benefit" + ].astype(bool) for i, person in enumerate(persons): household = population[person["household"]] @@ -322,21 +373,23 @@ def test_non_dependant_deduction_invariants(population, year): if nd is None: continue # 9. The exemptions, read straight from the inputs. - benefits = household["benefits"] + # A family award belongs to its claimant and partner; a self-employed + # loss counts as nil earnings (UC reg 57(2)). + on = {b: bool(couple[i]) and household["benefits"][b] > 0 for b in BENEFITS} student = nd["education"] != "NOT_IN_EDUCATION" - uc_without_earnings = ( - benefits["universal_credit_pre_benefit_cap"] > 0 and nd["earnings"] <= 0 + # Inputs are stored as float32, so a tiny draw can underflow to nil. + earned = ( + max(0.0, float(np.float32(nd["earnings"]))) + + max(0.0, float(np.float32(nd["self_employment"]))) + > 0 ) + uc_without_earnings = on["universal_credit_pre_benefit_cap"] and not earned hb_exempt = ( student - or benefits["pension_credit"] > 0 + or on["pension_credit"] or ( nd["age"] < 25 - and ( - benefits["income_support"] > 0 - or benefits["jsa_income"] > 0 - or uc_without_earnings - ) + and (on["income_support"] or on["jsa_income"] or uc_without_earnings) ) ) assert bool(r["housing_benefit_non_dep_deduction_exempt"][i]) == hb_exempt @@ -344,7 +397,7 @@ def test_non_dependant_deduction_invariants(population, year): ctr_exempt = ( student or any( - benefits[b] > 0 + on[b] for b in [ "income_support", "jsa_income", @@ -363,7 +416,13 @@ def test_non_dependant_deduction_invariants(population, year): and not r["housing_benefit_non_dep_deduction_exempt"][i] ): assert hb_lowest - 0.01 <= hb_deduction <= hb_top + 0.01 - if nd["hours"] < hb.remunerative_work_hours: + # Reg 6(6): on IS, JSA(IB) or ESA(IR) is not remunerative work. + if ( + nd["hours"] < hb.remunerative_work_hours + or on["income_support"] + or on["jsa_income"] + or on["esa_income"] + ): assert hb_deduction == pytest.approx(hb_lowest, abs=0.01) # 2-3. Council Tax Reduction bounds and the not-in-work amount. country = household["country"] @@ -387,6 +446,7 @@ def test_non_dependant_deduction_invariants(population, year): expected_hb = aggregate( r["household_benefits_individual_non_dep_deduction"], persons, + couple, benunit_count, [False] * benunit_count, ) @@ -406,6 +466,7 @@ def test_non_dependant_deduction_invariants(population, year): expected_ctr = aggregate( r["council_tax_reduction_individual_non_dep_deduction"], persons, + couple, benunit_count, welsh_working_age, ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py new file mode 100644 index 0000000000..5a0aebbc14 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -0,0 +1,74 @@ +"""Shared definitions for Housing Benefit and Council Tax Reduction +non-dependant deductions.""" + +from policyengine_uk.model_api import * + +# Family benefits counted in a couple's gross income. Universal Credit is taken +# before the benefit cap: the capped award depends on Housing Benefit. +FAMILY_GROSS_INCOME_BENEFITS = [ + "universal_credit_pre_benefit_cap", + "child_tax_credit", + "working_tax_credit", + "child_benefit", +] + + +def is_claimant_or_partner(person, period): + """The claimant or partner of a benefit unit: its head, or any member who + is not a child or qualifying young person (SSCBA 1992 s.142), which is how + the Housing Benefit Regulations 2006 (regs 2(1) and 19) and the English and + Welsh Council Tax Reduction regulations (reg 2(1)) define the members of a + family other than the claimant and partner.""" + return person("is_benunit_head", period) | ~person( + "is_child_or_qualifying_young_person_for_child_benefit", period + ) + + +def non_dependant_weekly_gross_income(person, period): + """Normal weekly gross income for the deduction bands. A claimant or + partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 + para 8(4)): each member's taxable income plus the family's Universal + Credit, tax credits and child benefit. Anyone else in the benefit unit is a + separate non-dependant, banded on their own taxable income. Taxable income + excludes the disregarded disability benefits (HB reg 74(9)).""" + claimant_or_partner = is_claimant_or_partner(person, period) + own_income = max_(0, person("total_income", period)) + family_benefits = sum( + person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS + ) + couple_income = ( + person.benunit.sum(own_income * claimant_or_partner) + family_benefits + ) + annual = where(claimant_or_partner, couple_income, own_income) + return annual / WEEKS_IN_YEAR + + +def has_earned_income(person, period): + """Whether the person has earned income (UC Regs 2013 reg 52), with + self-employed losses taken as nil (reg 57(2), step 3).""" + return ( + max_(0, person("employment_income", period)) + + max_(0, person("self_employment_income", period)) + ) > 0 + + +def deduction_per_family(benunit, period, deductions, both_members_of_couple): + """One deduction for the claimant and partner, the higher of theirs (HB reg + 74(3); CTR Sch 1 para 8(3)), or both where both_members_of_couple; plus a + separate deduction for each other member.""" + claimant_or_partner = is_claimant_or_partner(benunit.members, period) + couple = where( + both_members_of_couple, + benunit.sum(deductions * claimant_or_partner), + benunit.max(deductions * claimant_or_partner), + ) + return couple + benunit.sum(deductions * ~claimant_or_partner) + + +def charged_to_other_families(benunit, period, deduction_for_benunit): + """Deductions for the non-dependant families in the household other than + this one: each family's amount counted once (via its head).""" + is_benunit_head = benunit.members("is_benunit_head", period) + counted = is_benunit_head * benunit.project(deduction_for_benunit) + in_household = benunit.max(benunit.members.household.sum(counted)) + return in_household - deduction_for_benunit 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 32dc5d9662..87c0c8d6c4 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 @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + is_claimant_or_partner, + non_dependant_weekly_gross_income, +) class household_benefits_individual_non_dep_deduction(Variable): @@ -7,10 +11,11 @@ class household_benefits_individual_non_dep_deduction(Variable): label = "Housing Benefit individual non-dependent deduction" documentation = ( "A non-dependant in remunerative work pays the deduction for the band " - "containing their normal gross weekly income, counting a couple's " - "joint income; bands include their lower edge. A non-dependant not in " - "remunerative work pays the lowest amount. Gross income is taxable " - "total income, so the disregarded disability benefits are excluded." + "containing their normal gross weekly income (a couple's joint income " + "for a claimant or partner of the non-dependant's family); bands " + "include their lower edge. A non-dependant not in remunerative work, " + "including one on Income Support, income-based JSA or income-related " + "ESA (reg 6(6)), pays the lowest amount." ) definition_period = YEAR unit = GBP @@ -18,16 +23,20 @@ class household_benefits_individual_non_dep_deduction(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/6", ) def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction - income = max_(0, person("total_income", period)) - counted = person("age", period) >= p.age_threshold - weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + weekly_income = non_dependant_weekly_gross_income(person, period) + on_income_related_benefit = is_claimant_or_partner(person, period) & ( + (person.benunit("income_support", period) > 0) + | (person.benunit("jsa_income", period) > 0) + | (person.benunit("esa_income", period) > 0) + ) in_remunerative_work = ( person("weekly_hours", period) >= p.remunerative_work_hours - ) + ) & ~on_income_related_benefit banded = p.amount.calc(weekly_income) not_in_remunerative_work = p.amount.calc(np.zeros_like(weekly_income)) weekly = where(in_remunerative_work, banded, not_in_remunerative_work) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 1c998c6694..1002656b47 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + has_earned_income, + is_claimant_or_partner, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, ) @@ -28,20 +32,22 @@ class housing_benefit_non_dep_deduction_exempt(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction full_time_student = is_full_time_student_non_dep(person, period) - on_pension_credit = person.benunit("pension_credit", period) > 0 - on_legacy_income_related_benefit = ( - person.benunit("income_support", period) > 0 - ) | (person.benunit("jsa_income", period) > 0) + # Receipt of a family award belongs to its claimant and partner. + claimant_or_partner = is_claimant_or_partner(person, period) + on_pension_credit = claimant_or_partner & ( + person.benunit("pension_credit", period) > 0 + ) + on_legacy_income_related_benefit = claimant_or_partner & ( + (person.benunit("income_support", period) > 0) + | (person.benunit("jsa_income", period) > 0) + ) # Entitlement before the benefit cap: universal_credit itself depends on # Housing Benefit through the cap. - entitled_to_universal_credit = ( + entitled_to_universal_credit = claimant_or_partner & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) - earned_income = add( - person, period, ["employment_income", "self_employment_income"] - ) - universal_credit_without_earned_income = entitled_to_universal_credit & ( - earned_income <= 0 + universal_credit_without_earned_income = ( + entitled_to_universal_credit & ~has_earned_income(person, period) ) under_age_limit = person("age", period) < p.income_related_benefit_age_limit return ( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 86eb32437a..30b29f0786 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + charged_to_other_families, + deduction_per_family, +) class housing_benefit_non_dep_deductions(Variable): @@ -7,8 +11,9 @@ class housing_benefit_non_dep_deductions(Variable): label = "non-dependent deductions" documentation = ( "Deductions for the non-dependants in other benefit units of the " - "household: one per couple, the higher of the two members' amounts, " - "and none if the claimant or partner is exempt." + "household: one for each couple, the higher of the two members' " + "amounts, and one for each other member; none if the claimant or " + "partner is exempt." ) definition_period = YEAR unit = GBP @@ -21,13 +26,12 @@ def formula(benunit, period, parameters): deductions = benunit.members( "household_benefits_individual_non_dep_deduction", period ) - deduction_for_benunit = benunit.max(deductions) - is_benunit_head = benunit.members("is_benunit_head", period) - counted = is_benunit_head * benunit.project(deduction_for_benunit) - deductions_in_household = benunit.max(benunit.members.household.sum(counted)) + deduction_for_benunit = deduction_per_family(benunit, period, deductions, False) claimant_exempt = benunit( "housing_benefit_non_dep_deductions_claimant_exempt", period ) return where( - claimant_exempt, 0, deductions_in_household - deduction_for_benunit + claimant_exempt, + 0, + charged_to_other_families(benunit, period, deduction_for_benunit), ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 4568e9f714..0ddaef9cc1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + is_claimant_or_partner, +) class housing_benefit_non_dep_deductions_claimant_exempt(Variable): @@ -21,8 +24,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): def formula(benunit, period, parameters): person = benunit.members - # Claimant or partner as in council_tax_reduction_household_has_non_dep_exemption. - claimant_or_partner = person("is_adult", period) + claimant_or_partner = is_claimant_or_partner(person, period) qualifying = ( person("is_blind", period) | (person("attendance_allowance", period) > 0) 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 fea66dd381..fb8f466080 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 @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + non_dependant_weekly_gross_income, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_england_pensioner_scheme, is_scotland_scheme, @@ -13,12 +16,12 @@ class council_tax_reduction_individual_non_dep_deduction(Variable): documentation = ( "The England pensioner, Scottish and Welsh schemes' own weekly scales. " "A non-dependant in remunerative work pays the deduction for the band " - "containing their normal gross weekly income, counting a couple's " - "joint income; bands include their lower edge. A non-dependant not in " + "containing their normal gross weekly income (a couple's joint income " + "for a claimant or partner of the non-dependant's family); bands " + "include their lower edge. A non-dependant not in " "remunerative work pays the lowest amount; an exempt non-dependant " - "pays nothing. Gross income is taxable total income, so the " - "disregarded disability benefits are excluded. English working-age " - "local schemes have their own variables." + "pays nothing. English working-age local schemes have their own " + "variables." ) definition_period = YEAR unit = GBP @@ -47,9 +50,7 @@ def formula(person, period, parameters): is_scotland_scheme(country), is_wales_scheme(country), ] - income = max_(0, person("total_income", period)) - counted = person("age", period) >= 18 - weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + weekly_income = non_dependant_weekly_gross_income(person, period) no_income = np.zeros_like(weekly_income) remunerative_work_hours = select( schemes, diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 004237bc6a..066c8c7d26 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + has_earned_income, + is_claimant_or_partner, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, ) @@ -49,7 +53,9 @@ def formula(person, period, parameters): is_wales_scheme(country), ] full_time_student = is_full_time_student_non_dep(person, period) - on_income_related_benefit = ( + # Receipt of a family award belongs to its claimant and partner. + claimant_or_partner = is_claimant_or_partner(person, period) + on_income_related_benefit = claimant_or_partner & ( (person.benunit("income_support", period) > 0) | (person.benunit("jsa_income", period) > 0) | (person.benunit("esa_income", period) > 0) @@ -64,14 +70,13 @@ def formula(person, period, parameters): ], default=False, ) - entitled_to_universal_credit = ( + entitled_to_universal_credit = claimant_or_partner & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) - earned_income = add( - person, period, ["employment_income", "self_employment_income"] - ) universal_credit_without_earned_income = ( - universal_credit_limb & entitled_to_universal_credit & (earned_income <= 0) + universal_credit_limb + & entitled_to_universal_credit + & ~has_earned_income(person, period) ) return ( full_time_student diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index 4384831a88..7d0c3f9484 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + charged_to_other_families, + deduction_per_family, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_wales_scheme, ) @@ -12,8 +16,8 @@ class council_tax_reduction_non_dep_deductions(Variable): "Deductions for the non-dependants in other benefit units of the " "household: one per couple, the higher of the two members' amounts " "(both, in the Welsh scheme for people who are not pensioners, when " - "the couple has a Universal Credit award), and none if the applicant " - "or partner is exempt." + "the couple has a Universal Credit award), one for each other member, " + "and none if the applicant or partner is exempt." ) definition_period = YEAR unit = GBP @@ -39,17 +43,14 @@ def formula(benunit, period, parameters): & ~has_pensioner & (benunit("universal_credit_pre_benefit_cap", period) > 0) ) - deduction_for_benunit = where( - each_member_deducted, - benunit.sum(deductions), - benunit.max(deductions), + deduction_for_benunit = deduction_per_family( + benunit, period, deductions, each_member_deducted ) - is_benunit_head = benunit.members("is_benunit_head", period) - counted = is_benunit_head * benunit.project(deduction_for_benunit) - deductions_in_household = benunit.max(benunit.members.household.sum(counted)) applicant_exempt = benunit.household( "council_tax_reduction_household_has_non_dep_exemption", period ) return where( - applicant_exempt, 0, deductions_in_household - deduction_for_benunit + applicant_exempt, + 0, + charged_to_other_families(benunit, period, deduction_for_benunit), ) From 12ac5f605d062053a95d7db8d0551c70f90d7704 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 05:36:02 -0400 Subject: [PATCH 03/31] Address review round 2: UC earned income, award payee, capped UC, LGFA disregard - "No earned income" follows UC Regs 2013 regs 52-62: employment income plus SSP and SMP less relievable pension contributions (reg 55(4)-(5)), self-employed earnings with losses nil and the minimum income floor where the UC model applies it (regs 57(2), 62), and other paid work. - IS, JSA(IB), ESA(IR) and SPC count only for the person they are payable to (HB Regs 2006 reg 2(3)-(3A)): the member reporting the award, or the head; used for the exemptions and HB reg 6(6). UC still counts for both joint claimants. - Gross income uses UC after the benefit cap (UC reg 81), applying the cap to the capped benefits other than HB, which a non-dependant family does not receive. - CTR exempts an adult for whom another person is entitled to child benefit (LGFA 1992 Sch 1 para 3, via para 8(8)(b) and equivalents). - Tests: the review's counterexamples, and property oracles for the payee and the child benefit disregard. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 260 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 29 +- .../non_dep_deduction/_non_dependants.py | 75 ++++- ...d_benefits_individual_non_dep_deduction.py | 10 +- ...ousing_benefit_non_dep_deduction_exempt.py | 15 +- ..._tax_reduction_non_dep_deduction_exempt.py | 36 ++- 6 files changed, 386 insertions(+), 39 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index e91df019b0..de9ebc33ad 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -165,3 +165,263 @@ council_tax_reduction_non_dep_deduction_exempt: [false, false] housing_benefit_non_dep_deductions: [1_060.80, 0] council_tax_reduction_non_dep_deductions: [270.40, 0] + +# Round 2 of the independent review. + +- name: Statutory maternity pay is earned income for the Universal Credit exemption + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 22 + statutory_maternity_pay: 8000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 2000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 55(4)(b): SMP is employed earnings, so the award is not + # calculated on no earned income. Not in remunerative work: the lowest amounts. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Wages fully paid into a pension leave no earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 24 + employment_income: 6240 + employee_pension_contributions: 6240 + weekly_hours: 8 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 55(5)(a): relievable pension contributions are deducted + # from employed earnings, leaving none (HB reg 74(8)(b); CTR para 8(8)(c)). + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: The minimum income floor counts as earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 24 + self_employment_income: 0 + uc_mif_applies: true + uc_minimum_income_floor: 20000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 62(2): a person to whom the floor applies is treated as + # having earned income equal to it. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Income-related ESA is payable to the recipient, not the working partner + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + recipient: + age: 45 + esa_income_reported: 2600 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + partner: + age: 40 + employment_income: 10920 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 2600 + pension_credit: 0 + would_claim_uc: false + households: + household: + members: [claimant, recipient, partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB Regs 2006 reg 2(3A): only the recipient is on ESA(IR), so only they are + # out of remunerative work (reg 6(6)) and exempt from CTR (para 8(8)(a)). The + # partner's 210 a week is in the HB 192 band (46.85) and under the CTR 279 band + # (5.20); one deduction for the couple, the higher. + housing_benefit_non_dep_deductions: [2_436.20, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Gross income counts Universal Credit after the benefit cap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 40 + employment_income: 52 # 1 a week, so not exempt as UC without earnings + total_income: 52 + weekly_hours: 35 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 30000 + benefit_cap: 20000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 81: the cap reduces the award to 20,000; with 1 a week of + # wages, 385.62 a week, + # in the HB 365 band (105.20) and the CTR 279 band (10.60). Before the cap it + # would be 576.92 a week. + housing_benefit_non_dep_deductions: [5_470.40, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A young person for whom child benefit is paid is disregarded for CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + parent: + age: 45 + employment_income: 10400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + young_person: + age: 19 + current_education: NOT_IN_EDUCATION + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 16 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [parent, young_person] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 1406.6 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + would_claim_uc: false + households: + household: + members: [claimant, parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # CTR para 8(8)(b) and LGFA 1992 Sch 1 para 3: the 19-year-old on approved + # training is disregarded, so only the parent is deducted: 227.05 a week with + # the child benefit, under 279 (5.20). HB has no such exemption: the parent + # (192 band, 46.85) plus the young person, not in remunerative work (20.40). + council_tax_reduction_non_dep_deduction_exempt: [false, false, true] + council_tax_reduction_non_dep_deductions: [270.40, 0] + housing_benefit_non_dep_deduction_exempt: [false, false, false] + housing_benefit_non_dep_deductions: [3_497.00, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 4d8112ad06..8ed4a8baa9 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -38,9 +38,11 @@ a State Pension Credit recipient, or under 25 and on Income Support, income-based JSA or Universal Credit without earned income. CTR (any age): a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national - scheme, on Universal Credit without earned income. Benefit receipt counts - only for the claimant or partner of the award; a self-employed loss counts - as nil earnings (UC reg 57(2)). + scheme, on Universal Credit without earned income, or an adult for whom + someone else is entitled to child benefit (LGFA 1992 Sch 1 para 3). IS, + JSA(IB), ESA(IR) and SPC count only for the person they are payable to; + Universal Credit for both joint claimants; a self-employed loss counts as + nil earnings (UC reg 57(2)). """ from pathlib import Path @@ -238,6 +240,7 @@ def calculate(sim, year): "council_tax_reduction_individual_non_dep_deduction", "is_benunit_head", "is_child_or_qualifying_young_person_for_child_benefit", + "is_qualifying_young_person_for_child_benefit", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -373,9 +376,16 @@ def test_non_dependant_deduction_invariants(population, year): if nd is None: continue # 9. The exemptions, read straight from the inputs. - # A family award belongs to its claimant and partner; a self-employed - # loss counts as nil earnings (UC reg 57(2)). - on = {b: bool(couple[i]) and household["benefits"][b] > 0 for b in BENEFITS} + # IS, JSA(IB), ESA(IR) and SPC count for their payee: here the benefit + # unit's head, as no member reports them (HB reg 2(3)-(3A)). Universal + # Credit counts for both joint claimants. A self-employed loss counts as + # nil earnings (UC reg 57(2)). + payee = bool(r["is_benunit_head"][i]) + on = { + b: (bool(couple[i]) if b == "universal_credit_pre_benefit_cap" else payee) + and household["benefits"][b] > 0 + for b in BENEFITS + } student = nd["education"] != "NOT_IN_EDUCATION" # Inputs are stored as float32, so a tiny draw can underflow to nil. earned = ( @@ -406,6 +416,13 @@ def test_non_dependant_deduction_invariants(population, year): ] ) or (national and uc_without_earnings) + # LGFA 1992 Sch 1 para 3: an adult qualifying young person in + # someone else's family. + or ( + nd["age"] >= 18 + and bool(r["is_qualifying_young_person_for_child_benefit"][i]) + and not couple[i] + ) ) assert ( bool(r["council_tax_reduction_non_dep_deduction_exempt"][i]) == ctr_exempt diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 5a0aebbc14..47d1b762b9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -3,15 +3,27 @@ from policyengine_uk.model_api import * -# Family benefits counted in a couple's gross income. Universal Credit is taken -# before the benefit cap: the capped award depends on Housing Benefit. +# Family benefits counted in a couple's gross income, besides Universal Credit. FAMILY_GROSS_INCOME_BENEFITS = [ - "universal_credit_pre_benefit_cap", "child_tax_credit", "working_tax_credit", "child_benefit", ] +# Benefits the benefit cap counts, other than Housing Benefit (benefit_cap_reduction). +CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ + "child_benefit", + "child_tax_credit", + "jsa_income", + "income_support", + "esa_income", + "universal_credit_pre_benefit_cap", + "jsa_contrib", + "incapacity_benefit", + "esa_contrib", + "sda", +] + def is_claimant_or_partner(person, period): """The claimant or partner of a benefit unit: its head, or any member who @@ -33,7 +45,7 @@ def non_dependant_weekly_gross_income(person, period): excludes the disregarded disability benefits (HB reg 74(9)).""" claimant_or_partner = is_claimant_or_partner(person, period) own_income = max_(0, person("total_income", period)) - family_benefits = sum( + family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS ) couple_income = ( @@ -43,13 +55,56 @@ def non_dependant_weekly_gross_income(person, period): return annual / WEEKS_IN_YEAR +def universal_credit_after_benefit_cap(benunit, period): + """A family's Universal Credit award after the benefit cap (UC Regs 2013 + reg 81), for a family that is not liable for rent. universal_credit itself + depends on Housing Benefit through the cap, so the cap is applied here to + the capped benefits other than Housing Benefit, which such a family does + not receive.""" + capped = sum( + benunit(benefit, period) for benefit in CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT + ) + reduction = max_(0, capped - benunit("benefit_cap", period)) + return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) + + def has_earned_income(person, period): - """Whether the person has earned income (UC Regs 2013 reg 52), with - self-employed losses taken as nil (reg 57(2), step 3).""" - return ( - max_(0, person("employment_income", period)) - + max_(0, person("self_employment_income", period)) - ) > 0 + """Whether the person has earned income (UC Regs 2013 reg 52): employed + earnings, including statutory sick and maternity pay and less relievable + pension contributions (reg 55(4)-(5)); self-employed earnings, a loss + counting as nil (reg 57(2)) and the minimum income floor applying where + the Universal Credit model applies it (reg 62); and other paid work.""" + employed = max_( + 0, + add( + person, + period, + ["employment_income", "statutory_sick_pay", "statutory_maternity_pay"], + ) + - person("pension_contributions", period), + ) + self_employed = max_(0, person("self_employment_income", period)) + self_employed = where( + person("uc_mif_applies", period), + max_(self_employed, person("uc_minimum_income_floor", period)), + self_employed, + ) + other = max_(0, person("miscellaneous_income", period)) + return (employed + self_employed + other) > 0 + + +def is_award_payee(person, period, award, reported): + """Whether a family award is payable to this person: Housing Benefit's + test for being "on" income-based JSA or income-related ESA (HB Regs 2006 + reg 2(3) and (3A)), applied also to Income Support and State Pension + Credit, which are paid to the claimant. The payee is the member who + reports the award, or the benefit unit's head where none does.""" + has_award = person.benunit(award, period) > 0 + reports = person(reported, period) > 0 + payee = where( + person.benunit.any(reports), reports, person("is_benunit_head", period) + ) + return has_award & payee def deduction_per_family(benunit, period, deductions, both_members_of_couple): 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 87c0c8d6c4..bd7bc84f3f 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 @@ -1,6 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - is_claimant_or_partner, + is_award_payee, non_dependant_weekly_gross_income, ) @@ -29,10 +29,10 @@ class household_benefits_individual_non_dep_deduction(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction weekly_income = non_dependant_weekly_gross_income(person, period) - on_income_related_benefit = is_claimant_or_partner(person, period) & ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) - | (person.benunit("esa_income", period) > 0) + on_income_related_benefit = ( + is_award_payee(person, period, "income_support", "income_support_reported") + | is_award_payee(person, period, "jsa_income", "jsa_income_reported") + | is_award_payee(person, period, "esa_income", "esa_income_reported") ) in_remunerative_work = ( person("weekly_hours", period) >= p.remunerative_work_hours diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 1002656b47..d26c291744 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, + is_award_payee, is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( @@ -32,15 +33,15 @@ class housing_benefit_non_dep_deduction_exempt(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction full_time_student = is_full_time_student_non_dep(person, period) - # Receipt of a family award belongs to its claimant and partner. + # IS, income-based JSA and SPC count for the person they are payable + # to; Universal Credit for both joint claimants. claimant_or_partner = is_claimant_or_partner(person, period) - on_pension_credit = claimant_or_partner & ( - person.benunit("pension_credit", period) > 0 - ) - on_legacy_income_related_benefit = claimant_or_partner & ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) + on_pension_credit = is_award_payee( + person, period, "pension_credit", "pension_credit_reported" ) + on_legacy_income_related_benefit = is_award_payee( + person, period, "income_support", "income_support_reported" + ) | is_award_payee(person, period, "jsa_income", "jsa_income_reported") # Entitlement before the benefit cap: universal_credit itself depends on # Housing Benefit through the cap. entitled_to_universal_credit = claimant_or_partner & ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 066c8c7d26..4bf4ceebce 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, + is_award_payee, is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( @@ -21,12 +22,13 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "Modelled exemptions in the England pensioner, Scottish and Welsh " "schemes: a full-time student; a non-dependant on Income Support, " "income-based JSA, income-related ESA or State Pension Credit (any " - "age); and, from the year each scheme added it, one entitled to " - "Universal Credit calculated on no earned income. Wales excludes " + "age); an adult for whom someone else is entitled to child benefit " + "(LGFA 1992 Sch 1 para 3); and, from the year each scheme added it, " + "one entitled to Universal Credit calculated on no earned income. Wales excludes " "members of the ESA work-related activity group; with no ESA group " - "input, income-related ESA is treated as exempt. Not modelled: persons " - "disregarded for council tax discounts (LGFA 1992 Sch 1) other than " - "students, youth training allowances, a normal home elsewhere, and " + "input, income-related ESA is treated as exempt. Not modelled: other " + "persons disregarded for council tax discounts (LGFA 1992 Sch 1), " + "youth training allowances, a normal home elsewhere, and " "absence in hospital or on armed forces operations." ) definition_period = YEAR @@ -53,13 +55,24 @@ def formula(person, period, parameters): is_wales_scheme(country), ] full_time_student = is_full_time_student_non_dep(person, period) - # Receipt of a family award belongs to its claimant and partner. + # IS, income-based JSA, income-related ESA and SPC count for the person + # they are payable to; Universal Credit for both joint claimants. claimant_or_partner = is_claimant_or_partner(person, period) - on_income_related_benefit = claimant_or_partner & ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) - | (person.benunit("esa_income", period) > 0) - | (person.benunit("pension_credit", period) > 0) + on_income_related_benefit = ( + is_award_payee(person, period, "income_support", "income_support_reported") + | is_award_payee(person, period, "jsa_income", "jsa_income_reported") + | is_award_payee(person, period, "esa_income", "esa_income_reported") + | is_award_payee( + person, period, "pension_credit", "pension_credit_reported" + ) + ) + # LGFA 1992 Sch 1 para 3 (via para 8(8)(b) and equivalents): an adult + # for whom another person is entitled to child benefit, that is a + # qualifying young person in someone else's family. + disregarded_for_child_benefit = ( + (person("age", period) >= 18) + & person("is_qualifying_young_person_for_child_benefit", period) + & ~claimant_or_partner ) universal_credit_limb = select( schemes, @@ -81,5 +94,6 @@ def formula(person, period, parameters): return ( full_time_student | on_income_related_benefit + | disregarded_for_child_benefit | universal_credit_without_earned_income ) From 9ebd8f691803f761f9159cb5a324edc65bd09c53 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 05:48:44 -0400 Subject: [PATCH 04/31] Use is_claimant_or_partner for the couple in non-dependant deductions Replaces the local claimant-or-partner helper (benefit unit head, or anyone not a child benefit child or qualifying young person) with the is_claimant_or_partner variable: the SSCBA s.137 couple, at most two people. A partner at school is now a partner, and a third adult in a benefit unit is deducted separately. Adds the round-2 review cases: a blind 16-year-old partner (Wales), a non-dependant couple with a partner at school, and a 17-year-old partner's PIP in Merton. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 143 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 9 +- .../non_dep_deduction/_non_dependants.py | 15 +- ...ousing_benefit_non_dep_deduction_exempt.py | 3 +- ...efit_non_dep_deductions_claimant_exempt.py | 5 +- ..._tax_reduction_non_dep_deduction_exempt.py | 3 +- 6 files changed, 151 insertions(+), 27 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index de9ebc33ad..0ee6a4fa4d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -425,3 +425,146 @@ council_tax_reduction_non_dep_deductions: [270.40, 0] housing_benefit_non_dep_deduction_exempt: [false, false, false] housing_benefit_non_dep_deductions: [3_497.00, 0] + +# Round 3: partners are the SSCBA s.137 couple (is_claimant_or_partner), not +# whoever is not a child benefit child or qualifying young person. + +- name: A blind 16-year-old partner exempts a Welsh 19-year-old applicant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 19 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 16 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + is_blind: true + is_claimant_or_partner: true + lodger: + age: 18 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant, partner] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, partner, lodger] + country: WALES + local_authority: CARDIFF + output: + # HB reg 74(6)(a) and WSI 2013/3029 Sch 6 para 5(6)(a): the claimant's + # partner is blind, so no deduction for the lodger (576.92 a week, which + # would otherwise be 119.85 HB and 16.80 CTR a week). + housing_benefit_non_dep_deductions_claimant_exempt: [true, false] + council_tax_reduction_household_has_non_dep_exemption: true + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: A non-dependant couple with a partner at school is banded on joint income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + older_partner: + age: 20 + employment_income: 9_360 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + younger_partner: + age: 18 + employment_income: 8_320 + weekly_hours: 10 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + couple_unit: + members: [older_partner, younger_partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, older_partner, younger_partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB reg 74(3)-(4) and CTR Sch 1 para 8(3)-(4): one deduction for the + # couple, banded on their joint 340 a week because the older partner is in + # remunerative work: 64.35 HB (269 band) and 10.60 CTR (279 band). + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A 17-year-old partner's PIP exempts a Merton applicant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 17 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + pip_dl_category: STANDARD + is_claimant_or_partner: true + lodger: + age: 40 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant, partner] + would_claim_uc: false + claims_all_entitled_benefits: true + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [applicant, partner, lodger] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Merton scheme para 30(6): no deduction where the applicant or partner + # gets the daily living component of PIP. + council_tax_reduction_household_has_non_dep_exemption: true + merton_council_tax_reduction_non_dep_deductions: [0, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 8ed4a8baa9..99af0cb70a 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -239,7 +239,7 @@ def calculate(sim, year): "council_tax_reduction_individual_non_dep_deduction_eligible", "council_tax_reduction_individual_non_dep_deduction", "is_benunit_head", - "is_child_or_qualifying_young_person_for_child_benefit", + "is_claimant_or_partner", "is_qualifying_young_person_for_child_benefit", ], benunit=[ @@ -357,11 +357,8 @@ def test_non_dependant_deduction_invariants(population, year): r["council_tax_reduction_household_has_pensioner"], [1 + len(h["non_dependants"]) for h in population], ) - # Claimant or partner of each person's own family: its head, or a member - # who is not an SSCBA s.142 child or qualifying young person. - couple = r["is_benunit_head"].astype(bool) | ~r[ - "is_child_or_qualifying_young_person_for_child_benefit" - ].astype(bool) + # Claimant or partner of each person's own family (SSCBA s.137 couple). + couple = r["is_claimant_or_partner"].astype(bool) for i, person in enumerate(persons): household = population[person["household"]] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 47d1b762b9..4167ce5d46 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -25,17 +25,6 @@ ] -def is_claimant_or_partner(person, period): - """The claimant or partner of a benefit unit: its head, or any member who - is not a child or qualifying young person (SSCBA 1992 s.142), which is how - the Housing Benefit Regulations 2006 (regs 2(1) and 19) and the English and - Welsh Council Tax Reduction regulations (reg 2(1)) define the members of a - family other than the claimant and partner.""" - return person("is_benunit_head", period) | ~person( - "is_child_or_qualifying_young_person_for_child_benefit", period - ) - - def non_dependant_weekly_gross_income(person, period): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 @@ -43,7 +32,7 @@ def non_dependant_weekly_gross_income(person, period): Credit, tax credits and child benefit. Anyone else in the benefit unit is a separate non-dependant, banded on their own taxable income. Taxable income excludes the disregarded disability benefits (HB reg 74(9)).""" - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) own_income = max_(0, person("total_income", period)) family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS @@ -111,7 +100,7 @@ def deduction_per_family(benunit, period, deductions, both_members_of_couple): """One deduction for the claimant and partner, the higher of theirs (HB reg 74(3); CTR Sch 1 para 8(3)), or both where both_members_of_couple; plus a separate deduction for each other member.""" - claimant_or_partner = is_claimant_or_partner(benunit.members, period) + claimant_or_partner = benunit.members("is_claimant_or_partner", period) couple = where( both_members_of_couple, benunit.sum(deductions * claimant_or_partner), diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index d26c291744..38215bd6c9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -2,7 +2,6 @@ from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, is_award_payee, - is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, @@ -35,7 +34,7 @@ def formula(person, period, parameters): full_time_student = is_full_time_student_non_dep(person, period) # IS, income-based JSA and SPC count for the person they are payable # to; Universal Credit for both joint claimants. - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) on_pension_credit = is_award_payee( person, period, "pension_credit", "pension_credit_reported" ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 0ddaef9cc1..53535fe798 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -1,7 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - is_claimant_or_partner, -) class housing_benefit_non_dep_deductions_claimant_exempt(Variable): @@ -24,7 +21,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): def formula(benunit, period, parameters): person = benunit.members - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) qualifying = ( person("is_blind", period) | (person("attendance_allowance", period) > 0) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 4bf4ceebce..0b1721e365 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -2,7 +2,6 @@ from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, is_award_payee, - is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, @@ -57,7 +56,7 @@ def formula(person, period, parameters): full_time_student = is_full_time_student_non_dep(person, period) # IS, income-based JSA, income-related ESA and SPC count for the person # they are payable to; Universal Credit for both joint claimants. - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) on_income_related_benefit = ( is_award_payee(person, period, "income_support", "income_support_reported") | is_award_payee(person, period, "jsa_income", "jsa_income_reported") From 75604f5f5555255186a522b9ddb52f615de83b09 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:37:40 -0400 Subject: [PATCH 05/31] Describe the claimant-or-partner source and the joint-claim JSA gap The changelog and the property generator's comment now name is_claimant_or_partner instead of the SSCBA s.142 definitions, and is_award_payee says that both members of a joint-claim jobseeker's allowance couple are on it (HB reg 2(3)(c)), which the model cannot identify. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- .../tests/test_non_dependant_deduction_properties.py | 7 ++++--- .../housing_benefit/non_dep_deduction/_non_dependants.py | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index dbfa0aa46e..24d66cd535 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner identified by the SSCBA s.142 child and young person definitions; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 99af0cb70a..8741b3e64c 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -135,9 +135,10 @@ def households(draw): ) for _ in range(draw(st.integers(1, 2))) ] - # A dependent young person (SSCBA s.142) in the non-dependant family: in - # full-time education (a student) or on approved training (not a student, - # so a separate non-dependant with their own deduction). + # A young person aged 16-19 in the non-dependant family: in full-time + # education (a student) or on approved training (not a student). Whether + # they are the family's claimant or partner is read from the model's + # is_claimant_or_partner. if draw(st.booleans()): training = draw(st.booleans()) non_dependants.append( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 4167ce5d46..58d69dd498 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -87,7 +87,9 @@ def is_award_payee(person, period, award, reported): test for being "on" income-based JSA or income-related ESA (HB Regs 2006 reg 2(3) and (3A)), applied also to Income Support and State Pension Credit, which are paid to the claimant. The payee is the member who - reports the award, or the benefit unit's head where none does.""" + reports the award, or the benefit unit's head where none does. Both + members of a joint-claim jobseeker's allowance couple are on it (reg + 2(3)(c)), but the model has no joint-claim input, so only the payee is.""" has_award = person.benunit(award, period) > 0 reports = person(reported, period) > 0 payee = where( From 084e1f9d68f1e3c8af904d7bb910db5a9cbff66e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 08:15:38 -0400 Subject: [PATCH 06/31] Address review round 3: gross and earned income, Scottish partners, cap childcare, child benefit claims - Gross income adds statutory sick, maternity and paternity pay (earnings, HB reg 35(1)(i)) and Maternity Allowance to each person's taxable income, and the family's IS, JSA(IB), ESA(IR) and Pension Credit to a couple's. - Earned income for the UC limb adds statutory paternity pay (UC reg 55(4)(c)), and relievable pension contributions come off self-employed earnings when there are no employed earnings (reg 57(2), step 4). - Scotland's working-age scheme exempts a non-dependant whose partner is on IS, JSA(IB) or ESA(IR) and who is not on Universal Credit, from April 2022 (SSI 2021/249 reg 90(8)(a), reg 4(1)). - The after-cap UC in gross income keeps the cap's childcare protection (UC reg 81(1)-(2)). - The LGFA 1992 Sch 1 para 3 disregard needs a child benefit claim. - The property oracle derives the claimant and applicant exemptions from the generated inputs, and the generator varies statutory pay, pension contributions, the start-up period, the award's reporter and child benefit claims. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- ...er_of_income_related_benefit_claimant.yaml | 17 + .../non_dependant_deduction_review_cases.yaml | 448 +++++++++++++++++- ...test_non_dependant_deduction_properties.py | 164 +++++-- .../non_dep_deduction/_non_dependants.py | 75 ++- ..._tax_reduction_non_dep_deduction_exempt.py | 55 ++- 6 files changed, 687 insertions(+), 74 deletions(-) create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 24d66cd535..3ad3195c5f 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap and its childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml new file mode 100644 index 0000000000..d9f934cc99 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml @@ -0,0 +1,17 @@ +description: Scotland's working-age Council Tax Reduction scheme makes no deduction for a non-dependant who is, or whose partner is, on Income Support, income-based JSA or income-related ESA and who is not on Universal Credit (a "qualifying income-related benefit claimant"), from the year this applies (SSI 2021/249 reg. 90(8)(a) with reg. 4(1)). Before April 2022 (SSI 2012/303 reg. 67(8)(a)), and in the pension-age scheme, only a non-dependant who is on one of these benefits is exempt. +values: + 2013-04-01: false + 2022-04-01: true +metadata: + label: Scotland working-age CTR exempts a non-dependant whose partner is on an income-related benefit + unit: bool + period: year + reference: + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 90(8)(a) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/90 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 4(1) ("qualifying income-related benefit claimant") + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/4 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 1 (in force 1 April 2022) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/1 + - title: Council Tax Reduction (Scotland) Regulations 2012, reg. 67(8)(a) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/67 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 0ee6a4fa4d..10f51ccfee 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -331,8 +331,9 @@ output: # HB Regs 2006 reg 2(3A): only the recipient is on ESA(IR), so only they are # out of remunerative work (reg 6(6)) and exempt from CTR (para 8(8)(a)). The - # partner's 210 a week is in the HB 192 band (46.85) and under the CTR 279 band - # (5.20); one deduction for the couple, the higher. + # partner is banded on the couple's 260 a week (210 wages plus 50 ESA), in + # the HB 192 band (46.85) and under the CTR 279 band (5.20); one deduction + # for the couple, the higher. housing_benefit_non_dep_deductions: [2_436.20, 0] council_tax_reduction_non_dep_deductions: [270.40, 0] @@ -374,7 +375,7 @@ # UC Regs 2013 reg 81: the cap reduces the award to 20,000; with 1 a week of # wages, 385.62 a week, # in the HB 365 band (105.20) and the CTR 279 band (10.60). Before the cap it - # would be 576.92 a week. + # would be 577.92 a week. housing_benefit_non_dep_deductions: [5_470.40, 0] council_tax_reduction_non_dep_deductions: [551.20, 0] @@ -523,7 +524,7 @@ output: # HB reg 74(3)-(4) and CTR Sch 1 para 8(3)-(4): one deduction for the # couple, banded on their joint 340 a week because the older partner is in - # remunerative work: 64.35 HB (269 band) and 10.60 CTR (279 band). + # remunerative work: 64.35 HB (279 band) and 10.60 CTR (279 band). housing_benefit_non_dep_deductions: [3_346.20, 0] council_tax_reduction_non_dep_deductions: [551.20, 0] @@ -568,3 +569,442 @@ # gets the daily living component of PIP. council_tax_reduction_household_has_non_dep_exemption: true merton_council_tax_reduction_non_dep_deductions: [0, 0] + +# Round 3 of the independent review: gross and earned income, the Scottish +# working-age scheme, the benefit cap's childcare protection, and child +# benefit entitlement. + +- name: Statutory maternity pay counts towards a non-dependant couple's gross income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + worker: + age: 30 + employment_income: 13_520 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 28 + statutory_maternity_pay: 1_560 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + couple_unit: + members: [worker, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, worker, partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB Regs 2006 reg 35(1)(i): statutory maternity pay is earnings. The + # couple's joint gross income is 260 wages plus 30 SMP, 290 a week (reg + # 74(4); CTR para 8(4)): the HB 279 band (64.35) and the CTR 279 band + # (10.60). Without the SMP, 260 would be a band lower in each. + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A couple's income-related ESA counts towards their joint gross income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + recipient: + age: 45 + esa_income_reported: 5_200 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 40 + employment_income: 9_360 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 5_200 + pension_credit: 0 + households: + household: + members: [claimant, recipient, partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # The partner works 16 hours and is banded on the couple's 180 wages plus + # 100 ESA(IR), 280 a week: the HB 279 band (64.35) and the CTR 279 band + # (10.60). The recipient is out of remunerative work (reg 6(6)) and exempt + # from CTR (para 8(8)(a)); one deduction for the couple, the higher. + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: Statutory paternity pay is earned income for the Universal Credit exemption + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + lodger: + age: 24 + statutory_paternity_pay: 300 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 2_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, lodger] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 55(4)(c): statutory paternity pay is employed earnings, + # so the award is not calculated on no earned income and neither + # exemption applies (HB reg 74(8)(b); CTR para 8(8)(c)). Out of + # remunerative work, the lodger pays the lowest amounts, 20.40 and 5.20. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Pension contributions are deducted from self-employed earnings in the start-up period + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + lodger: + age: 24 + self_employment_income: 6_000 + pension_contributions: 6_000 + uc_is_in_startup_period: true + weekly_hours: 10 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 2_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, lodger] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 57(2), step 4: with no employed earnings, the 6,000 of + # relievable pension contributions comes off the 6,000 profit, leaving nil. + # The minimum income floor does not apply in the start-up period (reg + # 62(5)), so the award is on no earned income: exempt under HB reg + # 74(8)(b) (aged under 25) and CTR para 8(8)(c). + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: Scotland's working-age scheme exempts the partner of an Income Support claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 5_000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2021/249 reg 90(8)(a) with reg 4(1): the partner "has a partner who + # is" on Income Support and is not on Universal Credit, so both are exempt + # from CTR. Housing Benefit has no such rule (reg 74(8)(a) is for the + # person on IS, and only under 25): both are out of remunerative work, and + # the couple pays one lowest amount, 20.40 a week. + council_tax_reduction_non_dep_deduction_exempt: [false, true, true] + council_tax_reduction_non_dep_deductions: [0, 0] + housing_benefit_non_dep_deductions: [1_060.80, 0] + +- name: Before April 2022 Scotland exempted only the Income Support claimant + period: 2021 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 5_000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2012/303 reg 67(8)(a): only a non-dependant "who is on income + # support" is exempt. The partner, out of remunerative work, pays the + # lowest 2021-22 amount, 4.30 a week. + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_non_dep_deductions: [223.60, 0] + +- name: Scotland's pension-age scheme exempts only the Income Support claimant + period: 2026 + input: + people: + applicant: + age: 80 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + income_support: 5_000 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2012/319 reg 48(8)(a): a non-dependant "who is on income support". + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + +- name: The benefit cap does not reduce Universal Credit when childcare costs exceed the excess + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + guardian: + age: 20 + employment_income: 9_360 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + older_child: + age: 4 + is_claimant_or_partner: false + younger_child: + age: 2 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + guardian_unit: + members: [guardian, older_child, younger_child] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 22_000 + uc_childcare_element: 12_000 + child_benefit: 2_500 + benefit_cap: 22_020 + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, guardian, older_child, younger_child] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 81(1)-(2): the excess over the cap is 2,480, less than + # the 12,000 childcare costs element, so the award is not reduced. Gross + # income is 180 wages plus 22,000 UC and 2,500 child benefit, 651.15 a + # week: the top bands, 131.45 HB and 15.95 CTR. + housing_benefit_non_dep_deductions: [6_835.40, 0] + council_tax_reduction_non_dep_deductions: [829.40, 0] + +- name: A young person no one claims child benefit for is not disregarded for CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + parent: + age: 45 + employment_income: 10_400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + young_person: + age: 19 + current_education: NOT_IN_EDUCATION + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 16 + weekly_hours: 0 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [parent, young_person] + benunit_is_rent_liable: false + would_claim_child_benefit: false + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + would_claim_uc: false + households: + household: + members: [claimant, parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # LGFA 1992 Sch 1 para 3 needs another person to be "entitled to child + # benefit" for the young person, and entitlement needs a claim (SSAA 1992 + # s.1(1)). With no claim, both are charged: the parent's 200 a week is + # under the CTR 279 band (5.20), and the young person, out of remunerative + # work, pays the lowest amount (5.20). + council_tax_reduction_non_dep_deduction_exempt: [false, false, false] + council_tax_reduction_non_dep_deductions: [540.80, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 8741b3e64c..64e47752ee 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -19,8 +19,9 @@ 3. A non-dependant not in remunerative work (under 16 hours or, for Housing Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)) pays the lowest amount, whatever their income. -4. If the claimant or partner is exempt, the claimant family's deductions - are 0. +4. If the claimant or partner is blind or receives a qualifying disability + benefit (read from the generated inputs, and checked against the model's + flags), the claimant family's deductions are 0. 5. Aggregation: a family's deductions are the sum, over the other families in the household, of their claimant and partner's higher amount (both amounts for a Welsh working-age applicant's non-dependant couple with a Universal @@ -39,10 +40,16 @@ income-based JSA or Universal Credit without earned income. CTR (any age): a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national scheme, on Universal Credit without earned income, or an adult for whom - someone else is entitled to child benefit (LGFA 1992 Sch 1 para 3). IS, - JSA(IB), ESA(IR) and SPC count only for the person they are payable to; - Universal Credit for both joint claimants; a self-employed loss counts as - nil earnings (UC reg 57(2)). + someone else is entitled to child benefit, which needs a claim (LGFA 1992 + Sch 1 para 3). In Scotland's working-age scheme from April 2022, also + anyone whose partner is on IS, JSA(IB) or ESA(IR) and who is not on + Universal Credit (SSI 2021/249 reg 90(8)(a), reg 4(1)). IS, JSA(IB), + ESA(IR) and SPC count only for the person they are payable to: the member + who reports them, or the benefit unit's head. Universal Credit counts for + both joint claimants. Earned income includes statutory sick, maternity and + paternity pay, less relievable pension contributions, which come off + self-employed earnings only when there are no employed earnings; a + self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). """ from pathlib import Path @@ -95,6 +102,17 @@ "pension_credit", "universal_credit_pre_benefit_cap", ] +REPORTED = { + "income_support": "income_support_reported", + "jsa_income": "jsa_income_reported", + "esa_income": "esa_income_reported", + "pension_credit": "pension_credit_reported", +} +STATUTORY_PAY = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", +] # Family benefits counted in gross income, held at zero so the oracle knows # each family's gross income. OTHER_FAMILY_BENEFITS = ["child_tax_credit", "working_tax_credit", "child_benefit"] @@ -129,6 +147,12 @@ def households(draw): earnings=draw(st.one_of(st.just(0.0), st.floats(0, 60_000))), other_income=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), self_employment=draw(st.one_of(st.just(0.0), st.floats(-20_000, 20_000))), + statutory_pay={ + name: draw(st.one_of(st.just(0.0), st.floats(0, 10_000))) + for name in STATUTORY_PAY + }, + pension_contributions=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + startup_period=draw(st.booleans()), hours=draw(st.sampled_from([0.0, 10.0, 15.9, 16.0, 35.0, 45.0])), income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), approved_training=False, @@ -148,6 +172,9 @@ def households(draw): earnings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), other_income=0.0, self_employment=0.0, + statutory_pay=dict.fromkeys(STATUTORY_PAY, 0.0), + pension_contributions=0.0, + startup_period=False, hours=draw(st.sampled_from([0.0, 8.0, 16.0, 35.0])), income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 10_000))), approved_training=training, @@ -161,6 +188,10 @@ def households(draw): benefit: draw(st.sampled_from([0.0, 0.0, 0.0, 1_000.0])) for benefit in BENEFITS }, + # Which adult non-dependant reports IS, JSA(IB), ESA(IR) and SPC, if + # anyone does. + reporter=draw(st.sampled_from([None, 0, 1])), + claims_child_benefit=draw(st.sampled_from([True, True, False])), non_dependants=non_dependants, ) @@ -191,12 +222,20 @@ def build(population, year, raise_income=False): for n, nd in enumerate(household["non_dependants"]): name = f"non_dependant_{h}_{n}" other = nd["other_income"] + (nd["income_rise"] if raise_income else 0) + reports = household.get("reporter") == n people[name] = dict( age=nd["age"], current_education=nd["education"], total_income=nd["earnings"] + other, employment_income=nd["earnings"], self_employment_income=nd["self_employment"], + **nd.get("statutory_pay", dict.fromkeys(STATUTORY_PAY, 0.0)), + pension_contributions=nd.get("pension_contributions", 0.0), + uc_is_in_startup_period=nd.get("startup_period", False), + **{ + reported: household["benefits"][benefit] if reports else 0.0 + for benefit, reported in REPORTED.items() + }, is_in_approved_training=nd["approved_training"], # Young people started at 16; adults' start ages don't matter. age_started_or_accepted_current_education_or_training=16, @@ -209,7 +248,9 @@ def build(population, year, raise_income=False): }, ) members.append(name) - persons.append(dict(household=h, benunit=2 * h + 1, non_dependant=nd)) + persons.append( + dict(household=h, benunit=2 * h + 1, non_dependant=nd, position=n) + ) benunits[f"claimant_unit_{h}"] = dict( members=[claimant], benunit_is_rent_liable=True, @@ -218,6 +259,7 @@ def build(population, year, raise_income=False): benunits[f"non_dependant_unit_{h}"] = dict( members=members, benunit_is_rent_liable=False, + would_claim_child_benefit=household.get("claims_child_benefit", True), **household["benefits"], **{benefit: 0.0 for benefit in OTHER_FAMILY_BENEFITS}, ) @@ -242,6 +284,8 @@ def calculate(sim, year): "is_benunit_head", "is_claimant_or_partner", "is_qualifying_young_person_for_child_benefit", + "uc_mif_applies", + "uc_minimum_income_floor", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -360,6 +404,24 @@ def test_non_dependant_deduction_invariants(population, year): ) # Claimant or partner of each person's own family (SSCBA s.137 couple). couple = r["is_claimant_or_partner"].astype(bool) + members_of = {} + for i, person in enumerate(persons): + members_of.setdefault(person["benunit"], []).append(i) + + def payee_of(i, benefit): + """The award's payee: the member who reports it, or the head.""" + person = persons[i] + household = population[person["household"]] + reporter = household.get("reporter") + reported = ( + person["non_dependant"] is not None + and reporter is not None + and reporter < len(household["non_dependants"]) + and household["benefits"][benefit] > 0 + ) + if reported: + return person["position"] == reporter + return bool(r["is_benunit_head"][i]) for i, person in enumerate(persons): household = population[person["household"]] @@ -374,23 +436,31 @@ def test_non_dependant_deduction_invariants(population, year): if nd is None: continue # 9. The exemptions, read straight from the inputs. - # IS, JSA(IB), ESA(IR) and SPC count for their payee: here the benefit - # unit's head, as no member reports them (HB reg 2(3)-(3A)). Universal - # Credit counts for both joint claimants. A self-employed loss counts as - # nil earnings (UC reg 57(2)). - payee = bool(r["is_benunit_head"][i]) - on = { - b: (bool(couple[i]) if b == "universal_credit_pre_benefit_cap" else payee) - and household["benefits"][b] > 0 - for b in BENEFITS - } + # IS, JSA(IB), ESA(IR) and SPC count for their payee: the member who + # reports them, or the benefit unit's head where none does (HB reg + # 2(3)-(3A)). Universal Credit counts for both joint claimants. + on = {b: payee_of(i, b) and household["benefits"][b] > 0 for b in REPORTED} + on["universal_credit_pre_benefit_cap"] = ( + bool(couple[i]) + and household["benefits"]["universal_credit_pre_benefit_cap"] > 0 + ) student = nd["education"] != "NOT_IN_EDUCATION" - # Inputs are stored as float32, so a tiny draw can underflow to nil. - earned = ( - max(0.0, float(np.float32(nd["earnings"]))) - + max(0.0, float(np.float32(nd["self_employment"]))) - > 0 + # Earned income (UC regs 55(4)-(5), 57(2), 62), in the inputs' float32. + f32 = np.float32 + statutory_pay = nd.get("statutory_pay", {}) + employed_gross = f32(nd["earnings"]) + sum( + (f32(statutory_pay.get(name, 0.0)) for name in STATUTORY_PAY), f32(0) ) + pension = f32(nd.get("pension_contributions", 0.0)) + employed = max(f32(0), employed_gross - pension) + self_employed = max( + f32(0), + max(f32(0), f32(nd["self_employment"])) + - (f32(0) if employed_gross > 0 else pension), + ) + if r["uc_mif_applies"][i]: + self_employed = max(self_employed, f32(r["uc_minimum_income_floor"][i])) + earned = employed + self_employed > 0 uc_without_earnings = on["universal_credit_pre_benefit_cap"] and not earned hb_exempt = ( student @@ -402,29 +472,40 @@ def test_non_dependant_deduction_invariants(population, year): ) assert bool(r["housing_benefit_non_dep_deduction_exempt"][i]) == hb_exempt national = household["country"] != "ENGLAND" or pension_age[i] + legacy = ["income_support", "jsa_income", "esa_income"] + on_legacy = any(on[b] for b in legacy) + # Scotland's working-age scheme (SSI 2021/249, from April 2022): a + # non-dependant who is, or whose partner is, on IS, JSA(IB) or ESA(IR) + # and who is not on Universal Credit. + if household["country"] == "SCOTLAND" and not pension_age[i]: + couple_on_legacy = bool(couple[i]) and any( + couple[j] + and any(payee_of(j, b) and household["benefits"][b] > 0 for b in legacy) + for j in members_of[person["benunit"]] + ) + qualifying = (on_legacy or couple_on_legacy) and not on[ + "universal_credit_pre_benefit_cap" + ] + else: + qualifying = on_legacy ctr_exempt = ( student - or any( - on[b] - for b in [ - "income_support", - "jsa_income", - "esa_income", - "pension_credit", - ] - ) + or qualifying + or on["pension_credit"] or (national and uc_without_earnings) - # LGFA 1992 Sch 1 para 3: an adult qualifying young person in - # someone else's family. + # LGFA 1992 Sch 1 para 3: an adult in someone else's family for + # whom child benefit is claimed. or ( nd["age"] >= 18 and bool(r["is_qualifying_young_person_for_child_benefit"][i]) and not couple[i] + and household.get("claims_child_benefit", True) ) ) assert ( bool(r["council_tax_reduction_non_dep_deduction_exempt"][i]) == ctr_exempt ) + # 2-3. Housing Benefit bounds and the not-in-work amount. if ( r["housing_benefit_individual_non_dep_deduction_eligible"][i] @@ -465,9 +546,15 @@ def test_non_dependant_deduction_invariants(population, year): benunit_count, [False] * benunit_count, ) - expected_hb = np.where( - r["housing_benefit_non_dep_deductions_claimant_exempt"], 0, expected_hb + # 4. The claimant exemption, from the generated disability inputs: only + # the claimant family's claimant can be blind or on a qualifying benefit. + disabled = np.array([h["claimant_disability"] is not None for h in population]) + claimant_exempt = np.repeat(disabled, 2) & (np.arange(benunit_count) % 2 == 0) + np.testing.assert_array_equal( + r["housing_benefit_non_dep_deductions_claimant_exempt"].astype(bool), + claimant_exempt, ) + expected_hb = np.where(claimant_exempt, 0, expected_hb) np.testing.assert_allclose( r["housing_benefit_non_dep_deductions"], expected_hb, atol=0.01 ) @@ -485,9 +572,12 @@ def test_non_dependant_deduction_invariants(population, year): benunit_count, welsh_working_age, ) - household_exempt = np.repeat( - r["council_tax_reduction_household_has_non_dep_exemption"], 2 + # The applicant is the claimant family (its member is the oldest). + np.testing.assert_array_equal( + r["council_tax_reduction_household_has_non_dep_exemption"].astype(bool), + disabled, ) + household_exempt = np.repeat(disabled, 2) expected_ctr = np.where(household_exempt, 0, expected_ctr) np.testing.assert_allclose( r["council_tax_reduction_non_dep_deductions"], expected_ctr, atol=0.01 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 58d69dd498..8d7eaf9d20 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -4,10 +4,35 @@ from policyengine_uk.model_api import * # Family benefits counted in a couple's gross income, besides Universal Credit. +# None is taxable, so total_income leaves them out. FAMILY_GROSS_INCOME_BENEFITS = [ "child_tax_credit", "working_tax_credit", "child_benefit", + "income_support", + "jsa_income", + "esa_income", + "pension_credit", +] + +# A person's gross income outside total_income: statutory payments, which are +# earnings (HB Regs 2006 reg 35(1)(i); UC Regs 2013 reg 55(4)) the model +# keeps apart from employment income, and Maternity Allowance, which is +# untaxed. +PERSONAL_GROSS_INCOME_OUTSIDE_TOTAL_INCOME = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", + "maternity_allowance", +] + +# Statutory payments treated as employed earnings (UC Regs 2013 reg 55(4)). +# Statutory adoption, shared parental, parental bereavement and neonatal care +# pay have no input. +STATUTORY_PAY_EARNINGS = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", ] # Benefits the benefit cap counts, other than Housing Benefit (benefit_cap_reduction). @@ -28,12 +53,17 @@ def non_dependant_weekly_gross_income(person, period): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 - para 8(4)): each member's taxable income plus the family's Universal - Credit, tax credits and child benefit. Anyone else in the benefit unit is a - separate non-dependant, banded on their own taxable income. Taxable income - excludes the disregarded disability benefits (HB reg 74(9)).""" + para 8(4)): each member's own income plus the family's Universal Credit, + tax credits, child benefit, Income Support, income-based JSA, + income-related ESA and Pension Credit. Anyone else in the benefit unit is a + separate non-dependant, banded on their own income. A person's own income + is their taxable income plus statutory sick, maternity and paternity pay + and Maternity Allowance. It leaves out the disability benefits HB reg + 74(9) disregards, which are untaxed.""" claimant_or_partner = person("is_claimant_or_partner", period) - own_income = max_(0, person("total_income", period)) + own_income = max_(0, person("total_income", period)) + add( + person, period, PERSONAL_GROSS_INCOME_OUTSIDE_TOTAL_INCOME + ) family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS ) @@ -49,30 +79,37 @@ def universal_credit_after_benefit_cap(benunit, period): reg 81), for a family that is not liable for rent. universal_credit itself depends on Housing Benefit through the cap, so the cap is applied here to the capped benefits other than Housing Benefit, which such a family does - not receive.""" + not receive. The award is reduced by the excess minus the childcare costs + element, and not at all where that element is greater than the excess + (reg 81(1)-(2)).""" capped = sum( benunit(benefit, period) for benefit in CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT ) - reduction = max_(0, capped - benunit("benefit_cap", period)) + excess = max_(0, capped - benunit("benefit_cap", period)) + reduction = max_(0, excess - benunit("uc_childcare_element", period)) return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) def has_earned_income(person, period): """Whether the person has earned income (UC Regs 2013 reg 52): employed - earnings, including statutory sick and maternity pay and less relievable - pension contributions (reg 55(4)-(5)); self-employed earnings, a loss - counting as nil (reg 57(2)) and the minimum income floor applying where - the Universal Credit model applies it (reg 62); and other paid work.""" - employed = max_( + earnings, including statutory sick, maternity and paternity pay and less + relievable pension contributions (reg 55(4)-(5)); self-employed earnings, + a loss counting as nil and less any relievable pension contributions not + already deducted from employed earnings (reg 57(2), steps 3-4), with the + minimum income floor applying where the Universal Credit model applies it + (reg 62); and other paid work.""" + pension_contributions = person("pension_contributions", period) + employed_gross = add(person, period, ["employment_income", *STATUTORY_PAY_EARNINGS]) + has_employed_earnings = employed_gross > 0 + employed = max_(0, employed_gross - pension_contributions) + # Contributions are deducted from employed earnings where there are any + # (reg 55(5)(a)), and otherwise from self-employed earnings (reg 57(2), + # step 4). + self_employed = max_( 0, - add( - person, - period, - ["employment_income", "statutory_sick_pay", "statutory_maternity_pay"], - ) - - person("pension_contributions", period), + max_(0, person("self_employment_income", period)) + - where(has_employed_earnings, 0, pension_contributions), ) - self_employed = max_(0, person("self_employment_income", period)) self_employed = where( person("uc_mif_applies", period), max_(self_employed, person("uc_minimum_income_floor", period)), diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 0b1721e365..433ef49c7d 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -21,8 +21,11 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "Modelled exemptions in the England pensioner, Scottish and Welsh " "schemes: a full-time student; a non-dependant on Income Support, " "income-based JSA, income-related ESA or State Pension Credit (any " - "age); an adult for whom someone else is entitled to child benefit " - "(LGFA 1992 Sch 1 para 3); and, from the year each scheme added it, " + "age), and in Scotland's working-age scheme from April 2022 one whose " + "partner is on Income Support, income-based JSA or income-related ESA " + "and who is not on Universal Credit; an adult for whom someone else is " + "entitled to child benefit (LGFA 1992 Sch 1 para 3); and, from the year " + "each scheme added it, " "one entitled to Universal Credit calculated on no earned income. Wales excludes " "members of the ESA work-related activity group; with no ESA group " "input, income-related ESA is treated as exempt. Not modelled: other " @@ -37,6 +40,8 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/4", + "https://www.legislation.gov.uk/ukpga/1992/14/schedule/1/paragraph/3", ) def formula(person, period, parameters): @@ -57,20 +62,46 @@ def formula(person, period, parameters): # IS, income-based JSA, income-related ESA and SPC count for the person # they are payable to; Universal Credit for both joint claimants. claimant_or_partner = person("is_claimant_or_partner", period) - on_income_related_benefit = ( + on_legacy_income_related_benefit = ( is_award_payee(person, period, "income_support", "income_support_reported") | is_award_payee(person, period, "jsa_income", "jsa_income_reported") | is_award_payee(person, period, "esa_income", "esa_income_reported") - | is_award_payee( - person, period, "pension_credit", "pension_credit_reported" - ) + ) + on_pension_credit = is_award_payee( + person, period, "pension_credit", "pension_credit_reported" + ) + entitled_to_universal_credit = claimant_or_partner & ( + person.benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + # SSI 2021/249 reg 90(8)(a): a "qualifying income-related benefit + # claimant", defined in reg 4(1) as one "who is, or who has a partner + # who is," on IS, income-based JSA or income-related ESA "and is not on + # universal credit". Read for the non-dependant, as the definition's + # "applicant" would leave the limb no application. + couple_on_legacy_income_related_benefit = claimant_or_partner & ( + person.benunit.any(on_legacy_income_related_benefit & claimant_or_partner) + ) + partner_rule = ( + is_scotland_scheme(country) + & ~has_pensioner + & scotland.non_dep_deduction.working_age_exempt_partner_of_income_related_benefit_claimant + ) + qualifying_income_related_benefit_claimant = where( + partner_rule, + (on_legacy_income_related_benefit | couple_on_legacy_income_related_benefit) + & ~entitled_to_universal_credit, + on_legacy_income_related_benefit, ) # LGFA 1992 Sch 1 para 3 (via para 8(8)(b) and equivalents): an adult - # for whom another person is entitled to child benefit, that is a - # qualifying young person in someone else's family. + # "in respect of whom another person is entitled to child benefit". + # Entitlement needs a claim (SSAA 1992 s.1(1)); an election not to be + # paid keeps it (s.13A), so the opt-out flag is not read. + entitled_to_child_benefit_for = ( + add(person, period, ["child_benefit_respective_amount"]) > 0 + ) & person.benunit("would_claim_child_benefit", period) disregarded_for_child_benefit = ( (person("age", period) >= 18) - & person("is_qualifying_young_person_for_child_benefit", period) + & entitled_to_child_benefit_for & ~claimant_or_partner ) universal_credit_limb = select( @@ -82,9 +113,6 @@ def formula(person, period, parameters): ], default=False, ) - entitled_to_universal_credit = claimant_or_partner & ( - person.benunit("universal_credit_pre_benefit_cap", period) > 0 - ) universal_credit_without_earned_income = ( universal_credit_limb & entitled_to_universal_credit @@ -92,7 +120,8 @@ def formula(person, period, parameters): ) return ( full_time_student - | on_income_related_benefit + | qualifying_income_related_benefit_claimant + | on_pension_credit | disregarded_for_child_benefit | universal_credit_without_earned_income ) From aa44a1a07d62a8904c001b2af7d45adbff6681a9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 12:10:06 -0400 Subject: [PATCH 07/31] Address review round 4: Maternity Allowance in the cap basket, losses in gross income - The after-cap UC used in a couple's gross income counts Maternity Allowance among the capped benefits (Welfare Reform Act 2012 s.96(10)(i)). - A self-employment or property loss no longer cancels other gross income: it is added back to taxable income (DWP HB guidance A5.551). - A YAML case for the Scottish partner rule's "not on Universal Credit" condition. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 165 ++++++++++++++++++ .../non_dep_deduction/_non_dependants.py | 22 ++- 2 files changed, 180 insertions(+), 7 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 10f51ccfee..5020b099f9 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1008,3 +1008,168 @@ # work, pays the lowest amount (5.20). council_tax_reduction_non_dep_deduction_exempt: [false, false, false] council_tax_reduction_non_dep_deductions: [540.80, 0] + +# Round 4 of the independent review. + +- name: Maternity Allowance counts towards the benefit cap on a non-dependant's Universal Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + worker: + age: 20 + employment_income: 9_200 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 38 + maternity_allowance: 6_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + child_1: + age: 16 + is_claimant_or_partner: false + child_2: + age: 15 + is_claimant_or_partner: false + child_3: + age: 13 + is_claimant_or_partner: false + child_4: + age: 11 + is_claimant_or_partner: false + child_5: + age: 0 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + family_unit: + members: [worker, partner, child_1, child_2, child_3, child_4, child_5] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 14_000 + uc_childcare_element: 0 + child_benefit: 5_129.80 + benefit_cap: 22_020 + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, worker, partner, child_1, child_2, child_3, child_4, child_5] + country: ENGLAND + local_authority: MAIDSTONE + output: + # Welfare Reform Act 2012 s.96(10)(i): Maternity Allowance is a capped + # benefit. Capped benefits are 25,129.80, 3,109.80 over the cap, so UC + # is 10,890.20 (UC reg 81). Joint gross income is 9,200 wages + 6,000 MA + # + 10,890.20 UC + 5,129.80 child benefit = 600.38 a week: the HB 485 + # band (119.85) and the CTR 485 band (13.30). + housing_benefit_non_dep_deductions: [6_232.20, 0] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: A self-employment loss does not cancel a non-dependant's gross wages + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + lodger: + age: 30 + employment_income: 15_600 + self_employment_income: -10_400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, lodger] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB reg 74(2) and SI 2006/214 reg 55(2): "normal gross weekly income". + # Taxable income nets the 10,400 loss against the 15,600 wages; gross + # income does not (DWP HB guidance A5.551), so the lodger is banded on + # 300 a week: the HB 279 band (64.35) and the CTR 279 band (10.60). + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: Scotland's partner rule does not cover a couple on Universal Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + employment_income: 5_200 + weekly_hours: 10 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 1_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 5_000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2021/249 reg 4(1): a qualifying income-related benefit claimant "is + # not on universal credit". Here the couple is, so the partner rule does + # not apply. The recipient is exempt as on UC without earned income (reg + # 90(8)(c)); the partner has wages, so is charged the lowest amount (5.55, + # under 16 hours), the couple's one deduction. + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_non_dep_deductions: [288.60, 0] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 8d7eaf9d20..2f81307bd1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -35,7 +35,8 @@ "statutory_paternity_pay", ] -# Benefits the benefit cap counts, other than Housing Benefit (benefit_cap_reduction). +# Benefits the benefit cap counts (Welfare Reform Act 2012 s.96(10)), other than +# Housing Benefit. Bereavement allowance and the widow's benefits have no input. CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ "child_benefit", "child_tax_credit", @@ -47,6 +48,7 @@ "incapacity_benefit", "esa_contrib", "sda", + "maternity_allowance", ] @@ -57,11 +59,19 @@ def non_dependant_weekly_gross_income(person, period): tax credits, child benefit, Income Support, income-based JSA, income-related ESA and Pension Credit. Anyone else in the benefit unit is a separate non-dependant, banded on their own income. A person's own income - is their taxable income plus statutory sick, maternity and paternity pay - and Maternity Allowance. It leaves out the disability benefits HB reg + is their taxable income, with any self-employment or property loss added + back, plus statutory sick, maternity and paternity pay and Maternity + Allowance. It leaves out the disability benefits HB reg 74(9) disregards, which are untaxed.""" claimant_or_partner = person("is_claimant_or_partner", period) - own_income = max_(0, person("total_income", period)) + add( + # Gross income: a self-employment or property loss does not reduce other + # income, as it does in taxable income. DWP HB guidance A5.551 takes a + # self-employed non-dependant's gross income as "their total income with no + # deductions for business expenses"; the model has profit, not turnover. + losses = max_(0, -person("self_employment_income", period)) + max_( + 0, -person("property_income", period) + ) + own_income = max_(0, person("total_income", period) + losses) + add( person, period, PERSONAL_GROSS_INCOME_OUTSIDE_TOTAL_INCOME ) family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( @@ -82,9 +92,7 @@ def universal_credit_after_benefit_cap(benunit, period): not receive. The award is reduced by the excess minus the childcare costs element, and not at all where that element is greater than the excess (reg 81(1)-(2)).""" - capped = sum( - benunit(benefit, period) for benefit in CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT - ) + capped = add(benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT) excess = max_(0, capped - benunit("benefit_cap", period)) reduction = max_(0, excess - benunit("uc_childcare_element", period)) return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) From 8e60349ff4ca763086814b7b3e783cb15f3354c8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 20:10:55 -0400 Subject: [PATCH 08/31] Apply reg 74(3) and 74(6) in the sharers property test Main's HB conservation check asserted that families' deductions sum to the household total of individual deductions. #2007 makes a non-dependant couple pay one deduction, the higher (HB Regs 2006 reg 74(3)), and charges nothing to a family whose claimant or partner is exempt (reg 74(6)). The check now expects the pool of per-family amounts times the share of the rent borne by families that are not exempt, and keeps an upper bound at the household total. Co-Authored-By: Claude Opus 5.5 --- ...f_sharers_boarders_and_lodgers_properties.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 0b68d8b3b1..2350446393 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -282,7 +282,22 @@ def test_non_dependants(population): ) hb_total = sim.map_result(hb_individual, "person", "household") assert hb_total.max() > 0 - assert np.allclose(per_household(sim, hb), hb_total, rtol=1e-5, atol=0.01) + # Each non-dependant family pays one deduction for its couple, the higher + # (HB Regs 2006 reg 74(3)), and one for each other member. The pool is + # apportioned over the families liable for the rent, and none falls on a + # family whose claimant or partner is exempt (reg 74(6)). + families = sim.populations["benunit"] + couple = calc(sim, "is_claimant_or_partner").astype(bool) + family_amount = families.max(hb_individual * couple) + families.sum( + hb_individual * ~couple + ) + pool = per_household(sim, family_amount) + exempt = calc(sim, "housing_benefit_non_dep_deductions_claimant_exempt").astype( + bool + ) + borne = per_household(sim, calc(sim, "share_of_household_rent") * ~exempt) + assert np.allclose(per_household(sim, hb), pool * borne, rtol=1e-5, atol=0.01) + assert np.all(per_household(sim, hb) <= hb_total + 0.01) assert np.all(hb[payer] == 0) assert np.all(uc[payer] == 0) # Council Tax Reduction: sharers, boarders and lodgers are not From 257447381a191f4c2741dfb53e179c1722277c26 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:11:18 -0400 Subject: [PATCH 09/31] Count only the couple's capped benefits; test mixed-scheme applicants Round 6 of the independent review: - The after-cap UC used in a non-dependant family's gross income counts a person's own capped benefits (contribution-based JSA and ESA, incapacity benefit, SDA, Maternity Allowance) only for the claimant or partner. UC Regs 2013 reg 79(1) caps the benefits "to which a single person or couple is entitled". A YAML case gives a 17-year-old in the family her own Maternity Allowance; it fails on the previous head (HB 5,470.40, CTR 551.20 instead of 6,232.20 and 691.60). - New council_tax_reduction_non_dep_deductions_mixed_schemes.yaml covers jointly liable applicants under different national schemes in England, Wales and Scotland, including the recompute path where the applicant's scheme differs from the household head's. - 2025-26 second- and middle-band HB cases (45.15 and 101.35 a week). - The sharers property test's docstring states invariant 4 as tested. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- ...benefits_individual_non_dep_deduction.yaml | 30 +++ .../non_dependant_deduction_review_cases.yaml | 54 ++++ ...tion_non_dep_deductions_mixed_schemes.yaml | 241 ++++++++++++++++++ ...sharers_boarders_and_lodgers_properties.py | 5 +- .../non_dep_deduction/_non_dependants.py | 18 +- 6 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 3ad3195c5f..40b5dd5254 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap and its childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap, counting only the couple's own capped benefits and keeping the childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts, with each jointly liable applicant's deduction taken under its own scheme. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 4684b12782..effa50cf33 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -347,3 +347,33 @@ child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 19.65 * 52 + +- name: 2025-26 second band in remunerative work + period: 2025 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 10_400 # 200 a week + weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + household_benefits_individual_non_dep_deduction: 45.15 * 52 + +- name: 2025-26 middle band in remunerative work + period: 2025 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 20_800 # 400 a week + weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + household_benefits_individual_non_dep_deduction: 101.35 * 52 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 3542d69496..84f05e8771 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1173,3 +1173,57 @@ # under 16 hours), the couple's one deduction. council_tax_reduction_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_non_dep_deductions: [288.60, 0] + +# Round 6 of the independent review. + +- name: A young person's own Maternity Allowance does not cap the family's Universal Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + is_household_head: true + worker: + age: 20 + employment_income: 7_000 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + maternity_allowance: 6_000 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + family_unit: + members: [worker, young_person] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 14_000 + uc_childcare_element: 0 + child_benefit: 5_129.80 + uc_benefit_cap: 22_020 + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, worker, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 79(1): the cap counts the welfare benefits "to which a + # single person or couple is entitled". The young person's own 6,000 is + # not the claimant's, so the capped total is 19,129.80, under the cap, and + # UC stays 14,000. The worker is banded on 7,000 + 14,000 + 5,129.80 = + # 502.50 a week: the HB 485 band (119.85) and the CTR 485 band (13.30). + housing_benefit_non_dep_deductions: [6_232.20, 0] + council_tax_reduction_non_dep_deductions: [691.60, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml new file mode 100644 index 0000000000..6cd1e3895c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml @@ -0,0 +1,241 @@ +# Jointly liable applicants under different national schemes. Each applicant's +# scheme follows its own family (SI 2012/2885 reg 3; council_tax_reduction_ +# pensioner), and a non-dependant of two jointly liable people is apportioned +# equally between them (Sch 1 para 8(5); WSI 2013/3029 Sch 1 para 3(5) and +# Sch 6 para 5(5); SSI 2021/249 reg 90(5)). 2026-27 scales: England +# pensioners 279 band 10.60; Wales 279 band 13.35; Scotland lowest 5.55, 286 +# band 10.95. Cases from the round-6 review of #2007. + +- name: England, working-age head and pension-age sharer, deduction from the sharer's pensioner claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_household_head: true + current_education: NOT_IN_EDUCATION + sharer: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 25 + employment_income: 15_600 # 300 a week + hours_worked: 1_040 # 20 a week + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + households: + home: + members: [head, sharer, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + # The pension-age sharer's claim bears half of 10.60 * 52 = 551.20. The + # working-age head is in Maidstone's local scheme, so no national + # deduction; the person-level variable reports the head's scheme. + council_tax_reduction_non_dep_deductions: [0, 275.60, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 0, 0] + +- name: Wales, pension-age head and working-age sharer with a non-dependant UC couple + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + current_education: NOT_IN_EDUCATION + first_partner: + age: 30 + employment_income: 10_400 + hours_worked: 832 # 16 a week + current_education: NOT_IN_EDUCATION + second_partner: + age: 30 + employment_income: 5_200 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + sharer: + age: 30 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + couple_family: + members: [first_partner, second_partner] + universal_credit_pre_benefit_cap: 3_000 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + households: + home: + members: [head, first_partner, second_partner, sharer] + country: WALES + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + # The couple's joint 357.69 a week is in the 279 band (13.35 * 52 = + # 694.20). The pension-age head's claim takes one deduction for the couple + # (Sch 1 para 3(3)); the working-age sharer's takes one for each member + # of a couple with a UC award (Sch 6 para 5(3)). Each bears half. + council_tax_reduction_non_dep_deductions: [347.10, 0, 694.20] + +- name: Wales, working-age head and pension-age sharer with a non-dependant UC couple + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_household_head: true + current_education: NOT_IN_EDUCATION + first_partner: + age: 30 + employment_income: 10_400 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + second_partner: + age: 30 + employment_income: 5_200 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + sharer: + age: 70 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + couple_family: + members: [first_partner, second_partner] + universal_credit_pre_benefit_cap: 3_000 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + households: + home: + members: [head, first_partner, second_partner, sharer] + country: WALES + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_non_dep_deductions: [694.20, 0, 347.10] + +- name: Scotland, working-age head and pension-age sharer, the partner rule only for the working-age claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + current_education: NOT_IN_EDUCATION + sharer: + age: 70 + current_education: NOT_IN_EDUCATION + recipient: + age: 35 + esa_income_reported: 5_200 + current_education: NOT_IN_EDUCATION + worker: + age: 35 + employment_income: 15_600 + hours_worked: 1_560 # 30 a week + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + couple_family: + members: [recipient, worker] + esa_income: 5_200 + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + households: + home: + members: [head, sharer, recipient, worker] + country: SCOTLAND + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + # Working-age head (SSI 2021/249 reg 90(8)(a), reg 4(1)): the worker is + # the partner of an income-related ESA claimant not on UC, so no + # deduction. Pension-age sharer (SSI 2012/319 reg 48): only the recipient + # is exempt; the worker is banded on the couple's 400 a week, 10.95 * 52 + # = 569.40, half each. The person-level flags report the head's scheme. + council_tax_reduction_non_dep_deductions: [0, 284.70, 0] + council_tax_reduction_non_dep_deduction_exempt: [false, false, true, true] + +- name: Scotland, pension-age head and working-age sharer, the partner rule only for the working-age claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + current_education: NOT_IN_EDUCATION + sharer: + age: 40 + current_education: NOT_IN_EDUCATION + recipient: + age: 35 + esa_income_reported: 5_200 + current_education: NOT_IN_EDUCATION + worker: + age: 35 + employment_income: 15_600 + hours_worked: 1_560 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + couple_family: + members: [recipient, worker] + esa_income: 5_200 + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + households: + home: + members: [head, sharer, recipient, worker] + country: SCOTLAND + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_non_dep_deductions: [284.70, 0, 0] + council_tax_reduction_non_dep_deduction_exempt: [false, false, true, false] diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 2350446393..c770dc202b 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -19,7 +19,10 @@ most one family per household counts the household's non-dependants for Universal Credit, it is liable for the household's rent, and only it has Universal Credit deductions; Housing Benefit deductions over the families - liable for the rent sum to the household's non-dependants' deductions; a + liable for the rent sum to the household's non-dependant deductions (one + per couple, the higher: HB Regs 2006 reg 74(3)) times the share of the + rent borne by families whose claimant or partner is not exempt (reg + 74(6)), and never exceed the sum of individual deductions; a boarder's or lodger's family has no Universal Credit or Housing Benefit deductions; no sharer, boarder or lodger is a Council Tax Reduction non-dependant. diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 99888289c2..9dfdcb7ddf 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -36,14 +36,20 @@ ] # Benefits the benefit cap counts (Welfare Reform Act 2012 s.96(10)), other than -# Housing Benefit. Bereavement allowance and the widow's benefits have no input. -CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ +# Housing Benefit, awarded to the family. Bereavement allowance and the +# widow's benefits have no input. +CAPPED_FAMILY_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ "child_benefit", "child_tax_credit", "jsa_income", "income_support", "esa_income", "universal_credit_pre_benefit_cap", +] + +# Capped benefits a person receives in their own right. Only the single person's +# or couple's count (UC Regs 2013 reg 79(1)). +CAPPED_PERSONAL_BENEFITS = [ "jsa_contrib", "incapacity_benefit", "esa_contrib", @@ -93,7 +99,13 @@ def universal_credit_after_benefit_cap(benunit, period): receive (UC reg 80(2A) counts it as nil in any case). The award is reduced by the excess minus the childcare costs element, and not at all where that element is greater than the excess (reg 81(1)-(2)).""" - capped = add(benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT) + person = benunit.members + capped = add(benunit, period, CAPPED_FAMILY_BENEFITS_EXCEPT_HOUSING_BENEFIT) + ( + benunit.sum( + add(person, period, CAPPED_PERSONAL_BENEFITS) + * person("is_claimant_or_partner", period) + ) + ) excess = max_(0, capped - benunit("uc_benefit_cap", period)) reduction = max_(0, excess - benunit("uc_childcare_element", period)) return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) From 9ae8f16db1a0316da6c5ca47b91a841d0c83d924 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:13:37 -0400 Subject: [PATCH 10/31] Test that a worker's HB band counts their investment income Main's case "Other investment income does not move a worker's band" pinned the stopgap that left investment income out of the band until the reg 74(2) remunerative-work test was modelled. With the test in place, a worker is banded on all gross income: 739.02 a week, the 2025-26 top band. Co-Authored-By: Claude Opus 5.5 --- ...benefits_individual_non_dep_deduction.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index effa50cf33..7120539a0b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -377,3 +377,22 @@ child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 101.35 * 52 + +- name: A worker's band counts their investment income + period: 2025 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + hours_worked: 1_950 # 37.5 hours a week + employment_income: 9_517 # just over 183 a week + other_investment_income: 28_912 # 556 a week + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + # In remunerative work, banded on all gross income (reg 74(2)), 739.02 a + # week: the band from £577, 126.65 * 52. Banding the earnings alone would + # give 45.15 * 52 = 2,347.80. + household_benefits_individual_non_dep_deduction: 126.65 * 52 From 89ba1a1276428dee825c3e412cdfe624413abc26 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:21:13 -0400 Subject: [PATCH 11/31] Correct the Scottish mixed-scheme cases' person-level flags The 70-year-old applicant in each Scottish case is on Pension Credit, so their own council_tax_reduction_non_dep_deduction_exempt is true. The flag matters only for a non-dependant, so the cases now also assert who is eligible as a non-dependant. The deductions are unchanged. Reported by the #2017 session. The non-dependant property test's docstring now states that invariant 5 multiplies by the rent share (reg 74(5)) or the joint-liability share (Sch 1 para 8(5)), as its oracle already does. Co-Authored-By: Claude Opus 5.5 --- ...reduction_non_dep_deductions_mixed_schemes.yaml | 14 +++++++++++--- .../test_non_dependant_deduction_properties.py | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml index 6cd1e3895c..171fac905c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml @@ -190,9 +190,13 @@ # the partner of an income-related ESA claimant not on UC, so no # deduction. Pension-age sharer (SSI 2012/319 reg 48): only the recipient # is exempt; the worker is banded on the couple's 400 a week, 10.95 * 52 - # = 569.40, half each. The person-level flags report the head's scheme. + # = 569.40, half each. The person-level flags report the head's scheme, + # so the worker is exempt there. The 70-year-old sharer is on Pension + # Credit, so their own flag is true too; it matters only for a + # non-dependant. council_tax_reduction_non_dep_deductions: [0, 284.70, 0] - council_tax_reduction_non_dep_deduction_exempt: [false, false, true, true] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true, true, true] - name: Scotland, pension-age head and working-age sharer, the partner rule only for the working-age claim period: 2026 @@ -237,5 +241,9 @@ council_tax: 2_000 savings: 0 output: + # Under the pension-age head's scheme, only the ESA recipient is exempt; + # the 70-year-old head's own flag is true because they are on Pension + # Credit. council_tax_reduction_non_dep_deductions: [284.70, 0, 0] - council_tax_reduction_non_dep_deduction_exempt: [false, false, true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true, true] + council_tax_reduction_non_dep_deduction_exempt: [true, false, true, false] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 86eee1b8ec..132592d1de 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -26,7 +26,8 @@ the household, of their claimant and partner's higher amount (both amounts for a Welsh working-age applicant's non-dependant couple with a Universal Credit award) plus each other member's, recomputed here from the - individual amounts. + individual amounts, times the family's share of the rent (Housing Benefit, + reg 74(5)) or of the council tax liability (CTR, Sch 1 para 8(5)). 6. Monotonicity (metamorphic): raising any non-dependant's income never lowers an individual deduction or a family's deductions. 7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, From 4a0935b737cd71231e39340689b2148125f585c9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 00:23:37 -0400 Subject: [PATCH 12/31] Never charge a family that bears a share of the rent as a non-dependant A family liable for a share of the rent is a joint occupier, not a non-dependant (HB Regs 2006 reg 3(2)(d)). is_non_dependant_of_household_head tests the family's own rent, which is nil whenever the household's is, so a sharer of a nil rent was both a non-dependant and a bearer of a share. Main's aggregation charged that family part of its own deduction. This PR's never charges a family for its own members, so the household's total fell short of the pool times the shares borne. The CI example (Hypothesis, derandomized) is a council tenancy at nil rent with a sharer: HB deductions 510.90 against a pool of 1,021.80 borne in full. The sharer is now left out of the pool, as in the HB aggregation itself. HB is nil at nil rent, so no award changes. A YAML case pins it, and main's sharers property test treats a sharer as never a non-dependant for the HB pool. Both the replayed CI example and the YAML case fail on the previous head and pass here. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 31 +++++++++++++++++++ ...sharers_boarders_and_lodgers_properties.py | 11 +++++-- .../housing_benefit_non_dep_deductions.py | 14 +++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index c6adec9f83..845e7e1777 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -98,6 +98,37 @@ # Only person 3 is a non-dependant of the head's family: 3. housing_benefit_non_dep_deductions: [3, 0, 0] +- name: A sharer of a nil rent is not a non-dependant + period: 2025 + absolute_error_margin: 0 + input: + people: + person_1: + age: 40 + is_household_head: true + household_benefits_individual_non_dep_deduction: 0 + person_2: + age: 40 + household_benefits_individual_non_dep_deduction: 2 + benunits: + benunit_1: + members: [person_1] + housing_benefit_non_dep_deductions_claimant_exempt: false + benunit_2: + members: [person_2] + liable_for_share_of_household_rent: true + housing_benefit_non_dep_deductions_claimant_exempt: false + households: + household: + members: [person_1, person_2] + rent: 0 + output: + # Both families are liable for the rent, so each bears half. With a nil + # rent neither has any, but the sharer is still a joint occupier, not a + # non-dependant (HB Regs 2006 reg 3(2)(d)): no deduction for either. + share_of_household_rent: [0.5, 0.5] + housing_benefit_non_dep_deductions: [0, 0] + # The review case on #2184: an existing claimant in council housing with an # adult child whose only income is £28,912 of other investment income and who # does no paid work. The bands apply only in remunerative work (HB Regs 2006 diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index c770dc202b..0162003f6b 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -22,7 +22,8 @@ liable for the rent sum to the household's non-dependant deductions (one per couple, the higher: HB Regs 2006 reg 74(3)) times the share of the rent borne by families whose claimant or partner is not exempt (reg - 74(6)), and never exceed the sum of individual deductions; a + 74(6)), and never exceed the sum of individual deductions, with a sharer + never a non-dependant, even where the rent is nil (reg 3(2)(d)); a boarder's or lodger's family has no Universal Credit or Housing Benefit deductions; no sharer, boarder or lodger is a Council Tax Reduction non-dependant. @@ -280,8 +281,12 @@ def test_non_dependants(population): assert np.all(per_household(sim, uc) <= total + 0.01) # Housing Benefit: apportioned over the families liable for the rent. hb = calc(sim, "housing_benefit_non_dep_deductions") - hb_individual = ( - calc(sim, "household_benefits_individual_non_dep_deduction") * non_dependant + # A sharer's family is a joint occupier, never a non-dependant (HB Regs + # 2006 reg 3(2)(d)), even where the rent is nil and so the family is not + # benunit_is_rent_liable. + sharer = sim.map_result((roles == "sharer").astype(float), "benunit", "person") > 0 + hb_individual = calc(sim, "household_benefits_individual_non_dep_deduction") * ( + non_dependant & ~sharer ) hb_total = sim.map_result(hb_individual, "person", "household") assert hb_total.max() > 0 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index d4c3b27483..40710ed628 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -33,10 +33,18 @@ def formula(benunit, period, parameters): # occupier is apportioned between them by their shares of the # payments (reg 74(5); SPC reg 55(5)), so each family liable for the # household's rent bears its share; a boarder or lodger bears none. + # A family bearing a share is a joint occupier, never a + # non-dependant, even where the rent is nil and so the family is not + # benunit_is_rent_liable. person = benunit.members - deductions = person( - "household_benefits_individual_non_dep_deduction", period - ) * person("is_non_dependant_of_household_head", period) + joint_occupier = person.benunit("share_of_household_rent", period) > 0 + non_dependant = ( + person("is_non_dependant_of_household_head", period) & ~joint_occupier + ) + deductions = ( + person("household_benefits_individual_non_dep_deduction", period) + * non_dependant + ) deduction_for_benunit = deduction_per_family(benunit, period, deductions, False) share = benunit("share_of_household_rent", period) claimant_exempt = benunit( From 4515cc8fd642d0ee02969abcde3f9f6f07fb2dad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 21:35:15 -0400 Subject: [PATCH 13/31] Bring main's non-dependant tests to each scheme's own law #2620's gate cases, gross-income cases and the national CTR maintenance test now expect each CTR scheme's own scale, the flat (1)(b) amount for non-workers and the exemptions; the PR's own cases band the programme gross-income variables instead of total_income, which main now defines as the Income Tax Step 1 total. The property tests read #2027's is_on_* status, check banding against gross income built from the inputs, and check the after-cap Universal Credit against universal_credit. Co-Authored-By: Claude Opus 5.5 --- ...benefits_individual_non_dep_deduction.yaml | 31 +- .../housing_benefit_non_dep_gross_income.yaml | 3 +- ...ing_benefit_non_dep_remunerative_work.yaml | 20 +- ...sion_age_severe_disability_conditions.yaml | 16 +- .../claimant_household_head.yaml | 5 +- ...il_tax_reduction_non_dep_gross_income.yaml | 96 ++-- ...x_reduction_non_dep_remunerative_work.yaml | 419 +++++++++++++++--- .../test_housing_benefit_non_dep_history.py | 2 +- ...test_non_dependant_deduction_properties.py | 197 +++++++- .../test_non_dependant_spousal_maintenance.py | 56 ++- .../non_dep_deduction/_non_dependants.py | 11 +- 11 files changed, 683 insertions(+), 173 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 7feffae878..6ecfaa2b49 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -328,9 +328,18 @@ council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week employment_income: 5_200 # £100 a week, in both programmes' gross income + age: 30 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + country: WALES output: household_benefits_individual_non_dep_deduction: 793 - council_tax_reduction_individual_non_dep_deduction: 793 + # Council Tax Reduction has its own scale: Wales's lowest amount, £4.35 a + # week from 1 April 2018 (WSI 2018/14), £4.20 before it. + council_tax_reduction_individual_non_dep_deduction: 4.35 * 52 - name: Reduced rent without LHA period: 2025 @@ -338,6 +347,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 5_200 # 100 weekly + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 19.65 * 52 @@ -347,6 +360,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 30_056 # 578 weekly + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 126.65 * 52 @@ -356,6 +373,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 18_148 # 349 weekly + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 101.35 * 52 @@ -365,6 +386,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 9_517 # just over 183 weekly, the lower edge of the second band + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 45.15 * 52 @@ -373,6 +398,10 @@ input: housing_benefit_individual_non_dep_deduction_eligible: true other_investment_income: 28_912 # 556 weekly, no hours + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # Not in remunerative work, so reg 74(1)(b)'s amount whatever the income: # 19.65 * 52 = 1,021.80. Banding the £556 would give 115.45 * 52 = diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index a890f3a21e..e186f9be3a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -4,8 +4,7 @@ # Sch 1 para 3 and Sch 6 para 5. Gross income includes statutory pay and # maintenance; the listed disability payments are disregarded. # Hand arithmetic: wages 9,000 + statutory pay 520 = 9,520, just above -# 183/week. The existing national consumers use the HB scale (45.15/week -# at this income in 2025); correcting CTR rates is separate (#2007). +# 183/week, in the HB band from £183 (45.15/week in 2025). # Annual wages and statutory pay can relate to different parts of the year; # leave days themselves are not remunerative work (HB reg 6(7)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml index bb515785bf..c5d3406773 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml @@ -1,9 +1,8 @@ # HB Regs 2006 reg 74(1)(b), (2) and reg 6(1), (6); pension-age # HB Regs 2006 reg 55(1)(b), (2) and reg 6(1), (6). -# A non-worker or 15-hour worker with banding income pays 20.40 * 52 = -# 1,060.80 in 2026-27. One with no banding income keeps the scale's zero: -# the gate only lowers a deduction until #2007 models the exemptions in -# reg 74(6), (7), (8) and (10) and reg 55(6)-(9) with the flat charge. +# A non-worker or 15-hour worker pays 20.40 * 52 = 1,060.80 in 2026-27, +# whatever their income. The exemptions in reg 74(7), (8) and (10) and reg +# 55(7)-(9) act through eligibility, which most cases here take as an input. # A 16-hour worker's wages of 9,000 alone are below 192/week; adding # IIDB or child maintenance of 12,000 gives 403.85/week, so 105.20 * 52. @@ -175,7 +174,7 @@ output: household_benefits_individual_non_dep_deduction: 1060.8 -- name: housing_benefit keeps a non-worker with no banding income at zero (nil income) +- name: housing_benefit charges the flat amount to a non-worker with nil income period: 2026 absolute_error_margin: 0.01 input: @@ -188,24 +187,25 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - # Reg 74(1)(b) gives a non-exempt non-worker 1,060.80, but the zero is - # kept until #2007 models the exemptions alongside the flat charge. - household_benefits_individual_non_dep_deduction: 0 + # Reg 74(1)(b) gives a non-worker the flat amount whatever their income: + # 20.40 * 52. + household_benefits_individual_non_dep_deduction: 1060.8 - name: housing_benefit charges no new deduction to a non-dependant under 25 on Income Support # Reg 74(8)(a): no deduction for a non-dependant under 25 on Income Support. - # Income Support is not banding income, so the gate leaves the zero. + # The exemption makes them ineligible for a deduction. period: 2026 absolute_error_margin: 0.01 input: age: 22 - housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 0 social_security_income: 0 is_on_income_support: true is_on_income_based_jsa: false is_on_income_related_esa: false output: + housing_benefit_non_dep_deduction_exempt: true + housing_benefit_individual_non_dep_deduction_eligible: false household_benefits_individual_non_dep_deduction: 0 - name: housing_benefit excludes each income-related benefit recipient from remunerative work (Income Support) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml index a6a1b2bde1..88cc95616c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml @@ -1,8 +1,8 @@ # HB(SPC) Regs 2006 Sch 3 para 6, with the non-dependant definition in # reg 3: these cases assert final awards without a Guarantee Credit passport. -# The final HB and CTR awards include the model's current income rules, -# including a known defect outside this file: no non-dependant deduction -# exemption for a claimant on Attendance Allowance (#2007). The premium and +# The final HB and CTR awards include the model's current income rules. +# A claimant on Attendance Allowance has no non-dependant deductions (HB +# (SPC) Regs 2006 reg 55(6); SI 2012/2885 Sch 1 para 8(6)); the premium and # applicable-amount assertions do not depend on it. Each HB award is the # rent less 65% of income after tax, plus the savings credit where the award # is savings credit only (HB (SPC) Regs 2006 reg 27(4)(a)), above the @@ -129,9 +129,8 @@ guarantee_credit: [0, 0] has_non_dependant_for_severe_disability_premium: [true, false] pension_age_severe_disability_premium: [0, 0] - # The resident has no banding income, so the work gate leaves the - # scale's zero; reg 55(6) and Sch 1 para 8(6) exempt this claimant's - # non-dependants in any case (the AA exemption remains for #2007). + # Reg 55(6) and Sch 1 para 8(6) exempt this claimant's non-dependants: + # the claimant receives Attendance Allowance. housing_benefit: [3_898.70, 0] council_tax_benefit: [1_279.60, 0] @@ -175,9 +174,8 @@ # Savings credit only (908.46): HB income is Pension Credit income plus # the savings credit (HB (SPC) Regs 2006 reg 27(4)(a)), 0.65 x 908.46 = # 590.50 below the figure without it. - # Neither non-working resident has banding income, so the work gate - # leaves the scale's zero for both; the AA exemption (reg 55(6), Sch 1 - # para 8(6)) remains for #2007. + # The AA exemption (reg 55(6), Sch 1 para 8(6)) leaves no deduction for + # either resident. housing_benefit: [3_308.20, 0] council_tax_benefit: [1_097.91, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml index 720ebe1016..dd3a9e2b63 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml @@ -718,9 +718,8 @@ # student shares liability with a non-student, the non-student is solely # liable (LGFA 1992 s.75(4)), so the co-tenant claims on the whole bill # and the head is the co-tenant's non-dependant. No deduction is made for - # a full-time student non-dependant (reg 90(7)(c)). That exemption - # remains for #2007; the model's national deduction is nil here because - # the head has no banding income, which the work gate leaves at zero. + # a full-time student non-dependant (reg 90(7)(c)), so the co-tenant's + # reduction covers the whole bill. council_tax_reduction_liable_person: [true, true] council_tax_reduction_claimant_benunit: [false, true] council_tax_reduction_joint_liability_share: [0.5, 1] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index ea70a23fdd..3e4f20ac4d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -4,8 +4,10 @@ # Sch 1 para 3 and Sch 6 para 5. Gross income includes statutory pay and # maintenance; the listed disability payments are disregarded. # Hand arithmetic: wages 9,000 + statutory pay 520 = 9,520, just above -# 183/week. The existing national consumers use the HB scale (45.15/week -# at this income in 2025); correcting CTR rates is separate (#2007). +# 183/week. Each national scheme bands it on its own 2025-26 scale, below +# the first band's lower edge, so the non-dependant pays the scheme's lowest +# amount: England 5.00 (SI 2025/39), Scotland 5.35 (SSI 2025/24) and Wales +# 6.25 (WSI 2025/59) a week. # Annual wages and statutory pay can relate to different parts of the year; # leave days themselves are not remunerative work (HB reg 6(7)). @@ -50,9 +52,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 260] + council_tax_reduction_non_dep_deductions: [260, 0] + simulated_council_tax_reduction_benunit: [3_740, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with ENGLAND claimant aged 70 period: 2025 @@ -95,9 +97,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 260] + council_tax_reduction_non_dep_deductions: [260, 0] + simulated_council_tax_reduction_benunit: [3_740, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with ENGLAND claimant aged 70 period: 2025 @@ -140,9 +142,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 260] + council_tax_reduction_non_dep_deductions: [260, 0] + simulated_council_tax_reduction_benunit: [3_740, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with SCOTLAND claimant aged 40 period: 2025 @@ -185,9 +187,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with SCOTLAND claimant aged 40 period: 2025 @@ -230,9 +232,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with SCOTLAND claimant aged 40 period: 2025 @@ -275,9 +277,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with SCOTLAND claimant aged 70 period: 2025 @@ -320,9 +322,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with SCOTLAND claimant aged 70 period: 2025 @@ -365,9 +367,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with SCOTLAND claimant aged 70 period: 2025 @@ -410,9 +412,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with WALES claimant aged 40 period: 2025 @@ -455,9 +457,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with WALES claimant aged 40 period: 2025 @@ -500,9 +502,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with WALES claimant aged 40 period: 2025 @@ -545,9 +547,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with WALES claimant aged 70 period: 2025 @@ -590,9 +592,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with WALES claimant aged 70 period: 2025 @@ -635,9 +637,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with WALES claimant aged 70 period: 2025 @@ -680,9 +682,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income counts tax-exempt maintenance and periodic benefits period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index 4c9caa26cc..2cd2277ab9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -1,19 +1,28 @@ # England SI 2012/2885 Sch 1 para 8 and reg 10; Scotland SSI 2021/249 # reg 90 and reg 10, SSI 2012/319 reg 48 and reg 6; Wales WSI 2013/3029 # Sch 1 para 3, Sch 6 para 5 and reg 10: income banding requires remunerative -# work. This scoped fix retains the consumer's inherited HB scale: the -# 2026-27 flat amount is 20.40/week and 403.85/week of income gives 105.20. -# Actual national CTR flat amounts are England 5.20, Scotland 5.55, Wales -# 6.70; their scales remain for #2007. These are work-gate regressions. A -# non-worker with no banding income keeps the scale's zero: the gate only -# lowers a deduction until #2007 models the exemptions in paragraphs (6) to -# (8) with the flat charge. +# work. Each scheme's 2026-27 scale: a non-dependant outside remunerative work +# pays the lowest amount (England 5.20, SI 2026/27; Scotland 5.55, SSI +# 2026/56; Wales 6.70, WSI 2026/13), and 403.85/week of income in work falls +# in the band from £279 in England (10.60) and Wales (13.35) and from £286 in +# Scotland (10.95). Family awards, which a single non-dependant's gross income +# would count, are held at zero; English cases take the pension-age scheme, +# the only national one in England. - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -27,12 +36,21 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -46,12 +64,21 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -65,12 +92,21 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -84,12 +120,21 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -103,12 +148,21 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -122,12 +176,21 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -141,12 +204,21 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -160,12 +232,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -179,12 +259,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -198,12 +286,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -217,12 +313,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -236,12 +340,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -255,12 +367,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -274,12 +394,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -293,12 +421,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -312,12 +448,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -331,12 +475,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -350,12 +502,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -369,12 +529,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -388,12 +556,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -407,12 +583,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -426,12 +610,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -445,12 +637,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -464,12 +664,21 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: council_tax_reduction charges the flat amount on a non-worker's pension or nil income (pension income) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 70 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -479,12 +688,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 -- name: council_tax_reduction keeps a non-worker with no banding income at zero (nil income) +- name: council_tax_reduction charges the flat amount to a non-worker with nil income period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 70 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -494,14 +712,15 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - # Paragraph (1)(b) gives a non-exempt non-worker the flat amount, but the - # zero is kept until #2007 models the exemptions alongside the flat charge. - council_tax_reduction_individual_non_dep_deduction: 0 + # Paragraph (1)(b) gives a non-worker the flat amount whatever their + # income, here 5.20 * 52; Pension Credit, which would exempt them, is held + # at zero. + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: council_tax_reduction charges no new deduction to a non-dependant on Universal Credit without earnings # SI 2012/2885 Sch 1 para 8(8)(c): no deduction for a non-dependant - # entitled to Universal Credit with no earned income. Universal Credit is - # not banding income, so the gate leaves the zero. + # entitled to Universal Credit with no earned income. The family is + # pension-age for CTR, so England's national scheme applies. period: 2026 absolute_error_margin: 0.01 input: @@ -517,7 +736,8 @@ benunits: non_dependant_unit: members: [non_dependant] - universal_credit: 6_000 + universal_credit_pre_benefit_cap: 6_000 + council_tax_reduction_pensioner: true households: household: members: [non_dependant] @@ -526,12 +746,20 @@ council_tax_reduction_individual_non_dep_deduction: [0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (Income Support) - # The recipient is not in remunerative work, so the flat amount applies - # rather than a band. SI 2012/2885 Sch 1 para 8(8)(a) exempts this - # non-dependant altogether; that exemption remains for #2007. + # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and + # Sch 1 para 8(8)(a) exempts them altogether: no deduction. period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -542,15 +770,23 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 0 - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-based JSA) - # The recipient is not in remunerative work, so the flat amount applies - # rather than a band. SI 2012/2885 Sch 1 para 8(8)(a) exempts this - # non-dependant altogether; that exemption remains for #2007. + # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and + # Sch 1 para 8(8)(a) exempts them altogether: no deduction. period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -561,15 +797,23 @@ is_on_income_based_jsa: true is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 0 - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-related ESA) - # The recipient is not in remunerative work, so the flat amount applies - # rather than a band. SI 2012/2885 Sch 1 para 8(8)(a) exempts this - # non-dependant altogether; that exemption remains for #2007. + # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and + # Sch 1 para 8(8)(a) exempts them altogether: no deduction. period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -580,12 +824,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: true output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 0 - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (no excluded benefit) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -596,12 +849,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND @@ -614,12 +876,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND @@ -632,12 +903,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: SCOTLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.scotland.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: SCOTLAND @@ -650,12 +929,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.scotland.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: SCOTLAND @@ -668,12 +955,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: WALES council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.wales.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: WALES @@ -686,12 +981,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.wales.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: WALES @@ -704,4 +1007,4 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 diff --git a/policyengine_uk/tests/test_housing_benefit_non_dep_history.py b/policyengine_uk/tests/test_housing_benefit_non_dep_history.py index 47d4e369b0..0c45be6875 100644 --- a/policyengine_uk/tests/test_housing_benefit_non_dep_history.py +++ b/policyengine_uk/tests/test_housing_benefit_non_dep_history.py @@ -42,7 +42,7 @@ def test_all_historical_scale_cells(year, thresholds, amounts): # SI 2015/457 art 18(3); SI 2016/242 reg 5; SI 2017/260 art 22(4); # SI 2018/281 arts 1(2)(h), 23(3) and 24(3). - # Test the parameter itself, not the separate #2007 eligibility/banding fix. + # Test the parameter itself, not eligibility or banding. scale = load_parameter_file(str(PATH), "hb_non_dep_scale")(f"{year}-04-30") assert scale.thresholds == thresholds assert scale.amounts == amounts diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 132592d1de..f59085ce4d 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -45,12 +45,28 @@ Sch 1 para 3). In Scotland's working-age scheme from April 2022, also anyone whose partner is on IS, JSA(IB) or ESA(IR) and who is not on Universal Credit (SSI 2021/249 reg 90(8)(a), reg 4(1)). IS, JSA(IB), - ESA(IR) and SPC count only for the person they are payable to: the member - who reports them, or the benefit unit's head. Universal Credit counts for - both joint claimants. Earned income includes statutory sick, maternity and - paternity pay, less relievable pension contributions, which come off - self-employed earnings only when there are no employed earnings; a - self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). + ESA(IR) and SPC count only for the person they are payable to, read from + the model's is_on_* status (HB Regs 2006 reg 2(1), (3), (3A)); where the + claimant or partner reports the award, or no one does, that is the + reporter or the benefit unit's head, which the test checks. Universal + Credit counts for both joint claimants. Earned income includes statutory + sick, maternity and paternity pay, less relievable pension contributions, + which come off self-employed earnings only when there are no employed + earnings; a self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). +10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) +11. Banding: a non-dependant in remunerative work who is neither exempt nor + on IS, JSA(IB) or ESA(IR) pays the amount of the band whose lower edge is + the highest not above their normal gross weekly income, computed here from + the generated inputs: earnings, statutory sick, maternity and paternity + pay, self-employed profit with a loss as nil, and other investment income; + for the claimant or partner of the non-dependant's family, the couple's + sum plus the family's IS, JSA(IB), ESA(IR), Pension Credit and Universal + Credit. Incomes within a penny a week of an edge are skipped, since the + inputs are stored as float32. +12. Differential: the after-cap Universal Credit used in a non-dependant + family's gross income equals the model's universal_credit for a family + with no Housing Benefit, no Maternity Allowance and no deductions from + its award. """ from pathlib import Path @@ -109,6 +125,12 @@ "esa_income": "esa_income_reported", "pension_credit": "pension_credit_reported", } +IS_ON = { + "income_support": "is_on_income_support", + "jsa_income": "is_on_income_based_jsa", + "esa_income": "is_on_income_related_esa", + "pension_credit": "is_on_pension_credit", +} STATUTORY_PAY = [ "statutory_sick_pay", "statutory_maternity_pay", @@ -205,7 +227,6 @@ def build(population, year, raise_income=False): people[claimant] = dict( age=household["claimant_age"], current_education="NOT_IN_EDUCATION", - total_income=0.0, employment_income=0.0, self_employment_income=0.0, is_in_approved_training=False, @@ -227,8 +248,8 @@ def build(population, year, raise_income=False): people[name] = dict( age=nd["age"], current_education=nd["education"], - total_income=nd["earnings"] + other, employment_income=nd["earnings"], + other_investment_income=other, self_employment_income=nd["self_employment"], **nd.get("statutory_pay", dict.fromkeys(STATUTORY_PAY, 0.0)), pension_contributions=nd.get("pension_contributions", 0.0), @@ -237,6 +258,9 @@ def build(population, year, raise_income=False): reported: household["benefits"][benefit] if reports else 0.0 for benefit, reported in REPORTED.items() }, + # Held at zero so the oracle knows each person's gross income. + state_pension=0.0, + carers_allowance=0.0, is_in_approved_training=nd["approved_training"], # Young people started at 16; adults' start ages don't matter. age_started_or_accepted_current_education_or_training=16, @@ -287,6 +311,10 @@ def calculate(sim, year): "is_qualifying_young_person_for_child_benefit", "uc_mif_applies", "uc_minimum_income_floor", + "is_on_income_support", + "is_on_income_based_jsa", + "is_on_income_related_esa", + "is_on_pension_credit", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -427,6 +455,32 @@ def payee_of(i, benefit): return person["position"] == reporter return bool(r["is_benunit_head"][i]) + f32 = np.float32 + + def own_gross(j): + """A person's own gross income from the generated inputs (invariant + 11): a self-employment loss counts as nil.""" + nd_j = persons[j]["non_dependant"] + if nd_j is None: + return 0.0 + return float( + f32(nd_j["earnings"]) + + sum( + f32(nd_j.get("statutory_pay", {}).get(name, 0.0)) + for name in STATUTORY_PAY + ) + + max(f32(0), f32(nd_j["self_employment"])) + + f32(nd_j["other_income"]) + ) + + def band_amount(scale, weekly): + """The amount of the band whose lower edge is the highest not above + weekly income, or None within a penny of an edge.""" + if any(abs(weekly - t) < 0.01 for t in scale.thresholds[1:]): + return None + index = max(k for k, t in enumerate(scale.thresholds) if t <= weekly) + return scale.amounts[index] * WEEKS + for i, person in enumerate(persons): household = population[person["household"]] hb_deduction = r["household_benefits_individual_non_dep_deduction"][i] @@ -440,17 +494,30 @@ def payee_of(i, benefit): if nd is None: continue # 9. The exemptions, read straight from the inputs. - # IS, JSA(IB), ESA(IR) and SPC count for their payee: the member who - # reports them, or the benefit unit's head where none does (HB reg - # 2(3)-(3A)). Universal Credit counts for both joint claimants. - on = {b: payee_of(i, b) and household["benefits"][b] > 0 for b in REPORTED} + # IS, JSA(IB), ESA(IR) and SPC count for their payee (HB reg 2(1), + # (3)-(3A)): the model's is_on_* status. Where the reporter, if any, is + # the claimant or partner, that is the reporter, or the benefit unit's + # head where none reports. Universal Credit counts for both joint + # claimants. + on = {b: bool(r[IS_ON[b]][i]) for b in REPORTED} + reporter = household.get("reporter") + reporter_index = ( + None + if reporter is None or reporter >= len(household["non_dependants"]) + else members_of[person["benunit"]][reporter] + ) + if reporter_index is None or couple[reporter_index]: + for b in REPORTED: + assert on[b] == (payee_of(i, b) and household["benefits"][b] > 0), ( + b, + i, + ) on["universal_credit_pre_benefit_cap"] = ( bool(couple[i]) and household["benefits"]["universal_credit_pre_benefit_cap"] > 0 ) student = nd["education"] != "NOT_IN_EDUCATION" # Earned income (UC regs 55(4)-(5), 57(2), 62), in the inputs' float32. - f32 = np.float32 statutory_pay = nd.get("statutory_pay", {}) employed_gross = f32(nd["earnings"]) + sum( (f32(statutory_pay.get(name, 0.0)) for name in STATUTORY_PAY), f32(0) @@ -483,8 +550,7 @@ def payee_of(i, benefit): # and who is not on Universal Credit. if household["country"] == "SCOTLAND" and not pension_age[i]: couple_on_legacy = bool(couple[i]) and any( - couple[j] - and any(payee_of(j, b) and household["benefits"][b] > 0 for b in legacy) + couple[j] and any(r[IS_ON[b]][j] for b in legacy) for j in members_of[person["benunit"]] ) qualifying = (on_legacy or couple_on_legacy) and not on[ @@ -541,6 +607,33 @@ def payee_of(i, benefit): if nd["hours"] < scale.remunerative_work_hours: assert ctr_deduction == pytest.approx(lowest, abs=0.01) + # 11. Banding on gross income computed from the inputs. + if couple[i]: + weekly = ( + sum(own_gross(j) for j in members_of[person["benunit"]] if couple[j]) + + sum(household["benefits"].values()) + ) / WEEKS + else: + weekly = own_gross(i) / WEEKS + if ( + r["housing_benefit_individual_non_dep_deduction_eligible"][i] + and nd["hours"] >= hb.remunerative_work_hours + and not (on["income_support"] or on["jsa_income"] or on["esa_income"]) + ): + expected = band_amount(hb.amount, weekly) + if expected is not None: + assert hb_deduction == pytest.approx(expected, abs=0.01), (i, weekly) + if ( + national + and r["council_tax_reduction_individual_non_dep_deduction_eligible"][i] + and not r["council_tax_reduction_non_dep_deduction_exempt"][i] + and nd["hours"] >= scale.remunerative_work_hours + and not qualifying + ): + expected = band_amount(scale.amount, weekly) + if expected is not None: + assert ctr_deduction == pytest.approx(expected, abs=0.01), (i, weekly) + benunit_count = 2 * len(population) # 4-5. Housing Benefit aggregation and the claimant exemption. expected_hb = aggregate( @@ -661,8 +754,8 @@ def test_band_edges_are_inclusive(scale_name, years): age=40, current_education="NOT_IN_EDUCATION", weekly_hours={str(y): 35.0 for y in years}, - total_income=incomes, - employment_income={str(y): 0.0 for y in years}, + housing_benefit_non_dep_gross_income=incomes, + council_tax_reduction_non_dep_gross_income=incomes, ) benunits[f"claimant_unit_{k}"] = dict( members=[claimant], @@ -718,3 +811,73 @@ def test_ctr_scales_match_enacted_sums(): year, ) assert list(node.amounts) == pytest.approx(row["amounts"]), (scheme, year) + + +@PROPERTY_SETTINGS +@given( + families=st.lists( + st.fixed_dictionaries( + dict( + couple=st.booleans(), + universal_credit_pre_benefit_cap=st.one_of( + st.just(0.0), st.floats(0, 40_000) + ), + child_benefit=st.sampled_from([0.0, 1_500.0, 5_000.0]), + child_tax_credit=st.sampled_from([0.0, 3_000.0]), + uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0, 25_323.0]), + uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), + jsa_contrib=st.sampled_from([0.0, 4_000.0]), + esa_contrib=st.sampled_from([0.0, 5_000.0]), + ) + ), + min_size=1, + max_size=8, + ), + year=st.integers(2025, 2030), +) +def test_capped_universal_credit_matches_the_model(families, year): + """Invariant 12: the after-cap Universal Credit counted in a + non-dependant family's gross income equals universal_credit for a family + with no Housing Benefit, Maternity Allowance or deductions.""" + from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + universal_credit_after_benefit_cap, + ) + + people, benunits, households_ = {}, {}, {} + for f, family in enumerate(families): + members = [f"adult_{f}_{k}" for k in range(2 if family["couple"] else 1)] + for k, name in enumerate(members): + people[name] = dict( + age=30 - k, + jsa_contrib=family["jsa_contrib"] if k == 0 else 0.0, + esa_contrib=family["esa_contrib"] if k == 0 else 0.0, + maternity_allowance=0.0, + ) + benunits[f"family_{f}"] = dict( + members=members, + **{ + key: family[key] + for key in [ + "universal_credit_pre_benefit_cap", + "child_benefit", + "child_tax_credit", + "uc_benefit_cap", + "uc_childcare_element", + ] + }, + income_support=0.0, + jsa_income=0.0, + esa_income=0.0, + housing_benefit_pre_benefit_cap=0.0, + uc_deductions=0.0, + ) + households_[f"household_{f}"] = dict(members=members, country="ENGLAND") + sim = Simulation( + situation=for_years( + dict(people=people, benunits=benunits, households=households_), [year] + ) + ) + helper = universal_credit_after_benefit_cap(sim.populations["benunit"], year) + np.testing.assert_allclose( + helper, sim.calculate("universal_credit", year), atol=0.01 + ) diff --git a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py index e829efd5ef..bfe7580b84 100644 --- a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py +++ b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py @@ -13,17 +13,18 @@ YEAR = 2026 -@pytest.fixture(scope="module") -def non_dependants(): +CASES = [ + ("earnings_only", 7_800, 0), + ("maintenance_100", 7_800, 5_200), + ("maintenance_150", 7_800, 7_800), + ("same_gross_earnings", 13_000, 0), +] + + +def build(country, local_authority): # 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: + for name, earnings, maintenance in CASES: people[name] = { "age": {YEAR: 30}, "weekly_hours": {YEAR: 16}, @@ -35,18 +36,30 @@ def non_dependants(): benunits[name] = { "members": [name], "universal_credit": {YEAR: 0}, + "universal_credit_pre_benefit_cap": {YEAR: 0}, "pension_credit": {YEAR: 0}, } households[name] = { "members": [name], - "country": {YEAR: "ENGLAND"}, - "local_authority": {YEAR: "MERTON"}, + "country": {YEAR: country}, + "local_authority": {YEAR: local_authority}, } return Simulation( situation={"people": people, "benunits": benunits, "households": households} ) +@pytest.fixture(scope="module") +def non_dependants(): + return build("ENGLAND", "MERTON") + + +@pytest.fixture(scope="module") +def welsh_non_dependants(): + # Wales's national scheme covers working-age applicants too. + return build("WALES", "CARDIFF") + + def values(simulation, variable): return np.asarray(simulation.calculate(variable, YEAR), dtype=float) @@ -62,16 +75,19 @@ def test_housing_benefit_non_dependant_band_counts_spousal_maintenance(non_depen ) -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" +def test_national_ctr_non_dependant_band_counts_spousal_maintenance( + welsh_non_dependants, +): + # WSI 2013/3029 Sch 6 para 5(2), (9), as up-rated by WSI 2026/13: £150 + # earnings plus £100 maintenance and £250 earnings both fall below £279 + # (£6.70 a week); £150 plus £150 = £300 falls in £279–484.99 (£13.35). + np.testing.assert_allclose( + values( + welsh_non_dependants, "council_tax_reduction_individual_non_dep_deduction" + ), + [348.40, 348.40, 694.20, 348.40], + atol=0.01, ) - 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): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index acebeffe72..899373c74d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -58,12 +58,13 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): names the programme's person-level gross income variable.""" claimant_or_partner = person("is_claimant_or_partner", period) own_income = person(own_gross_income, period) - family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( - person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS - ) - couple_income = ( - person.benunit.sum(own_income * claimant_or_partner) + family_benefits + benunit = person.simulation.populations["benunit"] + family_benefits = universal_credit_after_benefit_cap(benunit, period) + add( + benunit, period, FAMILY_GROSS_INCOME_BENEFITS ) + couple_income = person.benunit.sum( + own_income * claimant_or_partner + ) + benunit.project(family_benefits) annual = where(claimant_or_partner, couple_income, own_income) return annual / WEEKS_IN_YEAR From b97699433a28bcb52222cb69c20b37fa04374e62 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 22:07:32 -0400 Subject: [PATCH 14/31] Pin investment income and losses in the gross-income cases; restate the fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- .../housing_benefit_non_dep_gross_income.yaml | 22 +++++++++++++++++++ ...il_tax_reduction_non_dep_gross_income.yaml | 22 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 40b5dd5254..bd8311447b 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap, counting only the couple's own capped benefits and keeping the childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts, with each jointly liable applicant's deduction taken under its own scheme. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated. Bands include their lower edge. A non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, pays the lowest amount whatever their income. A couple pays one deduction, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and any other adult in the family pays their own. Gross income counts other investment income and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index e186f9be3a..4753918418 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -601,3 +601,25 @@ miscellaneous_income: -300 output: housing_benefit_non_dep_gross_income: 1_000 + +- name: housing_benefit_non_dep_gross_income counts other investment income + # Gross income is earnings and any other income (DWP HB guidance A5.542). + period: 2025 + input: + social_security_income: 0 + employment_income: 1_000 + other_investment_income: 2_000 + output: + housing_benefit_non_dep_gross_income: 3_000 + +- name: housing_benefit_non_dep_gross_income counts a self-employment or property loss as nil + # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # does not reduce other income. + period: 2025 + input: + social_security_income: 0 + employment_income: 5_000 + self_employment_income: -3_000 + property_income: -1_000 + output: + housing_benefit_non_dep_gross_income: 5_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index 3e4f20ac4d..5736b27177 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -721,3 +721,25 @@ miscellaneous_income: -300 output: council_tax_reduction_non_dep_gross_income: 1_000 + +- name: council_tax_reduction_non_dep_gross_income counts other investment income + # Gross income is earnings and any other income (DWP HB guidance A5.542). + period: 2025 + input: + social_security_income: 0 + employment_income: 1_000 + other_investment_income: 2_000 + output: + council_tax_reduction_non_dep_gross_income: 3_000 + +- name: council_tax_reduction_non_dep_gross_income counts a self-employment or property loss as nil + # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # does not reduce other income. + period: 2025 + input: + social_security_income: 0 + employment_income: 5_000 + self_employment_income: -3_000 + property_income: -1_000 + output: + council_tax_reduction_non_dep_gross_income: 5_000 From 634047bcaea40143ecc331919b8a5e74db9869a6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 22:18:07 -0400 Subject: [PATCH 15/31] Count only the couple's income-based JSA and ESA in a couple's gross income Another member of the benefit unit claims those awards in their own right (#2027's claimant_or_partner_* awards), so they no longer raise the couple's band. The HB work-gate cases hold family awards at zero so a nil income is nil, and the property oracle follows the same rules. Co-Authored-By: Claude Opus 5.5 --- ...ing_benefit_non_dep_remunerative_work.yaml | 138 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 24 ++- .../non_dep_deduction/_non_dependants.py | 8 +- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml index c5d3406773..7d0dde3004 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml @@ -3,6 +3,8 @@ # A non-worker or 15-hour worker pays 20.40 * 52 = 1,060.80 in 2026-27, # whatever their income. The exemptions in reg 74(7), (8) and (10) and reg # 55(7)-(9) act through eligibility, which most cases here take as an input. +# Family awards, which a single non-dependant's gross income would count, are +# held at zero. # A 16-hour worker's wages of 9,000 alone are below 192/week; adding # IIDB or child maintenance of 12,000 gives 403.85/week, so 105.20 * 52. @@ -11,6 +13,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -30,6 +40,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -49,6 +67,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -68,6 +94,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -87,6 +121,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -106,6 +148,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -125,6 +175,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -144,6 +202,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -163,6 +229,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 70 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -178,6 +252,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 70 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -197,6 +279,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 22 hours_worked: 0 social_security_income: 0 @@ -212,6 +302,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -228,6 +326,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -244,6 +350,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -260,6 +374,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -276,6 +398,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.dwp.housing_benefit.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND @@ -294,6 +424,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.dwp.housing_benefit.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index f59085ce4d..270be009a1 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -506,8 +506,10 @@ def band_amount(scale, weekly): if reporter is None or reporter >= len(household["non_dependants"]) else members_of[person["benunit"]][reporter] ) + # Pension Credit's payee must also have reached the qualifying age + # (SPCA 2002 s.1(2)(b)), which payee_of doesn't model. if reporter_index is None or couple[reporter_index]: - for b in REPORTED: + for b in ["income_support", "jsa_income", "esa_income"]: assert on[b] == (payee_of(i, b) and household["benefits"][b] > 0), ( b, i, @@ -607,11 +609,27 @@ def band_amount(scale, weekly): if nd["hours"] < scale.remunerative_work_hours: assert ctr_deduction == pytest.approx(lowest, abs=0.01) - # 11. Banding on gross income computed from the inputs. + # 11. Banding on gross income computed from the inputs. Income-based + # JSA and income-related ESA count only where the claimant or partner + # reports them, or no one does. + couple_awards = ( + ["jsa_income", "esa_income"] + if reporter_index is None or couple[reporter_index] + else [] + ) + family_awards = sum( + household["benefits"][b] + for b in [ + "income_support", + "pension_credit", + "universal_credit_pre_benefit_cap", + *couple_awards, + ] + ) if couple[i]: weekly = ( sum(own_gross(j) for j in members_of[person["benunit"]] if couple[j]) - + sum(household["benefits"].values()) + + family_awards ) / WEEKS else: weekly = own_gross(i) / WEEKS diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 899373c74d..e003e7e50f 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -17,14 +17,16 @@ # Family benefits counted in a couple's gross income, besides Universal Credit. # They are awarded to the benefit unit, so the person-level gross income -# variables leave them out. +# variables leave them out. Income-based JSA and income-related ESA count the +# claimant's and partner's award only: another member claims in their own +# right. FAMILY_GROSS_INCOME_BENEFITS = [ "child_tax_credit", "working_tax_credit", "child_benefit", "income_support", - "jsa_income", - "esa_income", + "claimant_or_partner_jsa_income", + "claimant_or_partner_esa_income", "pension_credit", ] From f9f62902b37555bb2d4e6bc67feaecb1e9051c88 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 22:47:24 -0400 Subject: [PATCH 16/31] Count Universal Credit in the income of the claimants it assesses A family's UC counts in the couple's joint gross income where either of them claims it, and in another member's own income where is_uc_claimant makes them the claimant; the UC limb of the exemptions reads the same assessed claimants. Two cases pin it, and that another adult's own ESA stays out of the couple's income. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 107 ++++++++++++++++++ .../non_dep_deduction/_non_dependants.py | 36 ++++-- ...ousing_benefit_non_dep_deduction_exempt.py | 6 +- .../_national_non_dependants.py | 4 +- 4 files changed, 137 insertions(+), 16 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 845e7e1777..40edc163e7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -206,3 +206,110 @@ output: # HB Regs 2006 reg 74(6). housing_benefit_non_dep_deductions: [0, 0] + +- name: Universal Credit counts in the income of the claimant it assesses + # The non-dependant family's Universal Credit is the adult son's own claim + # (is_uc_claimant), so it counts in his gross income, not his parents' + # (HB Regs 2006 reg 74(2), (4)). The parents' joint income is the father's + # 180 a week, under the 192 band: 20.40, one deduction for the couple + # (reg 74(3)). The son works 2 hours, so he pays 20.40 (reg 74(1)(b)). + # (20.40 + 20.40) * 52 = 2,121.60. Counting the son's 100 a week of UC in + # the parents' income would give 280 a week and 64.35. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_household_head: true + father: + age: 50 + employment_income: 9_360 # 180 a week + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + mother: + age: 48 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + son: + age: 27 + employment_income: 1_040 # 20 a week + weekly_hours: 2 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: false + is_uc_claimant: true + benunits: + claimant_unit: + members: [claimant] + non_dependant_unit: + members: [father, mother, son] + universal_credit_pre_benefit_cap: 5_200 # 100 a week + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, father, mother, son] + tenure_type: RENT_FROM_COUNCIL + rent: 10_000 + output: + household_benefits_individual_non_dep_deduction: [0, 1_060.80, 1_060.80, 1_060.80] + housing_benefit_non_dep_deductions: [2_121.60, 0] + +- name: Another adult's own income-related ESA stays out of the couple's income + # The adult son claims income-related ESA in his own right, so it is his + # income, not his parents' (HB Regs 2006 reg 74(4)). The parents' joint + # income is the father's 180 a week, under the 192 band: 20.40, one + # deduction for the couple. The son does no work and pays 20.40 (reg + # 74(1)(b)). (20.40 + 20.40) * 52 = 2,121.60. Counting his 100 a week of + # ESA in the parents' income would give 280 a week and 64.35. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_household_head: true + father: + age: 50 + employment_income: 9_360 # 180 a week + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + mother: + age: 48 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + son: + age: 27 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + non_dependant_unit: + members: [father, mother, son] + esa_income: 5_200 # the son's own award, 100 a week + claimant_or_partner_esa_income: 0 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, father, mother, son] + tenure_type: RENT_FROM_COUNCIL + rent: 10_000 + output: + household_benefits_individual_non_dep_deduction: [0, 1_060.80, 1_060.80, 1_060.80] + housing_benefit_non_dep_deductions: [2_121.60, 0] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index e003e7e50f..91c61a5004 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -53,21 +53,35 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 - para 8(4)): each member's own gross income plus the family's Universal - Credit, tax credits, child benefit, Income Support, income-based JSA, - income-related ESA and Pension Credit. Anyone else in the benefit unit is - a separate non-dependant, banded on their own income. own_gross_income - names the programme's person-level gross income variable.""" + para 8(4)): each member's own gross income plus the family's tax credits, + child benefit, Income Support, income-based JSA, income-related ESA and + Pension Credit, and its Universal Credit where either member claims it. + Anyone else in the benefit unit is a separate non-dependant, banded on + their own income, with the Universal Credit where they alone claim it. + own_gross_income names the programme's person-level gross income + variable.""" claimant_or_partner = person("is_claimant_or_partner", period) own_income = person(own_gross_income, period) benunit = person.simulation.populations["benunit"] - family_benefits = universal_credit_after_benefit_cap(benunit, period) + add( - benunit, period, FAMILY_GROSS_INCOME_BENEFITS + other_awards = benunit.project(add(benunit, period, FAMILY_GROSS_INCOME_BENEFITS)) + # Universal Credit is the income of the claimants it assesses, the + # claimant and partner unless is_uc_claimant names someone else. + universal_credit = benunit.project( + universal_credit_after_benefit_cap(benunit, period) ) - couple_income = person.benunit.sum( - own_income * claimant_or_partner - ) + benunit.project(family_benefits) - annual = where(claimant_or_partner, couple_income, own_income) + uc_claimant = person("is_uc_assessed_claimant", period) + couple_claims_universal_credit = person.benunit.any( + uc_claimant & claimant_or_partner + ) + couple_income = ( + person.benunit.sum(own_income * claimant_or_partner) + + other_awards + + where(couple_claims_universal_credit, universal_credit, 0) + ) + single_income = own_income + where( + uc_claimant & ~couple_claims_universal_credit, universal_credit, 0 + ) + annual = where(claimant_or_partner, couple_income, single_income) return annual / WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 8c31b84f3e..3ef4cd0518 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -33,14 +33,14 @@ def formula(person, period, parameters): full_time_student = is_full_time_student_non_dep(person, period) # IS, income-based JSA and SPC count for the person they are payable # to (reg 2(1), (3)); Universal Credit for both joint claimants. - claimant_or_partner = person("is_claimant_or_partner", period) on_pension_credit = person("is_on_pension_credit", period) on_legacy_income_related_benefit = person( "is_on_income_support", period ) | person("is_on_income_based_jsa", period) # Entitlement before the benefit cap: universal_credit itself depends on - # Housing Benefit through the cap. - entitled_to_universal_credit = claimant_or_partner & ( + # Housing Benefit through the cap. The award is the claimants' it + # assesses. + entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) universal_credit_without_earned_income = ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py index 76da080f73..13198d5d67 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py @@ -52,7 +52,7 @@ def is_qualifying_income_related_benefit_claimant( couple_on_benefit = claimant_or_partner & ( person.benunit.any(on_benefit & claimant_or_partner) ) - entitled_to_universal_credit = claimant_or_partner & ( + entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) partner_rule = ( @@ -85,7 +85,7 @@ def non_dep_deduction_exempt(person, period, parameters, pensioner_applicant): # they are payable to; Universal Credit for both joint claimants. claimant_or_partner = person("is_claimant_or_partner", period) on_pension_credit = person("is_on_pension_credit", period) - entitled_to_universal_credit = claimant_or_partner & ( + entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) qualifying_income_related_benefit_claimant = ( From 86e86d3dbb87fd393f7a19539cb605b96a9d31e5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 23:28:25 -0400 Subject: [PATCH 17/31] Pin that another member's Universal Credit claim exempts no one else Co-Authored-By: Claude Opus 5.5 --- ...sing_benefit_non_dep_deduction_exempt.yaml | 50 +++++++++++++++++++ .../test_non_dependant_spousal_maintenance.py | 5 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml index 5699336677..7412593b5b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml @@ -117,3 +117,53 @@ universal_credit_pre_benefit_cap: 1_000 output: housing_benefit_non_dep_deduction_exempt: false + +- name: Another member's Universal Credit claim doesn't exempt a non-dependant under 25 + # The family's Universal Credit is the 40-year-old's own claim + # (is_uc_claimant), so the 22-year-old partner with no earnings is not + # entitled to it (HB Regs 2006 reg 74(8)(b); SI 2012/2885 Sch 1 para + # 8(8)(c)). The 40-year-old has earnings, so the limb exempts no one. + period: 2026 + input: + people: + claimant: + age: 70 + is_household_head: true + partner_a: + age: 22 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + partner_b: + age: 30 + employment_income: 9_360 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + other_adult: + age: 40 + employment_income: 1_040 + weekly_hours: 2 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: false + is_uc_claimant: true + benunits: + claimant_unit: + members: [claimant] + pension_credit: 0 + non_dependant_unit: + members: [partner_a, partner_b, other_adult] + universal_credit_pre_benefit_cap: 5_200 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, partner_a, partner_b, other_adult] + country: ENGLAND + local_authority: MAIDSTONE + output: + housing_benefit_non_dep_deduction_exempt: [false, false, false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false, false, false] diff --git a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py index bfe7580b84..ab7872153d 100644 --- a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py +++ b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py @@ -1,7 +1,8 @@ """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. +HB Regs 2006 reg 74(2), (9), pension-age HB reg 55(2), (10), SI 2012/2885 +Sch 1 para 8(2), (9) and WSI 2013/3029 Sch 6 para 5(2), (9) do not disregard +spousal maintenance. It remains exempt from income tax under ITTOIA 2005 s 727. """ From 008b8b0311d50658265d7615958177911c6b6024 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:51:25 -0500 Subject: [PATCH 18/31] Read Universal Credit's own earned income and capped award for non-dependants The exemption for a non-dependant on Universal Credit "calculated on the basis that [they do] not have any earned income" now reads uc_individual_earned_income, so allowable expenses, company income, trading losses and the couple minimum income floor follow UC's own rules. The capped award counted in a couple's gross income now shares the award's calculation of the regulation 81 reduction and of the part a protected minimum floor lets it take, and reads a stored or neutralized universal_credit, a universal_credit formula a reform replaces, and a stored cap reduction or welfare-benefit total. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/utils/supplied_inputs.py | 4 + .../non_dep_deduction/_non_dependants.py | 122 +++++++++++------- ...d_benefits_individual_non_dep_deduction.py | 2 +- .../benefit_cap/uc_benefit_cap_reduction.py | 18 ++- .../uc_benefit_cap_reduction_after_floor.py | 28 +++- .../_national_non_dependants.py | 2 +- 6 files changed, 115 insertions(+), 61 deletions(-) diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index 38897f81bf..f6ff5000a6 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -55,6 +55,7 @@ "receives_carer_benefit", "receives_carers_allowance", "receives_carer_support_payment", + "benefit_cap_welfare_benefits", "bus_fare_spending", "bus_in_london_trips", "current_education", @@ -83,9 +84,12 @@ "tenure_type", "targeted_childcare_entitlement", "trading_loss", + "uc_benefit_cap_reduction", + "uc_benefit_cap_reduction_after_floor", "uc_carer_element", "uc_LCWRA_element", "universal_childcare_entitlement", + "universal_credit", } ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 91c61a5004..827c86f9a4 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -4,10 +4,20 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.benefit_cap import benefit_cap_couple from policyengine_uk.utils.benefit_unit import add_for_members, award_of_couple +from policyengine_uk.utils.supplied_inputs import stored_as_input from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( CLAIMANT_OR_PARTNER_BENEFITS as CAPPED_CLAIMANT_OR_PARTNER_BENEFITS, FAMILY_BENEFITS as CAPPED_FAMILY_BENEFITS, ) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_reduction import ( + benefit_cap_reduction_of, +) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_reduction_after_floor import ( + benefit_cap_reduction_applied, +) +from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( + universal_credit as universal_credit_variable, +) from policyengine_uk.variables.gov.dwp.esa_income import ( income_related_esa_award_on_reports, ) @@ -30,15 +40,6 @@ "pension_credit", ] -# Statutory payments treated as employed earnings (UC Regs 2013 reg 55(4)). -# Statutory adoption, shared parental, parental bereavement and neonatal care -# pay have no input. -STATUTORY_PAY_EARNINGS = [ - "statutory_sick_pay", - "statutory_maternity_pay", - "statutory_paternity_pay", -] - # The welfare benefits the benefit cap counts (benefit_cap_welfare_benefits) # other than Housing Benefit, and Maternity Allowance, a welfare benefit # (Welfare Reform Act 2012 s.96(10)(i)) that total leaves out. Bereavement @@ -50,7 +51,7 @@ ] + ["maternity_allowance"] -def non_dependant_weekly_gross_income(person, period, own_gross_income): +def non_dependant_weekly_gross_income(person, period, parameters, own_gross_income): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 para 8(4)): each member's own gross income plus the family's tax credits, @@ -67,7 +68,7 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): # Universal Credit is the income of the claimants it assesses, the # claimant and partner unless is_uc_claimant names someone else. universal_credit = benunit.project( - universal_credit_after_benefit_cap(benunit, period) + universal_credit_after_benefit_cap(benunit, period, parameters) ) uc_claimant = person("is_uc_assessed_claimant", period) couple_claims_universal_credit = person.benunit.any( @@ -85,18 +86,61 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): return annual / WEEKS_IN_YEAR -def universal_credit_after_benefit_cap(benunit, period): +def reads_simulation_value(benunit, period, variable_name, default_formula=None): + """Whether to read the variable rather than recalculate it here: a value + stored as an input for the period, a neutralized variable, or, where + default_formula is given, a formula a reform has replaced.""" + variable = benunit.simulation.tax_benefit_system.get_variable(variable_name) + if variable.is_neutralized: + return True + if default_formula is not None: + formula = variable.get_formula(period) + if formula is None or formula.__code__ != default_formula.__code__: + return True + return stored_as_input(benunit, variable_name, period) + + +def universal_credit_after_benefit_cap(benunit, period, parameters): """A family's Universal Credit award after the benefit cap (UC Regs 2013 reg 81), for a family that is not liable for rent. universal_credit and uc_benefit_cap_reduction depend on Housing Benefit through the welfare - benefits they count, so the cap (uc_benefit_cap) is applied here to the - same welfare benefits other than Housing Benefit, which such a family does - not receive (UC reg 80(2A) counts it as nil in any case), for the same - single person or couple (benefit_cap_couple; reg 79(1)). The award is - reduced by the excess minus the childcare costs element, and not at all - where that element is greater than the excess (reg 81(1)-(2)). Deductions + benefits they count, so this applies the cap to the same welfare benefits + other than Housing Benefit, which such a family does not receive (UC reg + 80(2A) counts it as nil in any case), for the same single person or couple + (benefit_cap_couple; reg 79(1)). It shares the award's own calculation of + the reduction (benefit_cap_reduction_of) and of the part a protected + minimum floor lets it take (benefit_cap_reduction_applied). Deductions paid from the award to third parties or for debts are still the family's - income, so they are not taken off.""" + income, so they are not taken off, and the floor is applied as for a + family with none: the deduction rate depends on the cap reduction, and so + on Housing Benefit. A universal_credit stored as an input or given a new + formula by a reform is read as it is, and so is a stored or neutralized + uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or + benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" + if reads_simulation_value( + benunit, period, "universal_credit", universal_credit_variable.formula + ): + return benunit("universal_credit", period) + if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction_after_floor"): + applied = benunit("uc_benefit_cap_reduction_after_floor", period) + else: + if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction"): + reduction = benunit("uc_benefit_cap_reduction", period) + else: + reduction = benefit_cap_reduction_of( + benunit, period, capped_welfare_benefits(benunit, period) + ) + applied = benefit_cap_reduction_applied( + benunit, period, parameters, reduction, 0 + ) + return max_(benunit("universal_credit_pre_benefit_cap", period) - applied, 0) + + +def capped_welfare_benefits(benunit, period): + """The welfare benefits the Universal Credit cap counts, with Housing + Benefit at nil (reg 80(2A)) and Maternity Allowance in.""" + if reads_simulation_value(benunit, period, "benefit_cap_welfare_benefits"): + return benunit("benefit_cap_welfare_benefits", period) couple = benefit_cap_couple(benunit, period) income_based = award_of_couple( benunit, @@ -113,41 +157,21 @@ def universal_credit_after_benefit_cap(benunit, period): income_related_esa_award_on_reports, couple, ) - capped = income_based + add_for_members( + return income_based + add_for_members( benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT, couple ) - excess = max_(0, capped - benunit("uc_benefit_cap", period)) - reduction = max_(0, excess - benunit("uc_childcare_element", period)) - return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) def has_earned_income(person, period): - """Whether the person has earned income (UC Regs 2013 reg 52): employed - earnings, including statutory sick, maternity and paternity pay and less - relievable pension contributions (reg 55(4)-(5)); self-employed earnings, - a loss counting as nil and less any relievable pension contributions not - already deducted from employed earnings (reg 57(2), steps 3-4), with the - minimum income floor applying where the Universal Credit model applies it - (reg 62); and other paid work.""" - pension_contributions = person("pension_contributions", period) - employed_gross = add(person, period, ["employment_income", *STATUTORY_PAY_EARNINGS]) - has_employed_earnings = employed_gross > 0 - employed = max_(0, employed_gross - pension_contributions) - # Contributions are deducted from employed earnings where there are any - # (reg 55(5)(a)), and otherwise from self-employed earnings (reg 57(2), - # step 4). - self_employed = max_( - 0, - max_(0, person("self_employment_income", period)) - - where(has_employed_earnings, 0, pension_contributions), - ) - self_employed = where( - person("uc_mif_applies", period), - max_(self_employed, person("uc_minimum_income_floor", period)), - self_employed, - ) - other = max_(0, person("miscellaneous_income", period)) - return (employed + self_employed + other) > 0 + """Whether the person has earned income as Universal Credit calculates it + (HB Regs 2006 reg 74(8)(b), (11) and the CTR schemes' equivalents give + "earned income" the meaning in UC Regs 2013 reg 52): + uc_individual_earned_income, which takes off allowable expenses, tax, + National Insurance and relievable pension contributions (reg 55(3), (5)), + counts a company's income attributed to its owner and sets trading losses + against profits (regs 57(2), 77(3)), and applies the minimum income floor + only where reg 62 does, a couple's floor included.""" + return person("uc_individual_earned_income", period) > 0 def is_on_legacy_income_related_benefit(person, period): 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 313c681e39..d9389129d4 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 @@ -30,7 +30,7 @@ class household_benefits_individual_non_dep_deduction(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction weekly_income = non_dependant_weekly_gross_income( - person, period, "housing_benefit_non_dep_gross_income" + person, period, parameters, "housing_benefit_non_dep_gross_income" ) # Reg 6(6), or pension-age reg 6(6): a person on Income Support, # income-based JSA or income-related ESA is not in remunerative work. diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index eacb266686..adb72a2912 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -1,6 +1,19 @@ from policyengine_uk.model_api import * +def benefit_cap_reduction_of(benunit, period, welfare_benefits): + """The regulation 81 reduction for welfare benefits that already count + Housing Benefit as nil (reg. 80(2A)): the excess over the Universal Credit + cap, minus the childcare costs element, and nothing where that element is + greater than the excess or the family has no award (reg. 78(1)). The + non-dependant deductions call this too, with their own total of welfare + benefits (housing_benefit/non_dep_deduction/_non_dependants.py).""" + excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) + reduction = max_(excess - benunit("uc_childcare_element", period), 0) + has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 + return where(has_award, reduction, 0) + + class uc_benefit_cap_reduction(Variable): value_type = float entity = BenUnit @@ -34,7 +47,4 @@ def formula(benunit, period, parameters): welfare_benefits = benunit("benefit_cap_welfare_benefits", period) - benunit( "housing_benefit_pre_benefit_cap", period ) - excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) - reduction = max_(excess - benunit("uc_childcare_element", period), 0) - has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 - return where(has_award, reduction, 0) + return benefit_cap_reduction_of(benunit, period, welfare_benefits) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py index b40442dc06..dbe7c0b409 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py @@ -4,6 +4,24 @@ ) +def benefit_cap_reduction_applied( + benunit, period, parameters, cap_reduction, deduction_demand +): + """The part of a regulation 81 reduction taken off the award, given the + deductions demanded from it, under any protected minimum floor. The + non-dependant deductions call this too + (housing_benefit/non_dep_deduction/_non_dependants.py).""" + p = parameters(period).gov.dwp.universal_credit.deductions + _, applied = allocate_uc_reductions( + benunit("universal_credit_pre_benefit_cap", period), + cap_reduction, + deduction_demand, + benunit("uc_maximum_reduction", period), + p.minimum_payable * MONTHS_IN_YEAR, + ) + return applied + + class uc_benefit_cap_reduction_after_floor(Variable): label = "UC benefit cap reduction applied" documentation = ( @@ -24,18 +42,16 @@ class uc_benefit_cap_reduction_after_floor(Variable): defined_for = "claims_universal_credit" def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.universal_credit.deductions # JRF's September 2026 example (standard allowance £98 a week, a £15 # Fair Repayment Rate deduction, an £80 cap reduction, an 85% floor) # limits the total to £15. Meeting the deduction first, as here, # removes the whole cap reduction. - _, cap_reduction = allocate_uc_reductions( - benunit("universal_credit_pre_benefit_cap", period), + return benefit_cap_reduction_applied( + benunit, + period, + parameters, benunit("uc_benefit_cap_reduction", period), benunit("uc_deduction_rate", period) * benunit("uc_standard_allowance", period) * benunit("uc_award_carries_deductions", period), - benunit("uc_maximum_reduction", period), - p.minimum_payable * MONTHS_IN_YEAR, ) - return cap_reduction diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py index 13198d5d67..e5bc21bb04 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py @@ -148,7 +148,7 @@ def individual_non_dep_deduction(person, period, parameters, pensioner_applicant is_wales_scheme(country), ] weekly_income = non_dependant_weekly_gross_income( - person, period, "council_tax_reduction_non_dep_gross_income" + person, period, parameters, "council_tax_reduction_non_dep_gross_income" ) no_income = np.zeros_like(weekly_income) remunerative_work_hours = select( From eeb0cf8659cf89e965ff831c7fc708480b9faaac Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:51:32 -0500 Subject: [PATCH 19/31] Take both Welsh deductions only for the couple's own Universal Credit award WSI 2013/3029 Sch 6 para 5(3) lifts the one-deduction rule where there is an award of Universal Credit to the couple. Another member's award as a single claimant now leaves the couple one deduction. Co-Authored-By: Claude Opus 5.5 --- ...ouncil_tax_reduction_non_dep_deductions.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index f1741d1f5c..99297c3b37 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -20,9 +20,10 @@ class council_tax_reduction_non_dep_deductions(Variable): "Deductions for the non-dependants in other benefit units of the " "household: one per couple, the higher of the two members' amounts " "(both, in the Welsh scheme for people who are not pensioners, when " - "the couple has a Universal Credit award), one for each other member, " - "each under the applicant family's own scheme and apportioned between " - "jointly liable applicants; none if the applicant or partner is exempt." + "the couple's own Universal Credit award assesses either member), one " + "for each other member, each under the applicant family's own scheme " + "and apportioned between jointly liable applicants; none if the " + "applicant or partner is exempt." ) definition_period = YEAR unit = GBP @@ -39,9 +40,17 @@ def formula(benunit, period, parameters): country = benunit.household("country", period) # WSI 2013/3029 Sch 6 para 5(3): a working-age applicant's deduction is # for each member of a non-dependant couple with a Universal Credit - # award. - universal_credit_couple = is_wales_scheme(country) & ( - benunit("universal_credit_pre_benefit_cap", period) > 0 + # award. The award is the couple's own: one that assesses the + # claimant or partner. Another member's award, as a single claimant, + # leaves the couple with one deduction. + couple_claims_universal_credit = benunit.any( + person("is_uc_assessed_claimant", period) + & person("is_claimant_or_partner", period) + ) + universal_credit_couple = ( + is_wales_scheme(country) + & couple_claims_universal_credit + & (benunit("universal_credit_pre_benefit_cap", period) > 0) ) # council_tax_reduction_individual_non_dep_deduction is each person's # deduction under the scheme of the household head's family. A jointly From 44985a8c29b09e5afd7f46c7b6eb19871a7397c8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:51:36 -0500 Subject: [PATCH 20/31] Test the earned-income, capped-award and Welsh couple rules Four earned-income cases (expenses, a trading loss, company income, a couple's floor), two Welsh cases where a third adult alone claims Universal Credit, the capped award under an 85% protected floor and with stored and replaced values, and an invariant comparing the exemption with Universal Credit's earned income. The property generator now varies is_uc_claimant. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 354 +++++++++++++++ ...test_non_dependant_deduction_properties.py | 404 +++++++++++++++--- 2 files changed, 687 insertions(+), 71 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index f05106199d..49605e630c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1226,3 +1226,357 @@ # 502.50 a week: the HB 485 band (119.85) and the CTR 485 band (13.30). housing_benefit_non_dep_deductions: [6_232.20, 0] council_tax_reduction_non_dep_deductions: [691.60, 0] + +# Round 7 of the independent review: the Universal Credit exemption reads +# Universal Credit's own calculation of earned income (uc_individual_earned_income). + +- name: Employment expenses that cancel the wages leave no earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + employment_income: 1_000 + employment_expenses: 1_000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 55(3)(a): allowable employment expenses come off + # employed earnings, leaving none (HB reg 74(8)(b), (11); WSI 2013/3029 + # Sch 1 para 3(8)(c), (10)). + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: A trading loss that cancels the profit leaves no earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + self_employment_income: 1_000 + trading_loss: 1_000 + uc_mif_applies: false + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 57(2): the trades' profits and losses are added + # together before any nil floor, so the loss cancels the profit. The + # minimum income floor is held off, so only the loss rule decides. + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: A company's income attributed to its owner is earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + stands_as_sole_owner_or_partner_of_company: true + owned_company_carries_on_trade: true + owned_company_income_share: 1_000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 77(3)(b): the company's income is treated as the + # owner's self-employed earnings, so the exemption doesn't apply. Outside + # remunerative work the non-dependant pays the lowest amounts: HB 20.40 + # and the Welsh pensioner scale's 6.70 a week. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [348.40, 0] + +- name: The minimum income floor doesn't apply where a partner's earnings meet the couple threshold + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + weekly_hours: 0 + employment_income: 0 + uc_mif_applies: true + uc_minimum_income_floor: 10_000 + partner: + age: 30 + current_education: NOT_IN_EDUCATION + weekly_hours: 15 + employment_income: 30_000 + uc_mif_applies: false + uc_minimum_income_floor: 10_000 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant, partner] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant, partner] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 62(3): a member of a couple is treated as having the + # floor only while the couple's earned income is below the couple + # threshold. The partner's earned income exceeds it, so the non-dependant + # has none and is exempt. The Welsh working-age scheme deducts for each + # member of a Universal Credit couple (Sch 6 para 5(3)): the exempt + # non-dependant's nil and the partner's 6.70 a week, as the partner works + # under 16 hours. + housing_benefit_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0, 348.40] + council_tax_reduction_non_dep_deductions: [348.40, 0] + +- name: A third adult's Universal Credit leaves a Welsh non-dependant couple one deduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 40 + is_household_head: true + is_claimant_or_partner: true + is_uc_claimant: false + current_education: NOT_IN_EDUCATION + first: + age: 35 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 10_400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + second: + age: 34 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 5_200 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + third: + age: 25 + is_claimant_or_partner: false + is_uc_claimant: true + employment_income: 0 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + applicant_unit: + members: [applicant] + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + non_dependant_unit: + members: [first, second, third] + universal_credit_pre_benefit_cap: 3_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + home: + members: [applicant, first, second, third] + country: WALES + local_authority: CARDIFF + rent: 6_000 + council_tax: 2_000 + tenure_type: RENT_PRIVATELY + savings: 0 + output: + # WSI 2013/3029 Sch 6 para 5(3): only the couple's own Universal Credit + # award takes it out of the one-deduction rule. Here the third adult + # claims alone, so the couple, banded on 15,600 a year (300 a week), pays + # one 13.35 deduction, and the third adult, on Universal Credit with no + # earned income, pays none (para 5(8)(c)). + council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 694.20, 0] + council_tax_reduction_non_dep_deductions: [694.20, 0] + +- name: A third adult's Universal Credit leaves the couple one deduction from each Welsh applicant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + is_claimant_or_partner: true + is_uc_claimant: false + current_education: NOT_IN_EDUCATION + sharer: + age: 30 + is_claimant_or_partner: true + is_uc_claimant: false + current_education: NOT_IN_EDUCATION + first: + age: 35 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 10_400 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + second: + age: 34 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 5_200 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + third: + age: 25 + is_claimant_or_partner: false + is_uc_claimant: true + employment_income: 9_100 + hours_worked: 1_040 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + non_dependant_family: + members: [first, second, third] + would_claim_uc: false + universal_credit_pre_benefit_cap: 3_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 520 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + home: + members: [head, sharer, first, second, third] + country: WALES + local_authority: CARDIFF + rent: 12_000 + council_tax: 2_000 + tenure_type: RENT_PRIVATELY + savings: 0 + output: + # The pensioner head and the working-age sharer each apply under their + # own Welsh scheme and share the deductions equally (Sch 1 para 3(5); Sch + # 6 para 5(5)). The couple, banded on 15,600 + 520 = 16,120 a year (310 + # a week), pays one deduction under either scheme, as the Universal + # Credit is the third adult's; the third adult, banded on 9,100 + 3,000 + # (232.69 a week), pays their own. + council_tax_reduction_non_dep_deductions: [521.30, 521.30, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 270be009a1..1d43da7e1f 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -24,10 +24,12 @@ flags), the claimant family's deductions are 0. 5. Aggregation: a family's deductions are the sum, over the other families in the household, of their claimant and partner's higher amount (both amounts - for a Welsh working-age applicant's non-dependant couple with a Universal - Credit award) plus each other member's, recomputed here from the - individual amounts, times the family's share of the rent (Housing Benefit, - reg 74(5)) or of the council tax liability (CTR, Sch 1 para 8(5)). + for a Welsh working-age applicant's non-dependant couple whose own + Universal Credit award assesses either member; another member's award as + a single claimant leaves the couple one deduction) plus each other + member's, recomputed here from the individual amounts, times the family's + share of the rent (Housing Benefit, reg 74(5)) or of the council tax + liability (CTR, Sch 1 para 8(5)). 6. Monotonicity (metamorphic): raising any non-dependant's income never lowers an individual deduction or a family's deductions. 7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, @@ -49,10 +51,10 @@ the model's is_on_* status (HB Regs 2006 reg 2(1), (3), (3A)); where the claimant or partner reports the award, or no one does, that is the reporter or the benefit unit's head, which the test checks. Universal - Credit counts for both joint claimants. Earned income includes statutory - sick, maternity and paternity pay, less relievable pension contributions, - which come off self-employed earnings only when there are no employed - earnings; a self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). + Credit counts for the claimants it assesses: the members the generated + is_uc_claimant flags name (by default the claimant and partner), the two + eldest where more are flagged, which the test checks. Earned income is + Universal Credit's own calculation (invariant 13). 10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) 11. Banding: a non-dependant in remunerative work who is neither exempt nor on IS, JSA(IB) or ESA(IR) pays the amount of the band whose lower edge is @@ -60,13 +62,23 @@ the generated inputs: earnings, statutory sick, maternity and paternity pay, self-employed profit with a loss as nil, and other investment income; for the claimant or partner of the non-dependant's family, the couple's - sum plus the family's IS, JSA(IB), ESA(IR), Pension Credit and Universal - Credit. Incomes within a penny a week of an edge are skipped, since the + sum plus the family's IS, JSA(IB), ESA(IR) and Pension Credit, and its + Universal Credit where the award assesses either of them. Another member + who claims Universal Credit alone is banded on their own income and that + award. Incomes within a penny a week of an edge are skipped, since the inputs are stored as float32. 12. Differential: the after-cap Universal Credit used in a non-dependant family's gross income equals the model's universal_credit for a family with no Housing Benefit, no Maternity Allowance and no deductions from - its award. + its award, under current law and under an 85% protected minimum floor; + and it reads a stored universal_credit, cap reduction or welfare-benefit + total, and a universal_credit formula a reform replaces. +13. Differential: the Universal Credit exemption's earned-income test agrees + with Universal Credit's own calculation (uc_individual_earned_income) + across employment expenses, trading losses, company income attributed to + its owner, pension contributions and the minimum income floor of single + claimants and couples; and it agrees with the inputs where they settle it + without the tax and National Insurance calculation. """ from pathlib import Path @@ -214,6 +226,13 @@ def households(draw): # Which adult non-dependant reports IS, JSA(IB), ESA(IR) and SPC, if # anyone does. reporter=draw(st.sampled_from([None, 0, 1])), + # Explicit is_uc_claimant flags for the non-dependants, by position, + # or None to leave the claimant and partner as the claimants. A flag + # on the young person alone makes them a single claimant beside the + # couple. + uc_claimants=draw( + st.one_of(st.none(), st.lists(st.booleans(), min_size=3, max_size=3)) + ), claims_child_benefit=draw(st.sampled_from([True, True, False])), non_dependants=non_dependants, ) @@ -272,6 +291,8 @@ def build(population, year, raise_income=False): if benefit != "is_blind" }, ) + if household.get("uc_claimants") is not None: + people[name]["is_uc_claimant"] = household["uc_claimants"][n] members.append(name) persons.append( dict(household=h, benunit=2 * h + 1, non_dependant=nd, position=n) @@ -308,9 +329,9 @@ def calculate(sim, year): "council_tax_reduction_individual_non_dep_deduction", "is_benunit_head", "is_claimant_or_partner", + "is_uc_assessed_claimant", "is_qualifying_young_person_for_child_benefit", - "uc_mif_applies", - "uc_minimum_income_floor", + "uc_individual_earned_income", "is_on_income_support", "is_on_income_based_jsa", "is_on_income_related_esa", @@ -439,6 +460,31 @@ def test_non_dependant_deduction_invariants(population, year): members_of = {} for i, person in enumerate(persons): members_of.setdefault(person["benunit"], []).append(i) + # Universal Credit's claimants: the members is_uc_claimant flags (by + # default the claimant and partner), the two eldest where inputs flag + # more (WRA 2012 ss. 2(1), 40), in the order entered at equal ages. + assessed = r["is_uc_assessed_claimant"].astype(bool) + for members in members_of.values(): + household = population[persons[members[0]]["household"]] + flags = household.get("uc_claimants") + + def flagged(j): + if flags is None or persons[j]["non_dependant"] is None: + return bool(couple[j]) + return flags[persons[j]["position"]] + + def age(j): + nd = persons[j]["non_dependant"] + return household["claimant_age"] if nd is None else nd["age"] + + eldest = sorted(filter(flagged, members), key=lambda j: -age(j))[:2] + assert sorted(j for j in members if assessed[j]) == sorted(eldest) + # Whether a family's Universal Credit is the couple's own: it assesses + # the claimant or partner. + couple_claims_universal_credit = { + b: any(couple[j] and assessed[j] for j in members) + for b, members in members_of.items() + } def payee_of(i, benefit): """The award's payee: the member who reports it, or the head.""" @@ -497,8 +543,8 @@ def band_amount(scale, weekly): # IS, JSA(IB), ESA(IR) and SPC count for their payee (HB reg 2(1), # (3)-(3A)): the model's is_on_* status. Where the reporter, if any, is # the claimant or partner, that is the reporter, or the benefit unit's - # head where none reports. Universal Credit counts for both joint - # claimants. + # head where none reports. Universal Credit counts for the claimants + # it assesses. on = {b: bool(r[IS_ON[b]][i]) for b in REPORTED} reporter = household.get("reporter") reporter_index = ( @@ -515,25 +561,14 @@ def band_amount(scale, weekly): i, ) on["universal_credit_pre_benefit_cap"] = ( - bool(couple[i]) + bool(assessed[i]) and household["benefits"]["universal_credit_pre_benefit_cap"] > 0 ) student = nd["education"] != "NOT_IN_EDUCATION" - # Earned income (UC regs 55(4)-(5), 57(2), 62), in the inputs' float32. - statutory_pay = nd.get("statutory_pay", {}) - employed_gross = f32(nd["earnings"]) + sum( - (f32(statutory_pay.get(name, 0.0)) for name in STATUTORY_PAY), f32(0) - ) - pension = f32(nd.get("pension_contributions", 0.0)) - employed = max(f32(0), employed_gross - pension) - self_employed = max( - f32(0), - max(f32(0), f32(nd["self_employment"])) - - (f32(0) if employed_gross > 0 else pension), - ) - if r["uc_mif_applies"][i]: - self_employed = max(self_employed, f32(r["uc_minimum_income_floor"][i])) - earned = employed + self_employed > 0 + # Earned income is Universal Credit's own calculation (HB reg 74(11), + # CTR Sch 1 para 8(11A) and equivalents: UC Regs 2013 reg 52), which + # invariant 13 checks against the inputs. + earned = r["uc_individual_earned_income"][i] > 0 uc_without_earnings = on["universal_credit_pre_benefit_cap"] and not earned hb_exempt = ( student @@ -619,20 +654,23 @@ def band_amount(scale, weekly): ) family_awards = sum( household["benefits"][b] - for b in [ - "income_support", - "pension_credit", - "universal_credit_pre_benefit_cap", - *couple_awards, - ] + for b in ["income_support", "pension_credit", *couple_awards] ) + # Universal Credit is the income of the claimants it assesses: the + # couple's, or a single claimant's beside it. + universal_credit = household["benefits"]["universal_credit_pre_benefit_cap"] + couple_claims = couple_claims_universal_credit[person["benunit"]] if couple[i]: weekly = ( sum(own_gross(j) for j in members_of[person["benunit"]] if couple[j]) + family_awards + + (universal_credit if couple_claims else 0.0) ) / WEEKS else: - weekly = own_gross(i) / WEEKS + weekly = ( + own_gross(i) + + (universal_credit if assessed[i] and not couple_claims else 0.0) + ) / WEEKS if ( r["housing_benefit_individual_non_dep_deduction_eligible"][i] and nd["hours"] >= hb.remunerative_work_hours @@ -679,6 +717,7 @@ def band_amount(scale, weekly): welsh_working_age = [ population[b // 2]["country"] == "WALES" and not applicant_pensioner[b // 2] + and couple_claims_universal_credit[b] and r["universal_credit_pre_benefit_cap"][b] > 0 for b in range(benunit_count) ] @@ -831,36 +870,9 @@ def test_ctr_scales_match_enacted_sums(): assert list(node.amounts) == pytest.approx(row["amounts"]), (scheme, year) -@PROPERTY_SETTINGS -@given( - families=st.lists( - st.fixed_dictionaries( - dict( - couple=st.booleans(), - universal_credit_pre_benefit_cap=st.one_of( - st.just(0.0), st.floats(0, 40_000) - ), - child_benefit=st.sampled_from([0.0, 1_500.0, 5_000.0]), - child_tax_credit=st.sampled_from([0.0, 3_000.0]), - uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0, 25_323.0]), - uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), - jsa_contrib=st.sampled_from([0.0, 4_000.0]), - esa_contrib=st.sampled_from([0.0, 5_000.0]), - ) - ), - min_size=1, - max_size=8, - ), - year=st.integers(2025, 2030), -) -def test_capped_universal_credit_matches_the_model(families, year): - """Invariant 12: the after-cap Universal Credit counted in a - non-dependant family's gross income equals universal_credit for a family - with no Housing Benefit, Maternity Allowance or deductions.""" - from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - universal_credit_after_benefit_cap, - ) - +def capped_universal_credit_families(families, year, **extra): + """One household per family: the generated awards, no Housing Benefit, + no Maternity Allowance and no deductions from the award.""" people, benunits, households_ = {}, {}, {} for f, family in enumerate(families): members = [f"adult_{f}_{k}" for k in range(2 if family["couple"] else 1)] @@ -887,15 +899,265 @@ def test_capped_universal_credit_matches_the_model(families, year): jsa_income=0.0, esa_income=0.0, housing_benefit_pre_benefit_cap=0.0, + uc_deduction_rate=0.0, uc_deductions=0.0, + **extra, ) households_[f"household_{f}"] = dict(members=members, country="ENGLAND") + return for_years( + dict(people=people, benunits=benunits, households=households_), [year] + ) + + +def capped_universal_credit(sim, year): + from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + universal_credit_after_benefit_cap, + ) + + return universal_credit_after_benefit_cap( + sim.populations["benunit"], year, sim.tax_benefit_system.parameters + ) + + +CAPPED_FAMILY = st.fixed_dictionaries( + dict( + couple=st.booleans(), + universal_credit_pre_benefit_cap=st.one_of(st.just(0.0), st.floats(0, 40_000)), + child_benefit=st.sampled_from([0.0, 1_500.0, 5_000.0]), + child_tax_credit=st.sampled_from([0.0, 3_000.0]), + uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0, 25_323.0]), + uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), + jsa_contrib=st.sampled_from([0.0, 4_000.0]), + esa_contrib=st.sampled_from([0.0, 5_000.0]), + ) +) +ONE_CAPPED_FAMILY = dict( + couple=False, + universal_credit_pre_benefit_cap=20_000.0, + child_benefit=0.0, + child_tax_credit=0.0, + uc_benefit_cap=14_753.0, + uc_childcare_element=0.0, + jsa_contrib=0.0, + esa_contrib=0.0, +) + + +@PROPERTY_SETTINGS +@given( + families=st.lists(CAPPED_FAMILY, min_size=1, max_size=8), + year=st.integers(2025, 2030), + protected_floor=st.sampled_from([0.0, 0.85]), +) +@example(families=[ONE_CAPPED_FAMILY], year=2025, protected_floor=0.85) +def test_capped_universal_credit_matches_the_model(families, year, protected_floor): + """Invariant 12: the after-cap Universal Credit counted in a + non-dependant family's gross income equals universal_credit for a family + with no Housing Benefit, Maternity Allowance or deductions, with or + without a protected minimum floor.""" + reform = ( + { + "gov.dwp.universal_credit.deductions.protected_floor": { + f"{year}-01-01.{year}-12-31": protected_floor + } + } + if protected_floor + else None + ) + sim = Simulation( + situation=capped_universal_credit_families(families, year), reform=reform + ) + np.testing.assert_allclose( + capped_universal_credit(sim, year), + sim.calculate("universal_credit", year), + atol=0.01, + ) + + +def test_capped_universal_credit_reads_stored_and_replaced_values(): + """Invariant 12: a stored award, cap reduction or welfare-benefit total, + and a replaced universal_credit formula, reach the gross income. The + family's Universal Credit before the cap is 20,000 against a 14,753 cap.""" + from policyengine_core.reforms import Reform + from policyengine_core.simulations import Simulation as CoreSimulation + + from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( + universal_credit as current_law, + ) + + year = 2025 + for stored, expected in [ + (dict(), 14_753), + (dict(universal_credit=15_000.0), 15_000), + (dict(uc_benefit_cap_reduction=0.0), 20_000), + (dict(uc_benefit_cap_reduction_after_floor=1_000.0), 19_000), + (dict(benefit_cap_welfare_benefits=10_000.0), 20_000), + ]: + sim = Simulation( + situation=capped_universal_credit_families( + [ONE_CAPPED_FAMILY], year, **stored + ) + ) + helper = capped_universal_credit(sim, year) + assert helper == pytest.approx([expected], abs=0.01), stored + assert helper == pytest.approx( + sim.calculate("universal_credit", year), abs=0.01 + ), stored + + constant_award = type( + "universal_credit", + (current_law,), + dict(formula=lambda benunit, period, parameters: benunit.filled_array(12_345)), + ) + + class ConstantAward(Reform): + def apply(self): + self.update_variable(constant_award) + + sim = CoreSimulation( + tax_benefit_system=ConstantAward(SYSTEM), + situation=capped_universal_credit_families([ONE_CAPPED_FAMILY], year), + default_input_period=year, + default_calculation_period=year, + ) + assert capped_universal_credit(sim, year) == pytest.approx([12_345], abs=0.01) + + +# Earned-income cases for invariant 13. Each sets the person's earnings +# inputs; "settled" is what the inputs alone say, where they say it without +# the tax and National Insurance calculation: no earnings left after +# expenses, pension contributions and trading losses (False), or a small +# positive amount no deduction can remove (True). +EARNINGS = { + "none": (dict(), False), + "wages": (dict(employment_income=1_000.0), True), + "wages_cancelled_by_expenses": ( + dict(employment_income=1_000.0, employment_expenses=1_000.0), + False, + ), + "wages_partly_cancelled_by_expenses": ( + dict(employment_income=1_000.0, employment_expenses=500.0), + True, + ), + "wages_into_pension": ( + dict(employment_income=1_000.0, employee_pension_contributions=1_000.0), + None, + ), + "profit": (dict(self_employment_income=1_000.0), None), + "profit_cancelled_by_loss": ( + dict(self_employment_income=1_000.0, trading_loss=1_000.0), + None, + ), + "loss": (dict(self_employment_income=-500.0), None), + "company_income": ( + dict( + stands_as_sole_owner_or_partner_of_company=True, + owned_company_carries_on_trade=True, + owned_company_income_share=1_000.0, + ), + True, + ), + "company_income_without_owner_treatment": ( + dict(owned_company_carries_on_trade=True, owned_company_income_share=1_000.0), + False, + ), + "high_wages": (dict(employment_income=30_000.0), True), +} + + +@PROPERTY_SETTINGS +@given( + families=st.lists( + st.lists( + st.fixed_dictionaries( + dict( + age=st.integers(18, 89), + earnings=st.sampled_from(sorted(EARNINGS)), + # None leaves the minimum income floor to the model. + floor_applies=st.sampled_from([None, None, True, False]), + ) + ), + min_size=1, + max_size=2, + ), + min_size=1, + max_size=6, + ), + year=st.integers(2025, 2030), +) +@example( + # A couple's floor: the partner's earnings meet the couple threshold, so + # the floor doesn't apply to the other member (UC Regs 2013 reg 62(3)). + families=[ + [ + dict(age=24, earnings="none", floor_applies=True), + dict(age=30, earnings="high_wages", floor_applies=False), + ], + [dict(age=24, earnings="none", floor_applies=True)], + ], + year=2026, +) +def test_earned_income_exemption_reads_universal_credit(families, year): + """Invariant 13: the Universal Credit exemption's earned-income test is + Universal Credit's own calculation (HB Regs 2006 reg 74(8)(b), (11); WSI + 2013/3029 Sch 1 para 3(8)(c), (10)).""" + people, benunits, households_, cases = {}, {}, {}, [] + for f, family in enumerate(families): + claimant = f"claimant_{f}" + people[claimant] = dict(age=90, current_education="NOT_IN_EDUCATION") + cases.append(None) + members = [] + for k, member in enumerate(family): + name = f"member_{f}_{k}" + inputs, settled = EARNINGS[member["earnings"]] + people[name] = dict( + age=member["age"], + current_education="NOT_IN_EDUCATION", + weekly_hours=0.0, + **inputs, + ) + if member["floor_applies"] is not None: + people[name]["uc_mif_applies"] = member["floor_applies"] + people[name]["uc_minimum_income_floor"] = 10_000.0 + members.append(name) + cases.append((member, settled)) + no_awards = {benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS} + benunits[f"claimant_unit_{f}"] = dict( + members=[claimant], benunit_is_rent_liable=True, **no_awards + ) + benunits[f"family_{f}"] = dict( + members=members, + benunit_is_rent_liable=False, + **{**no_awards, "universal_credit_pre_benefit_cap": 1_000.0}, + ) + households_[f"household_{f}"] = dict( + members=[claimant, *members], + country="WALES", + local_authority=COUNTRIES["WALES"], + ) sim = Simulation( situation=for_years( dict(people=people, benunits=benunits, households=households_), [year] ) ) - helper = universal_credit_after_benefit_cap(sim.populations["benunit"], year) - np.testing.assert_allclose( - helper, sim.calculate("universal_credit", year), atol=0.01 + earned = np.asarray(sim.calculate("uc_individual_earned_income", year)) > 0 + claimant_ = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) + floor = np.asarray(sim.calculate("uc_mif_applies", year)) + hb_exempt = np.asarray( + sim.calculate("housing_benefit_non_dep_deduction_exempt", year) + ) + ctr_exempt = np.asarray( + sim.calculate("council_tax_reduction_non_dep_deduction_exempt", year) ) + for i, case in enumerate(cases): + if case is None: + continue + member, settled = case + assert claimant_[i] + # The predicate is Universal Credit's earned income: the Welsh + # pensioner scheme at any age, Housing Benefit under 25. + assert bool(ctr_exempt[i]) == (not earned[i]), (i, member) + assert bool(hb_exempt[i]) == (member["age"] < 25 and not earned[i]), (i, member) + # Where the inputs settle it, and the floor doesn't apply. + if settled is not None and not floor[i]: + assert bool(earned[i]) == settled, (i, member) From 23aba8c80b9ad7d0703a5f324aa16b20bbb64939 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:30 -0500 Subject: [PATCH 21/31] Count ISA interest and dividends in non-dependants' gross income Both gross-income variables now add tax_free_savings_income (ISA interest unless a total is supplied) and individual_savings_account_dividend_income, as main's local CTR schemes do. The documentation describes treating a loss as nil as an approximation: the model holds profits, not the receipts DWP guidance A5.551 counts. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_gross_income.yaml | 17 +++++- ...il_tax_reduction_non_dep_gross_income.yaml | 17 +++++- .../housing_benefit_non_dep_gross_income.py | 57 +++++++++++-------- ...ncil_tax_reduction_non_dep_gross_income.py | 57 +++++++++++-------- 4 files changed, 96 insertions(+), 52 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index 4753918418..a7b9638754 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -613,7 +613,8 @@ housing_benefit_non_dep_gross_income: 3_000 - name: housing_benefit_non_dep_gross_income counts a self-employment or property loss as nil - # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # The model has profits, not the receipts DWP HB guidance A5.551 counts + # before business expenses. As an approximation, a loss counts as nil and # does not reduce other income. period: 2025 input: @@ -623,3 +624,17 @@ property_income: -1_000 output: housing_benefit_non_dep_gross_income: 5_000 + +- name: housing_benefit_non_dep_gross_income counts ISA interest and dividends + # Gross income is earnings and any other income (DWP HB guidance A5.542), + # savings interest included (A5.570). The supplied exempt-interest total + # includes the ISA interest, which counts once. + period: 2025 + input: + social_security_income: 0 + employment_income: 9_000 + individual_savings_account_interest_income: 500 + tax_free_savings_income: 20_000 + individual_savings_account_dividend_income: 30_000 + output: + housing_benefit_non_dep_gross_income: 59_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index 5736b27177..ec89611965 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -733,7 +733,8 @@ council_tax_reduction_non_dep_gross_income: 3_000 - name: council_tax_reduction_non_dep_gross_income counts a self-employment or property loss as nil - # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # The model has profits, not the receipts DWP HB guidance A5.551 counts + # before business expenses. As an approximation, a loss counts as nil and # does not reduce other income. period: 2025 input: @@ -743,3 +744,17 @@ property_income: -1_000 output: council_tax_reduction_non_dep_gross_income: 5_000 + +- name: council_tax_reduction_non_dep_gross_income counts ISA interest and dividends + # Gross income is earnings and any other income (DWP HB guidance A5.542), + # savings interest included (A5.570). The supplied exempt-interest total + # includes the ISA interest, which counts once. + period: 2025 + input: + social_security_income: 0 + employment_income: 9_000 + individual_savings_account_interest_income: 500 + tax_free_savings_income: 20_000 + individual_savings_account_dividend_income: 30_000 + output: + council_tax_reduction_non_dep_gross_income: 59_000 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py index e485277a20..a43c9be498 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py @@ -16,31 +16,33 @@ class housing_benefit_non_dep_gross_income(Variable): "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", ) documentation = ( - "Annual gross income represented by the model for non-dependant " + "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "pension contributions. A self-employment or property loss counts as " - "nil, so it does not reduce other income: gross income from " - "self-employment has no deduction for business expenses (DWP HB " - "guidance A5.551), and the model has profit, not turnover. Claimant " - "war-pension and supplementary-pension disregards do not apply to " - "these non-dependant bands. It includes statutory sick, maternity and " - "paternity pay once through employment_benefits, child and spousal " - "maintenance, the tax-purpose social security income sources, and " - "the separately represented non-taxable periodic benefits. " - "Attendance Allowance, DLA and PIP are omitted under the specific " - "disability-payment disregards. The other disregarded disability " - "and compensation/trust payments have no separate model inputs. " - "Benefit-unit awards (including Child Benefit, tax credits and UC) " - "are not allocated to an individual recipient here. " - "BSP and AFCS inputs do not separate periodic income from capital " - "payments, so they are not added. Gross income includes earnings " - "and any other income (DWP HB guidance A5.542), so other investment " - "income counts. This is a person's own income: a claimant or partner " - "of the non-dependant's family is banded on the couple's joint " - "income, with the family's awards added, in the deduction formula, " - "which also applies the remunerative-work test. The gross-income " - "variable itself does not decide whether someone is in remunerative " - "work or whether a deduction is due." + "pension contributions. DWP HB guidance A5.551 counts a " + "self-employed non-dependant's total income with no deduction for " + "business expenses. The model holds self-employment and property " + "profits, not receipts, so this approximates gross income by adding " + "each profit and counting a loss as nil, which stops a loss reducing " + "other income. The claimant war-pension and supplementary-pension " + "disregards do not apply to these bands. The total includes " + "statutory sick, maternity and paternity pay once, through " + "employment_benefits; child and spousal maintenance; the tax-purpose " + "social security income sources; and the non-taxable periodic " + "benefits the model represents separately. It leaves out Attendance " + "Allowance, DLA and PIP under the disability-payment disregards; the " + "other disregarded disability, compensation and trust payments have " + "no model inputs. BSP and AFCS inputs do not separate periodic income " + "from capital payments, so the total leaves them out. Gross income " + "includes earnings and any other income (A5.542), and A5.570 adds " + "savings interest to earnings, so other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends count. This is " + "the person's own income. Benefit-unit awards, such as Child Benefit, " + "tax credits and Universal Credit, stay out of it: the deduction " + "formula adds them for the claimant and partner of the " + "non-dependant's family, whom it bands on their joint income, and " + "applies the remunerative-work test. This variable decides neither " + "whether someone is in remunerative work nor whether a deduction is " + "due." ) def formula(person, period, parameters): @@ -53,8 +55,11 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", + # Exempt interest, ISA interest unless a total is supplied. + "tax_free_savings_income", "other_investment_income", "dividend_income", + "individual_savings_account_dividend_income", "miscellaneous_income_before_trading_allowance", "maternity_allowance", "iidb", @@ -62,7 +67,9 @@ def formula(person, period, parameters): "war_widows_pension", ], ) - # A loss counts as nil (DWP HB guidance A5.551). + # The model has profits, not the receipts DWP HB guidance A5.551 + # counts before business expenses. As an approximation, a loss + # counts as nil, so it never reduces other income. losses_as_nil = max_(0, person("self_employment_income", period)) + max_( 0, person("property_income", period) ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py index e5bc8c7ec5..86f8ef5a3f 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py @@ -20,31 +20,33 @@ class council_tax_reduction_non_dep_gross_income(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", ) documentation = ( - "Annual gross income represented by the model for non-dependant " + "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "pension contributions. A self-employment or property loss counts as " - "nil, so it does not reduce other income: gross income from " - "self-employment has no deduction for business expenses (DWP HB " - "guidance A5.551), and the model has profit, not turnover. Claimant " - "war-pension and supplementary-pension disregards do not apply to " - "these non-dependant bands. It includes statutory sick, maternity and " - "paternity pay once through employment_benefits, child and spousal " - "maintenance, the tax-purpose social security income sources, and " - "the separately represented non-taxable periodic benefits. " - "Attendance Allowance, DLA and PIP are omitted under the specific " - "disability-payment disregards. The other disregarded disability " - "and compensation/trust payments have no separate model inputs. " - "Benefit-unit awards (including Child Benefit, tax credits and UC) " - "are not allocated to an individual recipient here. " - "BSP and AFCS inputs do not separate periodic income from capital " - "payments, so they are not added. Gross income includes earnings " - "and any other income (DWP HB guidance A5.542), so other investment " - "income counts. This is a person's own income: a claimant or partner " - "of the non-dependant's family is banded on the couple's joint " - "income, with the family's awards added, in the deduction formula, " - "which also applies the remunerative-work test. The gross-income " - "variable itself does not decide whether someone is in remunerative " - "work or whether a deduction is due." + "pension contributions. DWP HB guidance A5.551 counts a " + "self-employed non-dependant's total income with no deduction for " + "business expenses. The model holds self-employment and property " + "profits, not receipts, so this approximates gross income by adding " + "each profit and counting a loss as nil, which stops a loss reducing " + "other income. The claimant war-pension and supplementary-pension " + "disregards do not apply to these bands. The total includes " + "statutory sick, maternity and paternity pay once, through " + "employment_benefits; child and spousal maintenance; the tax-purpose " + "social security income sources; and the non-taxable periodic " + "benefits the model represents separately. It leaves out Attendance " + "Allowance, DLA and PIP under the disability-payment disregards; the " + "other disregarded disability, compensation and trust payments have " + "no model inputs. BSP and AFCS inputs do not separate periodic income " + "from capital payments, so the total leaves them out. Gross income " + "includes earnings and any other income (A5.542), and A5.570 adds " + "savings interest to earnings, so other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends count. This is " + "the person's own income. Benefit-unit awards, such as Child Benefit, " + "tax credits and Universal Credit, stay out of it: the deduction " + "formula adds them for the claimant and partner of the " + "non-dependant's family, whom it bands on their joint income, and " + "applies the remunerative-work test. This variable decides neither " + "whether someone is in remunerative work nor whether a deduction is " + "due." ) def formula(person, period, parameters): @@ -57,8 +59,11 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", + # Exempt interest, ISA interest unless a total is supplied. + "tax_free_savings_income", "other_investment_income", "dividend_income", + "individual_savings_account_dividend_income", "miscellaneous_income_before_trading_allowance", "maternity_allowance", "iidb", @@ -66,7 +71,9 @@ def formula(person, period, parameters): "war_widows_pension", ], ) - # A loss counts as nil (DWP HB guidance A5.551). + # The model has profits, not the receipts DWP HB guidance A5.551 + # counts before business expenses. As an approximation, a loss + # counts as nil, so it never reduces other income. losses_as_nil = max_(0, person("self_employment_income", period)) + max_( 0, person("property_income", period) ) From a99ff4e8558141db9a3af87287903dadf6d172e2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:33 -0500 Subject: [PATCH 22/31] Give the English work-gate cases a pension-age applicant Each English case had a sole 30-year-old forced onto the pension-age scheme. It now has a 70-year-old applicant and a separate 30-year-old non-dependant, and the model works out each family's scheme. Expected deductions don't change. Co-Authored-By: Claude Opus 5.5 --- ...x_reduction_non_dep_remunerative_work.yaml | 777 +++++++++++------- 1 file changed, 477 insertions(+), 300 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index 2cd2277ab9..0714fa51e3 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -6,233 +6,330 @@ # 2026/56; Wales 6.70, WSI 2026/13), and 403.85/week of income in work falls # in the band from £279 in England (10.60) and Wales (13.35) and from £286 in # Scotland (10.95). Family awards, which a single non-dependant's gross income -# would count, are held at zero; English cases take the pension-age scheme, -# the only national one in England. +# would count, are held at zero. English cases give the household a +# pension-age applicant, since England's only national scheme is the +# pension-age one, and the model works out each family's scheme. - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 0 - employment_income: 0 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 0 + employment_income: 0 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 0 - council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 0] + council_tax_reduction_non_dep_gross_income: [0, 12000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 780 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 780 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 15 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 15] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 16 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 16] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 1950 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 1950 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 37.5 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 37.5] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 0 - employment_income: 0 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 0 + employment_income: 0 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 0 - council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 0] + council_tax_reduction_non_dep_gross_income: [0, 12000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 780 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 780 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 15 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 15] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 16 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 16] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 1950 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 1950 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 37.5 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 37.5] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 @@ -719,31 +816,35 @@ - name: council_tax_reduction charges no new deduction to a non-dependant on Universal Credit without earnings # SI 2012/2885 Sch 1 para 8(8)(c): no deduction for a non-dependant - # entitled to Universal Credit with no earned income. The family is - # pension-age for CTR, so England's national scheme applies. + # entitled to Universal Credit with no earned income. The applicant is of + # pension age, so England's national scheme applies. period: 2026 absolute_error_margin: 0.01 input: people: + applicant: + age: 70 + social_security_income: 0 non_dependant: age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 0 social_security_income: 0 is_on_income_support: false is_on_income_based_jsa: false is_on_income_related_esa: false benunits: + applicant_unit: + members: [applicant] non_dependant_unit: members: [non_dependant] universal_credit_pre_benefit_cap: 6_000 - council_tax_reduction_pensioner: true households: household: - members: [non_dependant] + members: [applicant, non_dependant] country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: [0] + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (Income Support) # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and @@ -751,26 +852,39 @@ period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: true - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: true + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 0 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-based JSA) # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and @@ -778,26 +892,39 @@ period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: true - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: true + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 0 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-related ESA) # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and @@ -805,105 +932,155 @@ period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: true + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: true + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 0 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (no excluded benefit) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 1040 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 1040 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: SCOTLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 From c280fb058655b8e3a3b9cd47e7f39c8c516d5868 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:34 -0500 Subject: [PATCH 23/31] Pin a tiny rent share and a partner's PIP A family with a 0.5% share of the rent is a joint occupier, not a non-dependant (HB reg 3(2)(d)), and a partner's PIP daily living component exempts the claimant's family (reg 74(6)). Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 38 +++++++++++++++++++ ...it_non_dep_deductions_claimant_exempt.yaml | 18 +++++++++ 2 files changed, 56 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 40edc163e7..c566a310b0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -129,6 +129,44 @@ share_of_household_rent: [0.5, 0.5] housing_benefit_non_dep_deductions: [0, 0] +- name: A family with any share of the rent is a joint occupier, however small + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_1: + is_household_head: true + household_benefits_individual_non_dep_deduction: 1 + person_2: + household_benefits_individual_non_dep_deduction: 2 + person_3: + household_benefits_individual_non_dep_deduction: 3 + benunits: + benunit_1: + members: [person_1] + share_of_household_rent: 0.995 + housing_benefit_non_dep_deductions_claimant_exempt: false + benunit_2: + members: [person_2] + # A nil rent leaves the family not liable for any, though it bears a + # share of the payments. + share_of_household_rent: 0.005 + benunit_is_rent_liable: false + housing_benefit_non_dep_deductions_claimant_exempt: false + benunit_3: + members: [person_3] + share_of_household_rent: 0 + benunit_is_rent_liable: false + housing_benefit_non_dep_deductions_claimant_exempt: false + households: + household: + members: [person_1, person_2, person_3] + output: + # HB Regs 2006 reg 3(2)(d): person 2 shares the payments, so is a joint + # occupier, not a non-dependant. Only person 3's 3 is deducted, split by + # the shares: 3 x 0.995 and 3 x 0.005. + housing_benefit_non_dep_deductions: [2.985, 0.015, 0] + # The review case on #2184: an existing claimant in council housing with an # adult child whose only income is £28,912 of other investment income and who # does no paid work. The bands apply only in remunerative work (HB Regs 2006 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml index f4de59feff..ff959df9c7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml @@ -42,6 +42,24 @@ output: housing_benefit_non_dep_deductions_claimant_exempt: true +- name: Partner on the PIP daily living component + period: 2026 + input: + people: + claimant: + age: 40 + partner: + age: 38 + pip_dl_category: STANDARD + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + - name: Claimant on Armed Forces Independence Payment period: 2026 input: From e4f5062a450d8aac4ce656487e2c70bdb1ce4a73 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:36 -0500 Subject: [PATCH 24/31] Place the non-dependant property tests by their CI durations Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/tests/shard_durations.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/shard_durations.toml b/policyengine_uk/tests/shard_durations.toml index 2c52eb6701..86e9a1fc10 100644 --- a/policyengine_uk/tests/shard_durations.toml +++ b/policyengine_uk/tests/shard_durations.toml @@ -7,7 +7,10 @@ # all five Python shards of nine runs on 24 October 2026, each test's mean # over the runs: merge queue runs 40050881130, 40059344467, 40063244949, # 40067189795, 40071060766, 40079467297, 40083761750 and 40087883891, and -# pull request run 40095846881. A slow test not here is placed by the hash. +# pull request run 40095846881. The entries for +# test_non_dependant_deduction_properties.py come from pull request run +# 41269333878 alone, below 20 seconds included. A slow test not here is +# placed by the hash. # test_ci_test_shards.py fails if an entry names a test that no longer # exists, so a pull request that renames or removes a listed test should # rename or delete its line here. Regenerate when a shard drifts well above @@ -153,6 +156,7 @@ "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_residence_condition_is_metamorphic" = 101 "policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py::test_lha_category_is_the_claimed_schemes_old_category" = 101 "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_shared_carer_attribution_matches_the_reference_matching" = 101 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_non_dependant_deduction_invariants" = 101 "policyengine_uk/tests/test_dfe_childcare_student_support_properties.py::test_maintenance_loan_household_income_applies_the_age_25_test" = 100 "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_legacy_premium_equals_the_addition_without_young_people" = 100 "policyengine_uk/tests/test_pension_annual_allowance_properties.py::test_extra_contributions_never_raise_tax_by_more_than_the_charge" = 100 @@ -306,6 +310,7 @@ "policyengine_uk/tests/test_carer_overlapping_benefits_properties.py::test_carer_benefit_is_monotonic_in_state_pension" = 60 "policyengine_uk/tests/test_winter_heating_payment_properties.py::test_non_dependants_never_change_anyone_elses_payment" = 60 "policyengine_uk/tests/test_uc_single_claim_properties.py::test_single_standard_allowance_at_the_claimants_own_age" = 60 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_capped_universal_credit_matches_the_model" = 60 "policyengine_uk/tests/test_trading_loss_properties.py::test_zero_profit_trade_loss_cannot_raise_tax_or_reduce_programme_award[universal_credit-2026]" = 59 "policyengine_uk/tests/test_reform_inputs_survive_reset.py::test_a_reform_names_exactly_the_variables_it_leaves_inputs_for" = 59 "policyengine_uk/tests/test_uc_minimum_income_floor_properties.py::test_top_up_never_takes_couple_above_couple_threshold" = 59 @@ -579,3 +584,7 @@ "policyengine_uk/tests/test_disable_simulated_benefits_winter_heating.py::test_a_household_that_moves_is_paid_under_each_years_scheme[LONDON-SCOTLAND]" = 20 "policyengine_uk/tests/test_capital_gains_responses.py::test_revenue_falls_short_of_the_static_estimate" = 20 "policyengine_uk/tests/test_capital_gains_response_default.py::test_employer_cost_branches_hold_the_realisation_response" = 20 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[ENGLAND]" = 14 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[WALES]" = 11 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[SCOTLAND]" = 10 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[HOUSING_BENEFIT]" = 10 From fa93bd179e48d24f61d7b0e61fa5c08689d7665c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:38 -0500 Subject: [PATCH 25/31] Name who deducts in the notebook; label the exemptions Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/housing-benefit.ipynb | 2 +- .../housing_benefit_non_dep_deduction_exempt.py | 2 +- .../housing_benefit_non_dep_deductions_claimant_exempt.py | 2 +- .../council_tax_reduction_non_dep_deduction_exempt.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 59ae57eb68..64f220a8ba 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -42,7 +42,7 @@ "\n", "1. **Eligible rent**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate for the Housing Benefit category of dwelling (`housing_benefit_LHA_category`: the shared accommodation rate for a young individual with no non-dependant, or a claimant entitled to one bedroom who lacks exclusive use of self-contained accommodation, unless the severe disability premium applies, reg 13D(2)) or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\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 appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). Under reg 74 (SI 2006/214 reg 55), a non-dependant aged 18 or over in remunerative work (16 hours a week or more) pays the deduction for the band containing their normal gross weekly income, counting a couple's joint income and including each band's lower edge; one not in remunerative work pays the lowest amount. A couple pays one deduction, the higher. No deduction is made for full-time students, State Pension Credit recipients, or under-25s on Income Support, income-based JSA or Universal Credit without earned income, nor for any non-dependant if the claimant or partner is blind or receives Attendance Allowance, the DLA care component, the PIP daily living component or Armed Forces Independence Payment. The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", + "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). Under reg 74 (SI 2006/214 reg 55), for a non-dependant aged 18 or over in remunerative work (16 hours a week or more) the council deducts the amount for the band containing their normal gross weekly income, counting a couple's joint income and including each band's lower edge; for one not in remunerative work it deducts the lowest amount. It makes one deduction for a couple, the higher. Councils make no deduction for full-time students, State Pension Credit recipients, or under-25s on Income Support, income-based JSA or Universal Credit without earned income, and none for any non-dependant if the claimant or partner is blind or receives Attendance Allowance, the DLA care component, the PIP daily living component or Armed Forces Independence Payment. The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", " If the claimant or partner is on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`housing_benefit_on_passporting_benefit`), the whole of the family's earnings, other income and capital is disregarded ([SI 2006/213 Sch. 4 para. 12](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12), [Sch. 5 para. 4](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4) and [Sch. 6 para. 5](https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5); Northern Ireland: [SR 2006/405 Sch. 5 para. 12](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12), [Sch. 6 para. 4](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4) and [Sch. 7 para. 5](https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5)). `housing_benefit_applicable_income` and `housing_benefit_assessable_capital` are then zero, so the award is that maximum. This applies at any age: a mixed-age couple whose younger member is on one of these benefits is assessed under the working-age regulations ([SI 2006/213 reg. 5(1)(b)](https://www.legislation.gov.uk/uksi/2006/213/regulation/5)). Income Support, income-based Jobseeker's Allowance and income-related Employment and Support Allowance count when a positive amount is paid on the claimant's or partner's award (`in_receipt_of_income_support_jsa_ib_or_esa_ir`, which reads `claimant_or_partner_jsa_income` and `claimant_or_partner_esa_income`). Another member's own award, such as a non-dependant's, is payable to them and does not passport the claimant. A person is on Universal Credit on any day they are entitled to it, whether it is in payment or not ([reg. 2(3B)](https://www.legislation.gov.uk/uksi/2006/213/regulation/2)); PolicyEngine reads that as Universal Credit above nil before the benefit cap and deductions (`is_uc_entitled`), for a family that claims it, so the passport adds no circular dependency through the benefit cap. The universal credit limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). It matters only where Housing Benefit is paid alongside Universal Credit, for specified or temporary accommodation, which the eligibility rules above do not yet model.\n", "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `housing_benefit_benefit_cap_reduction`, the excess of the family's welfare benefits over the Housing Benefit cap (`housing_benefit_benefit_cap`, SI 2006/213 reg 75CA), leaving at least 50 pence a week (reg 75D). The cap does not apply where the claimant or couple is entitled to working tax credit (reg 75E), the claimant or partner receives a benefit listed in reg 75F, or the claim is under the pension-age regulations (reg 5). Universal Credit has its own cap.\n", "\n", diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 3ef4cd0518..9ed6cf1730 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -10,7 +10,7 @@ class housing_benefit_non_dep_deduction_exempt(Variable): value_type = bool entity = Person - label = "Housing Benefit makes no non-dependant deduction for this person" + label = "Exempt from Housing Benefit non-dependant deductions" documentation = ( "Modelled exemptions: a full-time student (SI 2006/213 reg 74(7)(c)-(e); " "SI 2006/214 reg 55(7)(e)); a non-dependant on State Pension Credit " diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 53535fe798..5b612d8181 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -4,7 +4,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): value_type = bool entity = BenUnit - label = "Housing Benefit claimant exempt from all non-dependant deductions" + label = "Exempt from all Housing Benefit non-dependant deductions" documentation = ( "No deduction is made for any non-dependant if the claimant or partner " "is blind or receives Attendance Allowance, the DLA care component, the " diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 9713b51913..f1914638b8 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -8,7 +8,7 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): value_type = bool entity = Person - label = "national Council Tax Reduction makes no non-dependant deduction for this person" + label = "Exempt from national Council Tax Reduction non-dependant deductions" documentation = ( "Modelled exemptions in the England pensioner, Scottish and Welsh " "schemes: a full-time student; a non-dependant on Income Support, " From 3252a32ef5b019497bc2bc2a5120be410066937c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:27:07 -0500 Subject: [PATCH 26/31] Restate the changelog fragment without a leading bullet Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index bd8311447b..34d291f0da 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated. Bands include their lower edge. A non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, pays the lowest amount whatever their income. A couple pays one deduction, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and any other adult in the family pays their own. Gross income counts other investment income and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. +The model now makes Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. It makes one deduction for a couple, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and a separate one for any other adult in the family. Gross income counts other investment income, ISA interest and ISA dividends, and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply, and the Universal Credit exemption reads Universal Credit's own calculation of earned income. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. From 65469540da8c53e5bc412d2c27e619d454940e68 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:29:14 -0500 Subject: [PATCH 27/31] Name who exempts ESA(IR) recipients in the CTR exemption documentation Co-Authored-By: Claude Opus 5.5 --- .../council_tax_reduction_non_dep_deduction_exempt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index f1914638b8..5c965738bb 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -17,10 +17,10 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "partner is on Income Support, income-based JSA or income-related ESA " "and who is not on Universal Credit; an adult for whom someone else is " "entitled to child benefit (LGFA 1992 Sch 1 para 3); and, from the year " - "each scheme added it, " - "one entitled to Universal Credit calculated on no earned income. Wales excludes " - "members of the ESA work-related activity group; with no ESA group " - "input, income-related ESA is treated as exempt. Not modelled: other " + "each scheme added it, one entitled to Universal Credit calculated on " + "no earned income. Welsh councils deduct for members of the ESA " + "work-related activity group; with no ESA group input, the model " + "exempts every income-related ESA recipient. Not modelled: other " "persons disregarded for council tax discounts (LGFA 1992 Sch 1), " "youth training allowances, a normal home elsewhere, and " "absence in hospital or on armed forces operations." From 77b5bc6640c73dc97ef71126d8d2f0073ad39e42 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:59:51 -0500 Subject: [PATCH 28/31] Set UC claimant flags for everyone or no one in the property populations An is_uc_claimant input for some people stores False for the rest, so the generated populations now enter it for every person or leave the model's default. The earned-income invariant compares the exemption with Universal Credit's earned income for the claimants the award assesses, and the capped-award invariant sets the protected floor in place on one simulation and checks that the floor never lowers an award. Co-Authored-By: Claude Opus 5.5 --- ...test_non_dependant_deduction_properties.py | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 1d43da7e1f..0d4e067f16 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -226,19 +226,21 @@ def households(draw): # Which adult non-dependant reports IS, JSA(IB), ESA(IR) and SPC, if # anyone does. reporter=draw(st.sampled_from([None, 0, 1])), - # Explicit is_uc_claimant flags for the non-dependants, by position, - # or None to leave the claimant and partner as the claimants. A flag - # on the young person alone makes them a single claimant beside the - # couple. - uc_claimants=draw( - st.one_of(st.none(), st.lists(st.booleans(), min_size=3, max_size=3)) - ), + # is_uc_claimant for the non-dependants, by position, where the test + # enters it (explicit_uc_claimants). A flag on the young person alone + # makes them a single claimant beside the couple. + uc_claimants=draw(st.lists(st.booleans(), min_size=3, max_size=3)), claims_child_benefit=draw(st.sampled_from([True, True, False])), non_dependants=non_dependants, ) -def build(population, year, raise_income=False): +def build(population, year, raise_income=False, explicit_uc_claimants=False): + """The generated population as a simulation. With explicit_uc_claimants, + every person's is_uc_claimant is an input: True for each claimant and the + generated flag for each non-dependant. (An input for some people stores + the default, False, for the rest.) Otherwise the model's default applies: + the claimant and partner of each family.""" people, benunits, households_, persons = {}, {}, {}, [] for h, household in enumerate(population): claimant = f"claimant_{h}" @@ -258,6 +260,8 @@ def build(population, year, raise_income=False): if name != "is_blind" }, ) + if explicit_uc_claimants: + people[claimant]["is_uc_claimant"] = True persons.append(dict(household=h, benunit=2 * h, non_dependant=None)) members = [] for n, nd in enumerate(household["non_dependants"]): @@ -291,7 +295,7 @@ def build(population, year, raise_income=False): if benefit != "is_blind" }, ) - if household.get("uc_claimants") is not None: + if explicit_uc_claimants: people[name]["is_uc_claimant"] = household["uc_claimants"][n] members.append(name) persons.append( @@ -381,6 +385,7 @@ def aggregate(individual, persons, couple, benunit_count, each_member): @given( population=st.lists(households(), min_size=1, max_size=12), year=st.integers(2025, 2030), + explicit_uc_claimants=st.booleans(), ) @example( # Rare under random draws: a Welsh working-age applicant's non-dependant @@ -441,9 +446,10 @@ def aggregate(individual, persons, couple, benunit_count, each_member): ], ], year=2026, + explicit_uc_claimants=False, ) -def test_non_dependant_deduction_invariants(population, year): - sim, persons = build(population, year) +def test_non_dependant_deduction_invariants(population, year, explicit_uc_claimants): + sim, persons = build(population, year, explicit_uc_claimants=explicit_uc_claimants) r = calculate(sim, year) p = SYSTEM.parameters(str(year)) hb = p.gov.dwp.housing_benefit.non_dep_deduction @@ -469,8 +475,10 @@ def test_non_dependant_deduction_invariants(population, year): flags = household.get("uc_claimants") def flagged(j): - if flags is None or persons[j]["non_dependant"] is None: + if not explicit_uc_claimants: return bool(couple[j]) + if persons[j]["non_dependant"] is None: + return True return flags[persons[j]["position"]] def age(j): @@ -743,7 +751,12 @@ def band_amount(scale, weekly): ) # 6. Raising non-dependants' incomes never lowers a deduction. - raised, _ = build(population, year, raise_income=True) + raised, _ = build( + population, + year, + raise_income=True, + explicit_uc_claimants=explicit_uc_claimants, + ) s = calculate(raised, year) for name in [ "household_benefits_individual_non_dep_deduction", @@ -943,35 +956,35 @@ def capped_universal_credit(sim, year): ) +PROTECTED_FLOOR = "gov.dwp.universal_credit.deductions.protected_floor" + + @PROPERTY_SETTINGS @given( families=st.lists(CAPPED_FAMILY, min_size=1, max_size=8), year=st.integers(2025, 2030), - protected_floor=st.sampled_from([0.0, 0.85]), ) -@example(families=[ONE_CAPPED_FAMILY], year=2025, protected_floor=0.85) -def test_capped_universal_credit_matches_the_model(families, year, protected_floor): +@example(families=[ONE_CAPPED_FAMILY], year=2025) +def test_capped_universal_credit_matches_the_model(families, year): """Invariant 12: the after-cap Universal Credit counted in a non-dependant family's gross income equals universal_credit for a family - with no Housing Benefit, Maternity Allowance or deductions, with or - without a protected minimum floor.""" - reform = ( - { - "gov.dwp.universal_credit.deductions.protected_floor": { - f"{year}-01-01.{year}-12-31": protected_floor - } - } - if protected_floor - else None - ) - sim = Simulation( - situation=capped_universal_credit_families(families, year), reform=reform - ) + with no Housing Benefit, Maternity Allowance or deductions, under current + law and under an 85% protected minimum floor. The floor is set in place + on the same simulation, which costs far less than a reformed one.""" + sim = Simulation(situation=capped_universal_credit_families(families, year)) + current_law = np.asarray(sim.calculate("universal_credit", year)) np.testing.assert_allclose( - capped_universal_credit(sim, year), - sim.calculate("universal_credit", year), - atol=0.01, + capped_universal_credit(sim, year), current_law, atol=0.01 ) + sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( + period=str(year), value=0.85 + ) + sim.tax_benefit_system.reset_parameter_caches() + sim.reset_calculations() + floored = np.asarray(sim.calculate("universal_credit", year)) + np.testing.assert_allclose(capped_universal_credit(sim, year), floored, atol=0.01) + # The floor only limits the reduction, so no award falls. + assert np.all(floored >= current_law - 0.01) def test_capped_universal_credit_reads_stored_and_replaced_values(): @@ -1141,7 +1154,7 @@ def test_earned_income_exemption_reads_universal_credit(families, year): ) ) earned = np.asarray(sim.calculate("uc_individual_earned_income", year)) > 0 - claimant_ = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) + assessed = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) floor = np.asarray(sim.calculate("uc_mif_applies", year)) hb_exempt = np.asarray( sim.calculate("housing_benefit_non_dep_deduction_exempt", year) @@ -1153,11 +1166,17 @@ def test_earned_income_exemption_reads_universal_credit(families, year): if case is None: continue member, settled = case - assert claimant_[i] - # The predicate is Universal Credit's earned income: the Welsh - # pensioner scheme at any age, Housing Benefit under 25. - assert bool(ctr_exempt[i]) == (not earned[i]), (i, member) - assert bool(hb_exempt[i]) == (member["age"] < 25 and not earned[i]), (i, member) + # The predicate is Universal Credit's earned income, for a claimant + # the award assesses (the model pairs claimant and partner, and may + # leave a second adult outside the couple): the Welsh pensioner + # scheme at any age, Housing Benefit under 25. + on_universal_credit = bool(assessed[i]) + uc_without_earnings = on_universal_credit and not earned[i] + assert bool(ctr_exempt[i]) == uc_without_earnings, (i, member) + assert bool(hb_exempt[i]) == (member["age"] < 25 and uc_without_earnings), ( + i, + member, + ) # Where the inputs settle it, and the floor doesn't apply. if settled is not None and not floor[i]: assert bool(earned[i]) == settled, (i, member) From 0ef6f871b9a0aef3e5f7683d0d862d355fd70514 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 03:13:13 -0500 Subject: [PATCH 29/31] Name who acts in the new documentation and test comments Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 6 +-- .../non_dependant_deduction_review_cases.yaml | 38 +++++++++---------- ...x_reduction_non_dep_remunerative_work.yaml | 4 +- ...test_non_dependant_deduction_properties.py | 4 +- .../non_dep_deduction/_non_dependants.py | 24 ++++++------ .../housing_benefit_non_dep_gross_income.py | 2 +- .../benefit_cap/uc_benefit_cap_reduction.py | 4 +- .../uc_benefit_cap_reduction_after_floor.py | 4 +- ...ouncil_tax_reduction_non_dep_deductions.py | 10 ++--- ...ncil_tax_reduction_non_dep_gross_income.py | 2 +- 10 files changed, 50 insertions(+), 48 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index c566a310b0..8b11ac331d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -162,9 +162,9 @@ household: members: [person_1, person_2, person_3] output: - # HB Regs 2006 reg 3(2)(d): person 2 shares the payments, so is a joint - # occupier, not a non-dependant. Only person 3's 3 is deducted, split by - # the shares: 3 x 0.995 and 3 x 0.005. + # HB Regs 2006 reg 3(2)(d): person 2 shares the payments, which makes + # them a joint occupier, not a non-dependant. The council deducts only + # person 3's 3, split by the shares: 3 x 0.995 and 3 x 0.005. housing_benefit_non_dep_deductions: [2.985, 0.015, 0] # The review case on #2184: an existing claimant in council housing with an diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 49605e630c..c11dc6b3b0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1312,8 +1312,8 @@ local_authority: CARDIFF output: # UC Regs 2013 reg 57(2): the trades' profits and losses are added - # together before any nil floor, so the loss cancels the profit. The - # minimum income floor is held off, so only the loss rule decides. + # together before any nil floor, so the loss cancels the profit. The case + # holds the minimum income floor off, so only the loss rule decides. housing_benefit_non_dep_deduction_exempt: [false, true] council_tax_reduction_non_dep_deduction_exempt: [false, true] housing_benefit_non_dep_deductions: [0, 0] @@ -1356,10 +1356,10 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 77(3)(b): the company's income is treated as the - # owner's self-employed earnings, so the exemption doesn't apply. Outside - # remunerative work the non-dependant pays the lowest amounts: HB 20.40 - # and the Welsh pensioner scale's 6.70 a week. + # UC Regs 2013 reg 77(3)(b) treats the company's income as the owner's + # self-employed earnings, so the exemption doesn't apply. Outside + # remunerative work the councils deduct the lowest amounts: HB 20.40 and + # the Welsh pensioner scale's 6.70 a week. housing_benefit_non_dep_deduction_exempt: [false, false] council_tax_reduction_non_dep_deduction_exempt: [false, false] housing_benefit_non_dep_deductions: [1_060.80, 0] @@ -1409,13 +1409,13 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 62(3): a member of a couple is treated as having the - # floor only while the couple's earned income is below the couple + # UC Regs 2013 reg 62(3) treats a member of a couple as having the floor + # only while the couple's earned income falls short of the couple # threshold. The partner's earned income exceeds it, so the non-dependant - # has none and is exempt. The Welsh working-age scheme deducts for each - # member of a Universal Credit couple (Sch 6 para 5(3)): the exempt - # non-dependant's nil and the partner's 6.70 a week, as the partner works - # under 16 hours. + # has none, and the exemption applies. The Welsh working-age scheme + # deducts for each member of a Universal Credit couple (Sch 6 para 5(3)): + # nil for the exempt non-dependant and 6.70 a week for the partner, who + # works under 16 hours. housing_benefit_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_individual_non_dep_deduction: [0, 0, 348.40] @@ -1487,9 +1487,9 @@ output: # WSI 2013/3029 Sch 6 para 5(3): only the couple's own Universal Credit # award takes it out of the one-deduction rule. Here the third adult - # claims alone, so the couple, banded on 15,600 a year (300 a week), pays - # one 13.35 deduction, and the third adult, on Universal Credit with no - # earned income, pays none (para 5(8)(c)). + # claims alone, so the council bands the couple on 15,600 a year (300 a + # week) and makes one 13.35 deduction. It deducts nothing for the third + # adult, on Universal Credit with no earned income (para 5(8)(c)). council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 694.20, 0] council_tax_reduction_non_dep_deductions: [694.20, 0] @@ -1575,8 +1575,8 @@ output: # The pensioner head and the working-age sharer each apply under their # own Welsh scheme and share the deductions equally (Sch 1 para 3(5); Sch - # 6 para 5(5)). The couple, banded on 15,600 + 520 = 16,120 a year (310 - # a week), pays one deduction under either scheme, as the Universal - # Credit is the third adult's; the third adult, banded on 9,100 + 3,000 - # (232.69 a week), pays their own. + # 6 para 5(5)). The council bands the couple on 15,600 + 520 = 16,120 a + # year (310 a week) and makes one deduction under either scheme, as the + # Universal Credit is the third adult's. It bands the third adult on + # 9,100 + 3,000 (232.69 a week) and deducts for them separately. council_tax_reduction_non_dep_deductions: [521.30, 521.30, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index 0714fa51e3..d3bbe37bea 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -7,8 +7,8 @@ # in the band from £279 in England (10.60) and Wales (13.35) and from £286 in # Scotland (10.95). Family awards, which a single non-dependant's gross income # would count, are held at zero. English cases give the household a -# pension-age applicant, since England's only national scheme is the -# pension-age one, and the model works out each family's scheme. +# pension-age applicant, since England's national scheme covers pension-age +# applicants only, and the model works out each family's scheme. - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 0d4e067f16..a19429282f 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -969,8 +969,8 @@ def test_capped_universal_credit_matches_the_model(families, year): """Invariant 12: the after-cap Universal Credit counted in a non-dependant family's gross income equals universal_credit for a family with no Housing Benefit, Maternity Allowance or deductions, under current - law and under an 85% protected minimum floor. The floor is set in place - on the same simulation, which costs far less than a reformed one.""" + law and under an 85% protected minimum floor. The test sets the floor in + place on the same simulation, which costs far less than a reformed one.""" sim = Simulation(situation=capped_universal_credit_families(families, year)) current_law = np.asarray(sim.calculate("universal_credit", year)) np.testing.assert_allclose( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 827c86f9a4..6ab90efa2c 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -87,9 +87,10 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco def reads_simulation_value(benunit, period, variable_name, default_formula=None): - """Whether to read the variable rather than recalculate it here: a value - stored as an input for the period, a neutralized variable, or, where - default_formula is given, a formula a reform has replaced.""" + """Whether to read the variable instead of recalculating it here: the + simulation stores an input for the period, a reform neutralized the + variable or, where the caller passes default_formula, replaced its + formula.""" variable = benunit.simulation.tax_benefit_system.get_variable(variable_name) if variable.is_neutralized: return True @@ -102,7 +103,7 @@ def reads_simulation_value(benunit, period, variable_name, default_formula=None) def universal_credit_after_benefit_cap(benunit, period, parameters): """A family's Universal Credit award after the benefit cap (UC Regs 2013 - reg 81), for a family that is not liable for rent. universal_credit and + reg 81), for a family with no liability for rent. universal_credit and uc_benefit_cap_reduction depend on Housing Benefit through the welfare benefits they count, so this applies the cap to the same welfare benefits other than Housing Benefit, which such a family does not receive (UC reg @@ -110,13 +111,14 @@ def universal_credit_after_benefit_cap(benunit, period, parameters): (benefit_cap_couple; reg 79(1)). It shares the award's own calculation of the reduction (benefit_cap_reduction_of) and of the part a protected minimum floor lets it take (benefit_cap_reduction_applied). Deductions - paid from the award to third parties or for debts are still the family's - income, so they are not taken off, and the floor is applied as for a - family with none: the deduction rate depends on the cap reduction, and so - on Housing Benefit. A universal_credit stored as an input or given a new - formula by a reform is read as it is, and so is a stored or neutralized - uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or - benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" + DWP pays from the award to third parties or for debts stay the family's + income, so this keeps them in and applies the floor as for a family with + none: the deduction rate depends on the cap reduction, and so on Housing + Benefit. Where the simulation stores universal_credit as an input, or a + reform replaces its formula, this reads it as it stands. It also reads a + stored or neutralized uc_benefit_cap_reduction_after_floor, + uc_benefit_cap_reduction or benefit_cap_welfare_benefits, the last with + Housing Benefit at nil.""" if reads_simulation_value( benunit, period, "universal_credit", universal_credit_variable.formula ): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py index a43c9be498..e591cca6ab 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py @@ -55,7 +55,7 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest, ISA interest unless a total is supplied. + # Exempt interest: ISA interest unless the inputs supply a total. "tax_free_savings_income", "other_investment_income", "dividend_income", diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index adb72a2912..36defbb6a3 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -4,8 +4,8 @@ def benefit_cap_reduction_of(benunit, period, welfare_benefits): """The regulation 81 reduction for welfare benefits that already count Housing Benefit as nil (reg. 80(2A)): the excess over the Universal Credit - cap, minus the childcare costs element, and nothing where that element is - greater than the excess or the family has no award (reg. 78(1)). The + cap, minus the childcare costs element, and nothing where that element + exceeds the excess or the family has no award (reg. 78(1)). The non-dependant deductions call this too, with their own total of welfare benefits (housing_benefit/non_dep_deduction/_non_dependants.py).""" excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py index dbe7c0b409..615ff5c537 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py @@ -7,8 +7,8 @@ def benefit_cap_reduction_applied( benunit, period, parameters, cap_reduction, deduction_demand ): - """The part of a regulation 81 reduction taken off the award, given the - deductions demanded from it, under any protected minimum floor. The + """The part of a regulation 81 reduction that comes off the award, given + the deductions it must also meet, under any protected minimum floor. The non-dependant deductions call this too (housing_benefit/non_dep_deduction/_non_dependants.py).""" p = parameters(period).gov.dwp.universal_credit.deductions diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index 99297c3b37..7eeaaab4fa 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -38,11 +38,11 @@ class council_tax_reduction_non_dep_deductions(Variable): def formula(benunit, period, parameters): person = benunit.members country = benunit.household("country", period) - # WSI 2013/3029 Sch 6 para 5(3): a working-age applicant's deduction is - # for each member of a non-dependant couple with a Universal Credit - # award. The award is the couple's own: one that assesses the - # claimant or partner. Another member's award, as a single claimant, - # leaves the couple with one deduction. + # WSI 2013/3029 Sch 6 para 5(3): from a working-age applicant's claim + # the council deducts for each member of a non-dependant couple with a + # Universal Credit award. The award must be the couple's own, one that + # assesses the claimant or partner. Another member's award as a single + # claimant leaves the couple one deduction. couple_claims_universal_credit = benunit.any( person("is_uc_assessed_claimant", period) & person("is_claimant_or_partner", period) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py index 86f8ef5a3f..fe3e109e02 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py @@ -59,7 +59,7 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest, ISA interest unless a total is supplied. + # Exempt interest: ISA interest unless the inputs supply a total. "tax_free_savings_income", "other_investment_income", "dividend_income", From 8371842e18d2d462bfb258ed77b33926df5ffc80 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 03:50:11 -0500 Subject: [PATCH 30/31] Calculate the capped award locally when a replaced award cycles A reform that scales main's universal_credit formula still reads the cap reduction, and so Housing Benefit and the non-dependant deduction. The deduction now catches that cycle and calculates the award without Housing Benefit, as for current law, and a test pins the result. Co-Authored-By: Claude Opus 5.5 --- ...test_non_dependant_deduction_properties.py | 64 +++++++++++++++++++ .../non_dep_deduction/_non_dependants.py | 18 ++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index a19429282f..96d2ad93bd 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -1035,6 +1035,70 @@ def apply(self): ) assert capped_universal_credit(sim, year) == pytest.approx([12_345], abs=0.01) + # A replacement that scales the current-law award still reads the cap + # reduction, and so Housing Benefit and this deduction. The gross income + # then uses the award calculated without Housing Benefit, and nothing + # cycles: the non-dependant's deduction matches current law's. + scaled_award = type( + "universal_credit", + (current_law,), + dict( + formula=lambda benunit, period, parameters: ( + 0.9 * current_law.formula(benunit, period, parameters) + ) + ), + ) + + class ScaledAward(Reform): + def apply(self): + self.update_variable(scaled_award) + + situation = for_years( + dict( + people=dict( + claimant=dict(age=70), + non_dependant=dict( + age=24, + current_education="NOT_IN_EDUCATION", + employment_income=5_000.0, + weekly_hours=20.0, + ), + ), + benunits=dict( + claimant_unit=dict(members=["claimant"], benunit_is_rent_liable=True), + non_dependant_unit=dict( + members=["non_dependant"], + benunit_is_rent_liable=False, + would_claim_uc=True, + ), + ), + households=dict( + household=dict( + members=["claimant", "non_dependant"], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=6_000.0, + tenure_type="RENT_PRIVATELY", + ) + ), + ), + [year], + ) + current = Simulation(situation=situation) + scaled = CoreSimulation( + tax_benefit_system=ScaledAward(SYSTEM), + situation=situation, + default_input_period=year, + default_calculation_period=year, + ) + assert scaled.calculate("universal_credit", year) == pytest.approx( + 0.9 * current.calculate("universal_credit", year), abs=0.01 + ) + for name in ["household_benefits_individual_non_dep_deduction", "housing_benefit"]: + assert scaled.calculate(name, year) == pytest.approx( + current.calculate(name, year), abs=0.01 + ), name + # Earned-income cases for invariant 13. Each sets the person's earnings # inputs; "settled" is what the inputs alone say, where they say it without diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 6ab90efa2c..284b9739c0 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -1,6 +1,8 @@ """Shared definitions for Housing Benefit and Council Tax Reduction non-dependant deductions.""" +from policyengine_core.errors import CycleError + from policyengine_uk.model_api import * from policyengine_uk.utils.benefit_cap import benefit_cap_couple from policyengine_uk.utils.benefit_unit import add_for_members, award_of_couple @@ -115,14 +117,20 @@ def universal_credit_after_benefit_cap(benunit, period, parameters): income, so this keeps them in and applies the floor as for a family with none: the deduction rate depends on the cap reduction, and so on Housing Benefit. Where the simulation stores universal_credit as an input, or a - reform replaces its formula, this reads it as it stands. It also reads a - stored or neutralized uc_benefit_cap_reduction_after_floor, - uc_benefit_cap_reduction or benefit_cap_welfare_benefits, the last with - Housing Benefit at nil.""" + reform replaces its formula, this reads it as it stands, unless the new + formula still reads the cap reduction and so this deduction; then it + calculates the award here. It also reads a stored or neutralized + uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or + benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" if reads_simulation_value( benunit, period, "universal_credit", universal_credit_variable.formula ): - return benunit("universal_credit", period) + try: + return benunit("universal_credit", period) + except CycleError: + # Core unwinds the calculation; the award below doesn't read + # Housing Benefit. + pass if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction_after_floor"): applied = benunit("uc_benefit_cap_reduction_after_floor", period) else: From 97f792542ad0ab21c6016414b7a53378ab197ef4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 03:59:30 -0500 Subject: [PATCH 31/31] Name who acts in the property tests' invariant list Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_deduction_properties.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 96d2ad93bd..a3175307e3 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -53,7 +53,7 @@ reporter or the benefit unit's head, which the test checks. Universal Credit counts for the claimants it assesses: the members the generated is_uc_claimant flags name (by default the claimant and partner), the two - eldest where more are flagged, which the test checks. Earned income is + eldest where the inputs flag more, which the test checks. Earned income is Universal Credit's own calculation (invariant 13). 10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) 11. Banding: a non-dependant in remunerative work who is neither exempt nor @@ -63,10 +63,10 @@ pay, self-employed profit with a loss as nil, and other investment income; for the claimant or partner of the non-dependant's family, the couple's sum plus the family's IS, JSA(IB), ESA(IR) and Pension Credit, and its - Universal Credit where the award assesses either of them. Another member - who claims Universal Credit alone is banded on their own income and that - award. Incomes within a penny a week of an edge are skipped, since the - inputs are stored as float32. + Universal Credit where the award assesses either of them. The council + bands another member who claims Universal Credit alone on their own + income and that award. The test skips incomes within a penny a week of an + edge, since the inputs hold float32 values. 12. Differential: the after-cap Universal Credit used in a non-dependant family's gross income equals the model's universal_credit for a family with no Housing Benefit, no Maternity Allowance and no deductions from