From a4c2a06082caaa206147857a6a46c536e56b4cad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:09:03 -0400 Subject: [PATCH 01/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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/43] 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 From 23de760a627c9989d196244597ba09b4a9a18fc3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 06:46:06 -0500 Subject: [PATCH 32/43] Read universal_credit in the deductions; give UC's cap a basket without HB Universal Credit's cap reduction read benefit_cap_welfare_benefits minus Housing Benefit, so universal_credit depended on Housing Benefit and the non-dependant deductions rebuilt the award with a caught CycleError. uc_benefit_cap_welfare_benefits now holds the Universal Credit cap's benefits with Housing Benefit at nil (UC Regs 2013 reg 80(2A)), from the same lists as benefit_cap_welfare_benefits. For a supplied combined total it subtracts housing_benefit_pre_benefit_cap, as uc_benefit_cap_reduction did. The Housing Benefit cap keeps the combined total. The deductions read universal_credit plus the debt deductions taken from a positive award (WRA 2012 s.8(1); SI 2013/380 Sch 6 para 2(1); SI 2013/383 reg 10; SI 2013/384 regs 10-11). The catch, the reconstruction, reads_simulation_value and the shared cap functions go; the UC cap files return to main's code apart from the basket. With no non-dependant in any household, housing_benefit_non_dep_deductions returns nil before reading income, so a supplied total still works for a single family. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 44 ++- .../uc_benefit_cap_welfare_benefits.yaml | 61 ++++ ...test_non_dependant_deduction_properties.py | 311 ++++++++++++------ policyengine_uk/utils/supplied_inputs.py | 3 - .../non_dep_deduction/_non_dependants.py | 131 +------- .../housing_benefit_non_dep_deductions.py | 7 + .../benefit_cap/uc_benefit_cap_reduction.py | 27 +- .../uc_benefit_cap_reduction_after_floor.py | 28 +- .../uc_benefit_cap_welfare_benefits.py | 75 +++++ 9 files changed, 402 insertions(+), 285 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py 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 c11dc6b3b0..c9cc019a0d 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 @@ -1,6 +1,6 @@ -# 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. +# End-to-end cases for the non-dependant deductions of Housing Benefit (HB Regs +# 2006 reg 74) and the England pensioner Council Tax Reduction scheme (SI +# 2012/2885 Sch 1 para 8), at 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. @@ -166,8 +166,6 @@ 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 @@ -426,8 +424,8 @@ 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. +# 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 @@ -569,9 +567,8 @@ council_tax_reduction_applicant_has_non_dep_exemption: [true, false] 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. +# 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 @@ -1008,9 +1005,7 @@ 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 +- name: A non-dependant family's Universal Credit counts after the benefit cap period: 2026 absolute_error_margin: 0.01 input: @@ -1068,13 +1063,17 @@ 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] + # The Universal Credit cap counts 14,000 UC and 5,129.80 child benefit, + # 19,129.80, under the 22,020 cap, so UC stays 14,000 (UC reg 81). Joint + # gross income is 9,200 wages + 6,000 Maternity Allowance + 14,000 UC + + # 5,129.80 child benefit = 660.19 a week: the top bands, 131.45 HB and + # 15.95 CTR. Welfare Reform Act 2012 s.96(10)(i) makes Maternity + # Allowance a welfare benefit, which the model's cap leaves out (#2784). + # Counting it gives 25,129.80, 3,109.80 over the cap, so UC of 10,890.20, + # 600.38 a week and the 485 bands: 6,232.20 HB and 691.60 CTR. + universal_credit: [0, 14_000] + housing_benefit_non_dep_deductions: [6_835.40, 0] + council_tax_reduction_non_dep_deductions: [829.40, 0] - name: A self-employment loss does not cancel a non-dependant's gross wages period: 2026 @@ -1173,8 +1172,6 @@ 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 @@ -1227,8 +1224,7 @@ 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). +# The Universal Credit exemption reads uc_individual_earned_income. - name: Employment expenses that cancel the wages leave no earned income period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml new file mode 100644 index 0000000000..61a1b2cfbb --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml @@ -0,0 +1,61 @@ +# The welfare benefits the Universal Credit cap counts, with Housing Benefit +# at nil (UC Regs 2013 reg 80(2A)). + +- name: The Universal Credit cap leaves out Housing Benefit the family receives + period: 2025 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 30 + benunits: + family: + members: [adult] + universal_credit_pre_benefit_cap: 15_000 + housing_benefit_pre_benefit_cap: 10_000 + child_benefit: 5_000 + child_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + uc_benefit_cap: 14_753 + uc_childcare_element: 0 + uc_deductions: 0 + households: + household: + members: [adult] + country: ENGLAND + output: + # The Housing Benefit cap counts all three awards (HB Regs 2006 reg + # 75C(2)(b)); the Universal Credit cap counts 15,000 + 5,000 = 20,000, + # 5,247 over the cap (reg 81). + benefit_cap_welfare_benefits: 30_000 + uc_benefit_cap_welfare_benefits: 20_000 + uc_benefit_cap_reduction: 5_247 + universal_credit: 9_753 + +- name: A supplied benefit cap total includes Housing Benefit, which the Universal Credit cap takes out + period: 2025 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 30 + benunits: + family: + members: [adult] + universal_credit_pre_benefit_cap: 20_000 + housing_benefit_pre_benefit_cap: 6_000 + benefit_cap_welfare_benefits: 26_000 + uc_benefit_cap: 14_753 + uc_childcare_element: 0 + uc_deductions: 0 + households: + household: + members: [adult] + country: ENGLAND + output: + # 26,000 - 6,000 = 20,000, 5,247 over the cap, so the award is 14,753. + uc_benefit_cap_welfare_benefits: 20_000 + uc_benefit_cap_reduction: 5_247 + universal_credit: 14_753 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index a3175307e3..4c893f62ef 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -81,6 +81,7 @@ without the tax and National Insurance calculation. """ +from functools import lru_cache from pathlib import Path import numpy as np @@ -90,6 +91,7 @@ from hypothesis import strategies as st from policyengine_uk import CountryTaxBenefitSystem, Simulation +from policyengine_uk.model_api import YEAR, BenUnit, Variable PROPERTY_SETTINGS = settings( max_examples=10, @@ -884,8 +886,8 @@ def test_ctr_scales_match_enacted_sums(): 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.""" + """One household per family: the generated awards and debt deduction + rate, no Housing Benefit and no Maternity Allowance.""" people, benunits, households_ = {}, {}, {} for f, family in enumerate(families): members = [f"adult_{f}_{k}" for k in range(2 if family["couple"] else 1)] @@ -896,6 +898,7 @@ def capped_universal_credit_families(families, year, **extra): esa_contrib=family["esa_contrib"] if k == 0 else 0.0, maternity_allowance=0.0, ) + rate = family.get("uc_deduction_rate", 0.0) benunits[f"family_{f}"] = dict( members=members, **{ @@ -912,8 +915,8 @@ def capped_universal_credit_families(families, year, **extra): jsa_income=0.0, esa_income=0.0, housing_benefit_pre_benefit_cap=0.0, - uc_deduction_rate=0.0, - uc_deductions=0.0, + uc_deduction_rate=rate, + **(dict() if rate else dict(uc_deductions=0.0)), **extra, ) households_[f"household_{f}"] = dict(members=members, country="ENGLAND") @@ -924,12 +927,10 @@ def capped_universal_credit_families(families, year, **extra): 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, + universal_credit_award, ) - return universal_credit_after_benefit_cap( - sim.populations["benunit"], year, sim.tax_benefit_system.parameters - ) + return universal_credit_award(sim.populations["benunit"], year) CAPPED_FAMILY = st.fixed_dictionaries( @@ -942,6 +943,7 @@ def capped_universal_credit(sim, year): 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]), + uc_deduction_rate=st.sampled_from([0.0, 0.05, 0.15]), ) ) ONE_CAPPED_FAMILY = dict( @@ -964,112 +966,88 @@ def capped_universal_credit(sim, year): families=st.lists(CAPPED_FAMILY, min_size=1, max_size=8), year=st.integers(2025, 2030), ) -@example(families=[ONE_CAPPED_FAMILY], year=2025) +@example( + # Under the 85% floor the 15% debt deduction comes first and the cap + # reduction goes, so the award before deductions is the full 20,000. + families=[ONE_CAPPED_FAMILY, dict(ONE_CAPPED_FAMILY, uc_deduction_rate=0.15)], + 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, under current - 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.""" + """Invariant 12: the Universal Credit cap counts the welfare benefits + other than Housing Benefit, and a non-dependant family's gross income + counts its award after the cap and before debt deductions, under current + law and under an 85% protected minimum floor. The test changes the floor + in place on the same simulation.""" 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), current_law, atol=0.01 - ) + + def check(): + # UC Regs 2013 reg 80(2A), with no Housing Benefit to take out. + np.testing.assert_array_equal( + sim.calculate("uc_benefit_cap_welfare_benefits", year), + sim.calculate("benefit_cap_welfare_benefits", year) + - sim.calculate("housing_benefit_pre_benefit_cap", year), + ) + award = np.asarray(sim.calculate("universal_credit", year)) + np.testing.assert_allclose( + capped_universal_credit(sim, year), + np.maximum( + sim.calculate("universal_credit_pre_benefit_cap", year) + - sim.calculate("uc_benefit_cap_reduction_after_floor", year), + 0, + ), + atol=0.01, + ) + return award + + current_law = check() 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. + try: + floored = check() + finally: + sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( + period=str(year), value=0.0 + ) + sim.tax_benefit_system.reset_parameter_caches() + # The floor only limits the reductions, so no award falls. assert np.all(floored >= current_law - 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, - ) +def test_a_supplied_benefit_cap_total_with_a_non_dependant_cycles(): + """A supplied benefit_cap_welfare_benefits includes Housing Benefit, so + Universal Credit takes out housing_benefit_pre_benefit_cap. Where the + simulation calculates that Housing Benefit for a household with a + non-dependant on Universal Credit, the award and the deduction depend on + each other, and the engine raises.""" + from policyengine_core.errors import CycleError 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) - - # 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, + age=40, current_education="NOT_IN_EDUCATION", - employment_income=5_000.0, + employment_income=4_000.0, weekly_hours=20.0, ), ), benunits=dict( - claimant_unit=dict(members=["claimant"], benunit_is_rent_liable=True), + claimant_unit=dict( + members=["claimant"], + benunit_is_rent_liable=True, + benefit_cap_welfare_benefits=0.0, + ), non_dependant_unit=dict( members=["non_dependant"], benunit_is_rent_liable=False, would_claim_uc=True, + universal_credit_pre_benefit_cap=15_000.0, + benefit_cap_welfare_benefits=15_000.0, ), ), households=dict( @@ -1084,20 +1062,157 @@ def apply(self): ), [year], ) - current = Simulation(situation=situation) - scaled = CoreSimulation( - tax_benefit_system=ScaledAward(SYSTEM), - situation=situation, + with pytest.raises(CycleError): + Simulation(situation=situation).calculate("housing_benefit", year) + + +class uc_award_scale(Variable): + value_type = float + entity = BenUnit + label = "Scale on the Universal Credit award, for a test reform" + definition_period = YEAR + default_value = 1.0 + + +@lru_cache(maxsize=None) +def scaled_award_system(): + """Current law, with universal_credit replaced by uc_award_scale times + current law's formula. The replacement still reads the cap reduction.""" + from policyengine_core.reforms import Reform + + from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( + universal_credit as current_law, + ) + + def formula(benunit, period, parameters): + return benunit("uc_award_scale", period) * current_law.formula( + benunit, period, parameters + ) + + class ScaledAward(Reform): + def apply(self): + self.add_variable(uc_award_scale) + self.update_variable( + type("universal_credit", (current_law,), dict(formula=formula)) + ) + + return ScaledAward(SYSTEM) + + +AWARD_YEAR = 2025 +AWARD_HOUSEHOLD = st.fixed_dictionaries( + dict( + employment_income=st.floats(0, 20_000), + universal_credit_pre_benefit_cap=st.floats(0, 25_000), + scale=st.floats(0, 1), + ) +) +# 4,000 of wages plus the 14,753 capped award is 360.63 a week, in the 348 +# band; a tenth less award gives 332.26 a week, in the 266 band. +BAND_CROSSING = dict( + employment_income=4_000.0, universal_credit_pre_benefit_cap=15_000.0, scale=0.9 +) + + +def award_households(households, awards=None): + """Each generated household twice, with the generated scale and then + with none: a 70-year-old claimant paying 6,000 rent, and a 40-year-old + non-dependant working 20 hours who claims Universal Credit under a 14,753 + cap. awards supplies each family's universal_credit.""" + people, benunits, households_ = {}, {}, {} + no_awards = {benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS} + for copy, scaled in enumerate([True, False]): + for h, household in enumerate(households): + claimant, non_dependant = f"claimant_{copy}_{h}", f"non_dep_{copy}_{h}" + people[claimant] = dict(age=70, current_education="NOT_IN_EDUCATION") + people[non_dependant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=household["employment_income"], + weekly_hours=20.0, + ) + benunits[f"claimant_unit_{copy}_{h}"] = dict( + members=[claimant], benunit_is_rent_liable=True, **no_awards + ) + benunits[f"non_dep_unit_{copy}_{h}"] = dict( + members=[non_dependant], + benunit_is_rent_liable=False, + would_claim_uc=True, + uc_benefit_cap=14_753.0, + uc_childcare_element=0.0, + **{ + **no_awards, + "universal_credit_pre_benefit_cap": household[ + "universal_credit_pre_benefit_cap" + ], + }, + ) + if awards is None: + benunits[f"non_dep_unit_{copy}_{h}"]["uc_award_scale"] = ( + household["scale"] if scaled else 1.0 + ) + households_[f"household_{copy}_{h}"] = dict( + members=[claimant, non_dependant], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=6_000.0, + tenure_type="RENT_PRIVATELY", + ) + if awards is not None: + for name, award in zip(benunits, awards): + benunits[name]["universal_credit"] = float(award) + return for_years( + dict(people=people, benunits=benunits, households=households_), [AWARD_YEAR] + ) + + +@PROPERTY_SETTINGS +@given(households=st.lists(AWARD_HOUSEHOLD, min_size=1, max_size=3)) +@example(households=[BAND_CROSSING]) +def test_deduction_follows_a_reformed_award(households): + """Invariant 12: under a reform that scales the Universal Credit award, + each household's Housing Benefit and CTR deductions equal those of the + same household with the reformed award supplied, and the Housing Benefit + deduction is the band of the non-dependant's wages plus that award, so + an award that crosses a band moves the deduction.""" + from policyengine_core.simulations import Simulation as CoreSimulation + + year = AWARD_YEAR + reformed = CoreSimulation( + tax_benefit_system=scaled_award_system(), + situation=award_households(households), 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 + awards = np.asarray(reformed.calculate("universal_credit", year)) + supplied = Simulation(situation=award_households(households, awards)) + for name in [ + "housing_benefit_non_dep_deductions", + "council_tax_reduction_non_dep_deductions", + "housing_benefit", + ]: + np.testing.assert_allclose( + reformed.calculate(name, year), + supplied.calculate(name, year), + atol=0.01, + err_msg=name, + ) + # Reg 74(1)(a), (2): the non-dependant works 20 hours and has no other + # income, so the deduction is the band of wages plus the award. + scale = SYSTEM.parameters(str(year)).gov.dwp.housing_benefit.non_dep_deduction + deductions = np.asarray( + reformed.calculate("housing_benefit_non_dep_deductions", year) ) - 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 + wages = [h["employment_income"] for h in households] * 2 + for i, (wage, award) in enumerate(zip(wages, awards[1::2])): + weekly = (wage + award) / WEEKS + edges = [bracket.threshold for bracket in scale.amount.brackets] + if min(abs(weekly - edge) for edge in edges) < 0.01: + continue + expected = scale.amount.calc(np.array([weekly]))[0] * WEEKS + assert deductions[2 * i] == pytest.approx(expected, abs=0.01), (i, weekly) + if households == [BAND_CROSSING]: + assert deductions[0] < deductions[2], deductions # Earned-income cases for invariant 13. Each sets the person's earnings diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index f6ff5000a6..ca05662268 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -84,12 +84,9 @@ "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 284b9739c0..565dba21cd 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,31 +1,7 @@ """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 -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, -) -from policyengine_uk.variables.gov.dwp.jsa_income import ( - income_related_jsa_award_on_reports, -) # 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 @@ -42,16 +18,6 @@ "pension_credit", ] -# 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 -# allowance and the widow's benefits have no input. -CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ - benefit - for benefit in CAPPED_CLAIMANT_OR_PARTNER_BENEFITS + CAPPED_FAMILY_BENEFITS - if benefit != "housing_benefit_pre_benefit_cap" -] + ["maternity_allowance"] - def non_dependant_weekly_gross_income(person, period, parameters, own_gross_income): """Normal weekly gross income for the deduction bands. A claimant or @@ -69,9 +35,7 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco 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, parameters) - ) + universal_credit = benunit.project(universal_credit_award(benunit, period)) uc_claimant = person("is_uc_assessed_claimant", period) couple_claims_universal_credit = person.benunit.any( uc_claimant & claimant_or_partner @@ -88,88 +52,17 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco return annual / WEEKS_IN_YEAR -def reads_simulation_value(benunit, period, variable_name, default_formula=None): - """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 - 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 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 - 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 - 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, 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 - ): - 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: - 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, - period, - "jsa_income", - "jsa_income_reported", - income_related_jsa_award_on_reports, - couple, - ) + award_of_couple( - benunit, - period, - "esa_income", - "esa_income_reported", - income_related_esa_award_on_reports, - couple, - ) - return income_based + add_for_members( - benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT, couple - ) +def universal_credit_award(benunit, period): + """Return a family's Universal Credit award after the benefit cap (UC + Regs 2013 reg 81) and before debt deductions. Welfare Reform Act 2012 + s.8(1) sets the award as the maximum amount less the amounts for + income. DWP then takes advances (SI 2013/383 reg 10), overpayments (SI + 2013/384 regs 10-11) and third-party payments (SI 2013/380 Sch 6 para + 2(1)) from the sum payable, so those deductions stay the family's + income. A nil award carries none.""" + universal_credit = benunit("universal_credit", period) + deductions = benunit("uc_deductions", period) + return universal_credit + where(universal_credit > 0, deductions, 0) def has_earned_income(person, period): 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 40710ed628..42054b797b 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 @@ -41,6 +41,13 @@ def formula(benunit, period, parameters): non_dependant = ( person("is_non_dependant_of_household_head", period) & ~joint_occupier ) + # With no non-dependant there is nothing to deduct, so the formula + # reads no income. Universal Credit reads Housing Benefit where the + # inputs supply benefit_cap_welfare_benefits + # (uc_benefit_cap_welfare_benefits); a household of one family can + # then still calculate both. + if not non_dependant.any(): + return benunit.empty_array() deductions = ( person("household_benefits_individual_non_dep_deduction", period) * non_dependant 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 36defbb6a3..a6fde7766a 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,19 +1,6 @@ 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 - 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) - 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 @@ -42,9 +29,11 @@ class uc_benefit_cap_reduction(Variable): def formula(benunit, period, parameters): # Reg 80(2A): where the welfare benefit is Housing Benefit, "the # amount to be used is nil". benefit_cap_welfare_benefits counts it - # for the Housing Benefit cap (HB Regs 2006 reg 75C(2)(b)), so it is - # taken out here; a family paid both is capped on its other benefits. - welfare_benefits = benunit("benefit_cap_welfare_benefits", period) - benunit( - "housing_benefit_pre_benefit_cap", period - ) - return benefit_cap_reduction_of(benunit, period, welfare_benefits) + # for the Housing Benefit cap (HB Regs 2006 reg 75C(2)(b)); + # uc_benefit_cap_welfare_benefits leaves it out, so a family paid + # both is capped on its other benefits. + welfare_benefits = benunit("uc_benefit_cap_welfare_benefits", 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) 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 615ff5c537..b40442dc06 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,24 +4,6 @@ ) -def benefit_cap_reduction_applied( - benunit, period, parameters, cap_reduction, deduction_demand -): - """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 - _, 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 = ( @@ -42,16 +24,18 @@ 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. - return benefit_cap_reduction_applied( - benunit, - period, - parameters, + _, cap_reduction = allocate_uc_reductions( + benunit("universal_credit_pre_benefit_cap", period), 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/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py new file mode 100644 index 0000000000..e2025325d9 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -0,0 +1,75 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.benefit_cap import benefit_cap_couple +from policyengine_uk.utils.benefit_unit import 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, + FAMILY_BENEFITS, +) +from policyengine_uk.variables.gov.dwp.esa_income import ( + income_related_esa_award_on_reports, +) +from policyengine_uk.variables.gov.dwp.jsa_income import ( + income_related_jsa_award_on_reports, +) + +# UC Regs 2013 reg 80(2A): where the welfare benefit is Housing Benefit, "the +# amount to be used is nil". The Universal Credit cap therefore counts the +# Housing Benefit cap's benefits other than Housing Benefit. +UC_FAMILY_BENEFITS = [ + benefit + for benefit in FAMILY_BENEFITS + if benefit != "housing_benefit_pre_benefit_cap" +] + + +class uc_benefit_cap_welfare_benefits(Variable): + value_type = float + entity = BenUnit + label = "Welfare benefits counted by the Universal Credit benefit cap" + documentation = ( + "The welfare benefits the Universal Credit cap counts: those in " + "benefit_cap_welfare_benefits, for the same single person or couple, " + "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Universal " + "Credit therefore reads no Housing Benefit, and the Housing Benefit " + "non-dependant deductions can read universal_credit. Where the " + "inputs supply benefit_cap_welfare_benefits, whose total includes " + "Housing Benefit, this subtracts housing_benefit_pre_benefit_cap " + "from it." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/96", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/80", + ) + + def formula(benunit, period, parameters): + if stored_as_input(benunit, "benefit_cap_welfare_benefits", period): + return benunit("benefit_cap_welfare_benefits", period) - benunit( + "housing_benefit_pre_benefit_cap", period + ) + # The rest follows benefit_cap_welfare_benefits, in the same order. + couple = benefit_cap_couple(benunit, period) + income_based = award_of_couple( + benunit, + period, + "jsa_income", + "jsa_income_reported", + income_related_jsa_award_on_reports, + couple, + ) + award_of_couple( + benunit, + period, + "esa_income", + "esa_income_reported", + income_related_esa_award_on_reports, + couple, + ) + return income_based + add_for_members( + benunit, + period, + CLAIMANT_OR_PARTNER_BENEFITS + UC_FAMILY_BENEFITS, + couple, + ) From 270d4eff595a312202d8634a4a9b9dcc702dec69 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 07:09:11 -0500 Subject: [PATCH 33/43] Test the award read, the cap basket and a reform crossing a band Invariant 12 now checks that the Universal Credit cap's basket equals the combined total less Housing Benefit, that the award counted in gross income is universal_credit plus debt deductions (the award before the cap less the reduction applied) under current law and an 85% floor, and that Universal Credit's trace reads no Housing Benefit. A Hypothesis property scales the award by a reform and checks each household's deductions against the same household with the reformed award supplied, and against the band of wages plus that award; the explicit example crosses from the 348 band to the 266 band. A supplied total with a calculated Housing Benefit and a non-dependant on Universal Credit raises CycleError. The tests build core simulations on the module's system, so the rewritten invariant 12 tests take about 45 seconds locally. The shard table takes the six entries and the earned-income test from run 41294689067. Comments on the cap total, the cap couple and the passporting rule describe the new basket, and the review cases carry no review history. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/tests/shard_durations.toml | 13 +- ...test_non_dependant_deduction_properties.py | 162 ++++++++---------- policyengine_uk/utils/benefit_cap.py | 11 +- .../gov/dwp/benefit_cap_welfare_benefits.py | 8 +- .../housing_benefit_on_passporting_benefit.py | 10 +- 5 files changed, 97 insertions(+), 107 deletions(-) diff --git a/policyengine_uk/tests/shard_durations.toml b/policyengine_uk/tests/shard_durations.toml index 86e9a1fc10..a5d21cd607 100644 --- a/policyengine_uk/tests/shard_durations.toml +++ b/policyengine_uk/tests/shard_durations.toml @@ -9,7 +9,7 @@ # 40067189795, 40071060766, 40079467297, 40083761750 and 40087883891, and # 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 +# 41294689067 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 @@ -156,7 +156,6 @@ "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 @@ -171,6 +170,7 @@ "policyengine_uk/tests/test_uc_state_pension_properties.py::test_uc_falls_pound_for_pound_in_state_pension" = 96 "policyengine_uk/tests/test_uc_work_related_groups_properties.py::test_qualifying_age_route_moves_only_claimants_past_it" = 96 "policyengine_uk/tests/test_uc_earned_income_before_work_allowance_properties.py::test_more_pay_never_turns_the_childcare_criterion_on" = 96 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_non_dependant_deduction_invariants" = 96 "policyengine_uk/tests/test_legacy_award_work_properties.py::test_screens_match_a_family_by_family_reading" = 95 "policyengine_uk/tests/test_dated_reform_routes.py::test_a_schedule_of_two_changes_matches_the_oracle" = 95 "policyengine_uk/tests/test_uc_state_pension_properties.py::test_state_pension_counts_like_private_pension" = 95 @@ -266,6 +266,7 @@ "policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py::test_no_op_without_sharers_boarders_or_lodgers" = 72 "policyengine_uk/tests/test_state_pension_age.py::test_status_matches_statute_for_every_birth_date[2017]" = 72 "policyengine_uk/tests/test_pawhp_household_income_properties.py::test_switching_pawhp_off_lowers_household_income_by_pawhp" = 72 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_capped_universal_credit_matches_the_model" = 72 "policyengine_uk/tests/test_process_time_reforms.py::test_a_triple_lock_rate_set_by_reform_uprates_the_state_pension[via_parameter_changes-generated-bare-year]" = 71 "policyengine_uk/tests/test_state_pension_age.py::test_status_matches_statute_for_every_birth_date[2019]" = 71 "policyengine_uk/tests/test_labour_supply_response_baseline.py::test_basic_rate_rise_gives_responses_of_the_implied_sign" = 71 @@ -310,7 +311,6 @@ "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 @@ -377,6 +377,7 @@ "policyengine_uk/tests/test_uc_statutory_pay_properties.py::test_uc_does_not_increase_as_statutory_paternity_pay_rises" = 48 "policyengine_uk/tests/test_salary_sacrifice_ni_only.py::test_explicit_actual_return_matches_actual_pay_and_member_inputs[personal_allowance]" = 48 "policyengine_uk/tests/test_income_support_eligibility_properties.py::test_more_work_or_jsa_never_makes_a_family_eligible" = 48 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_earned_income_exemption_reads_universal_credit" = 48 "policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py::test_disregard_is_non_decreasing_in_earnings" = 47 "policyengine_uk/tests/test_benefit_cap_uc_and_hb_properties.py::test_a_family_with_both_awards_is_capped_once_through_universal_credit" = 47 "policyengine_uk/tests/test_legacy_award_readers_properties.py::test_other_members_esa_never_changes_non_dependant_exemption" = 47 @@ -584,7 +585,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[WALES]" = 15 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[SCOTLAND]" = 12 "policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[HOUSING_BENEFIT]" = 10 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[ENGLAND]" = 9 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 4c893f62ef..c819ec3632 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -67,12 +67,20 @@ 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 - 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. +12. The Universal Credit cap counts the Housing Benefit cap's welfare + benefits other than Housing Benefit (UC Regs 2013 reg 80(2A)), so + universal_credit reads no Housing Benefit unless the inputs supply + benefit_cap_welfare_benefits. A non-dependant family's gross income + counts its award after the cap and before debt deductions: universal_credit + plus uc_deductions, which equals the award before the cap less the cap + reduction applied, under current law and under an 85% protected minimum + floor; the floor never lowers an award. Under a reform that scales + universal_credit, each household's deductions equal those of the same + household with the reformed award supplied, and the Housing Benefit + deduction is the band of wages plus that award, so it moves when the + award crosses a band. With a supplied total, a calculated Housing + Benefit and a non-dependant on Universal Credit, the engine raises + CycleError. 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 @@ -933,6 +941,19 @@ def capped_universal_credit(sim, year): return universal_credit_award(sim.populations["benunit"], year) +def core_simulation(situation, year, system=SYSTEM, trace=False): + """A simulation on a tax-benefit system built once for the module.""" + from policyengine_core.simulations import Simulation as CoreSimulation + + return CoreSimulation( + tax_benefit_system=system, + situation=situation, + default_input_period=year, + default_calculation_period=year, + trace=trace, + ) + + CAPPED_FAMILY = st.fixed_dictionaries( dict( couple=st.booleans(), @@ -976,18 +997,17 @@ def test_capped_universal_credit_matches_the_model(families, year): """Invariant 12: the Universal Credit cap counts the welfare benefits other than Housing Benefit, and a non-dependant family's gross income counts its award after the cap and before debt deductions, under current - law and under an 85% protected minimum floor. The test changes the floor - in place on the same simulation.""" - sim = Simulation(situation=capped_universal_credit_families(families, year)) + law and under an 85% protected minimum floor, which never lowers an + award.""" def check(): + sim = core_simulation(capped_universal_credit_families(families, year), year) # UC Regs 2013 reg 80(2A), with no Housing Benefit to take out. np.testing.assert_array_equal( sim.calculate("uc_benefit_cap_welfare_benefits", year), sim.calculate("benefit_cap_welfare_benefits", year) - sim.calculate("housing_benefit_pre_benefit_cap", year), ) - award = np.asarray(sim.calculate("universal_credit", year)) np.testing.assert_allclose( capped_universal_credit(sim, year), np.maximum( @@ -997,75 +1017,20 @@ def check(): ), atol=0.01, ) - return award + return np.asarray(sim.calculate("universal_credit", year)) current_law = check() - 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() + floor = SYSTEM.parameters.get_child(PROTECTED_FLOOR) + floor.update(period=str(year), value=0.85) + SYSTEM.reset_parameter_caches() try: floored = check() finally: - sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( - period=str(year), value=0.0 - ) - sim.tax_benefit_system.reset_parameter_caches() - # The floor only limits the reductions, so no award falls. + floor.update(period=str(year), value=0.0) + SYSTEM.reset_parameter_caches() assert np.all(floored >= current_law - 0.01) -def test_a_supplied_benefit_cap_total_with_a_non_dependant_cycles(): - """A supplied benefit_cap_welfare_benefits includes Housing Benefit, so - Universal Credit takes out housing_benefit_pre_benefit_cap. Where the - simulation calculates that Housing Benefit for a household with a - non-dependant on Universal Credit, the award and the deduction depend on - each other, and the engine raises.""" - from policyengine_core.errors import CycleError - - year = 2025 - situation = for_years( - dict( - people=dict( - claimant=dict(age=70), - non_dependant=dict( - age=40, - current_education="NOT_IN_EDUCATION", - employment_income=4_000.0, - weekly_hours=20.0, - ), - ), - benunits=dict( - claimant_unit=dict( - members=["claimant"], - benunit_is_rent_liable=True, - benefit_cap_welfare_benefits=0.0, - ), - non_dependant_unit=dict( - members=["non_dependant"], - benunit_is_rent_liable=False, - would_claim_uc=True, - universal_credit_pre_benefit_cap=15_000.0, - benefit_cap_welfare_benefits=15_000.0, - ), - ), - households=dict( - household=dict( - members=["claimant", "non_dependant"], - country="ENGLAND", - local_authority="MAIDSTONE", - rent=6_000.0, - tenure_type="RENT_PRIVATELY", - ) - ), - ), - [year], - ) - with pytest.raises(CycleError): - Simulation(situation=situation).calculate("housing_benefit", year) - - class uc_award_scale(Variable): value_type = float entity = BenUnit @@ -1114,14 +1079,15 @@ def apply(self): ) -def award_households(households, awards=None): +def award_households(households, awards=None, scaled=True): """Each generated household twice, with the generated scale and then with none: a 70-year-old claimant paying 6,000 rent, and a 40-year-old non-dependant working 20 hours who claims Universal Credit under a 14,753 - cap. awards supplies each family's universal_credit.""" + cap. awards supplies each family's universal_credit; scaled=False leaves + out the scale, for current law.""" people, benunits, households_ = {}, {}, {} no_awards = {benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS} - for copy, scaled in enumerate([True, False]): + for copy, with_scale in enumerate([True, False]): for h, household in enumerate(households): claimant, non_dependant = f"claimant_{copy}_{h}", f"non_dep_{copy}_{h}" people[claimant] = dict(age=70, current_education="NOT_IN_EDUCATION") @@ -1147,9 +1113,9 @@ def award_households(households, awards=None): ], }, ) - if awards is None: + if awards is None and scaled: benunits[f"non_dep_unit_{copy}_{h}"]["uc_award_scale"] = ( - household["scale"] if scaled else 1.0 + household["scale"] if with_scale else 1.0 ) households_[f"household_{copy}_{h}"] = dict( members=[claimant, non_dependant], @@ -1166,6 +1132,36 @@ def award_households(households, awards=None): ) +def test_universal_credit_reads_housing_benefit_only_in_a_supplied_total(): + """Invariant 12: Universal Credit's calculation reads no Housing Benefit, + so the deductions can read universal_credit. A supplied + benefit_cap_welfare_benefits includes Housing Benefit, so Universal + Credit takes out housing_benefit_pre_benefit_cap. Where the simulation + calculates that Housing Benefit for a household with a non-dependant on + Universal Credit, the award and the deduction depend on each other, and + the engine raises.""" + from policyengine_core.errors import CycleError + + year = 2025 + households = [BAND_CROSSING] + situation = award_households(households, scaled=False) + sim = core_simulation(situation, year, trace=True) + sim.calculate("universal_credit", year) + read = {key.split("<")[0] for key in sim.tracer.get_serialized_flat_trace()} + assert "universal_credit" in read + assert not [ + name + for name in read + if name.startswith("housing_benefit") and name != "housing_benefit_reported" + ], read + for unit in situation["benunits"].values(): + unit["benefit_cap_welfare_benefits"] = { + str(year): unit["universal_credit_pre_benefit_cap"][str(year)] + } + with pytest.raises(CycleError): + core_simulation(situation, year).calculate("housing_benefit", year) + + @PROPERTY_SETTINGS @given(households=st.lists(AWARD_HOUSEHOLD, min_size=1, max_size=3)) @example(households=[BAND_CROSSING]) @@ -1175,17 +1171,12 @@ def test_deduction_follows_a_reformed_award(households): same household with the reformed award supplied, and the Housing Benefit deduction is the band of the non-dependant's wages plus that award, so an award that crosses a band moves the deduction.""" - from policyengine_core.simulations import Simulation as CoreSimulation - year = AWARD_YEAR - reformed = CoreSimulation( - tax_benefit_system=scaled_award_system(), - situation=award_households(households), - default_input_period=year, - default_calculation_period=year, + reformed = core_simulation( + award_households(households), year, system=scaled_award_system() ) awards = np.asarray(reformed.calculate("universal_credit", year)) - supplied = Simulation(situation=award_households(households, awards)) + supplied = core_simulation(award_households(households, awards), year) for name in [ "housing_benefit_non_dep_deductions", "council_tax_reduction_non_dep_deductions", @@ -1206,8 +1197,7 @@ def test_deduction_follows_a_reformed_award(households): wages = [h["employment_income"] for h in households] * 2 for i, (wage, award) in enumerate(zip(wages, awards[1::2])): weekly = (wage + award) / WEEKS - edges = [bracket.threshold for bracket in scale.amount.brackets] - if min(abs(weekly - edge) for edge in edges) < 0.01: + if min(abs(weekly - edge) for edge in scale.amount.thresholds) < 0.01: continue expected = scale.amount.calc(np.array([weekly]))[0] * WEEKS assert deductions[2 * i] == pytest.approx(expected, abs=0.01), (i, weekly) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index 9a334e3ddf..82e28af299 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -39,11 +39,12 @@ def benefit_cap_couple(benunit, period): (is_uc_assessed_claimant): a single claimant or joint claimants, and the other member of a couple where one member claims as a single person (UC Regs 2013 reg 78(2)). Otherwise it is Housing Benefit's claimant and - partner (is_claimant_or_partner; HB Regs 2006 regs 2(1), 75A). One total - of welfare benefits (benefit_cap_welfare_benefits) serves both caps: a - family receiving both awards is capped only through Universal Credit (UC - Regs 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so its total is the - Universal Credit couple's. The two couples are the same unless + partner (is_claimant_or_partner; HB Regs 2006 regs 2(1), 75A). Both caps + count this couple's welfare benefits (benefit_cap_welfare_benefits, and + uc_benefit_cap_welfare_benefits without Housing Benefit): a family + receiving both awards is capped only through Universal Credit (UC Regs + 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so its couple is the + Universal Credit couple. The two couples are the same unless is_uc_claimant is entered. """ person = benunit.members diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index dc0af1081b..3471ff1c1e 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -16,10 +16,10 @@ # contributory or income-related benefit; it still counts in household income. # Housing Benefit counts its own amount before the cap (HB Regs 2006 # reg. 75C(2)(b)), so this total includes it. Universal Credit counts Housing -# Benefit as nil (UC Regs 2013 reg. 80(2A)): uc_benefit_cap_reduction takes it -# out. Universal Credit before the cap is in the total too; a family receiving -# it is outside the Housing Benefit cap (HB Regs 2006 reg. 75F(1)(g)). With -# those two adjustments one total serves both schemes. Reg. 75C(2)(a) counts +# Benefit as nil (UC Regs 2013 reg. 80(2A)), so its cap reads +# uc_benefit_cap_welfare_benefits, these benefits without Housing Benefit. +# Universal Credit before the cap is in the total too; HB reg. 75F(1)(g) puts a +# family receiving it outside the Housing Benefit cap. Reg. 75C(2)(a) counts # Housing Benefit for specified accommodation as nil; the model's input # in_specified_or_temporary_accommodation does not separate specified from # temporary accommodation, so that Housing Benefit is counted at its amount diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 780d17bc56..50ad361384 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -54,12 +54,10 @@ def formula(benunit, period, parameters): p.northern_ireland, p.great_britain, ) - # Universal Credit before the benefit cap and deductions: Universal - # Credit depends on Housing Benefit through the benefit cap - # (uc_benefit_cap_reduction, whose total benefit_cap_welfare_benefits - # counts Housing Benefit), so reading the amount paid - # (universal_credit) here would be circular. Before the cap is also - # the law's reading: entitlement counts whether or not it is paid. + # Universal Credit before the benefit cap and deductions: entitlement + # counts whether or not it is paid. The amount paid + # (universal_credit) reads Housing Benefit where the inputs supply + # benefit_cap_welfare_benefits (uc_benefit_cap_welfare_benefits). on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb on_legacy_benefit = benunit( "in_receipt_of_income_support_jsa_ib_or_esa_ir", period From 764d57b19433f98a0e7750089f6a85243e49d48d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 08:24:34 -0500 Subject: [PATCH 34/43] Register the cap total beside adjusted_net_income The registration sat where #2765 adds self_employment_income, so the two conflicted. The set's order means nothing; this place merges cleanly with #1969, #2661, #2765 and #2776. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/utils/supplied_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index ca05662268..5dec9471f1 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -46,6 +46,7 @@ SUPPLIED_INPUT_VARIABLES = frozenset( { "adjusted_net_income", + "benefit_cap_welfare_benefits", "carers_allowance", "carers_allowance_pre_couple_limit", "carers_allowance_pre_overlap", @@ -55,7 +56,6 @@ "receives_carer_benefit", "receives_carers_allowance", "receives_carer_support_payment", - "benefit_cap_welfare_benefits", "bus_fare_spending", "bus_in_london_trips", "current_education", From dfab37d348bb5d37e21dde3125f1929536837362 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 08:59:07 -0500 Subject: [PATCH 35/43] Name who acts in the gross-income docs, test comments and changelog DWP, the council and the model take the verbs the comments gave to guidance paragraphs, regulations and cases; the gross-income documentation drops its closing disclaimer, and the changelog qualifies the flat charge and the couple rule and adds the debt deductions and the Universal Credit cap's basket. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- .../housing_benefit_non_dep_gross_income.yaml | 9 ++-- .../non_dependant_deduction_review_cases.yaml | 22 +++++----- ...il_tax_reduction_non_dep_gross_income.yaml | 9 ++-- ...x_reduction_non_dep_remunerative_work.yaml | 4 +- ...test_non_dependant_deduction_properties.py | 13 +++--- .../non_dep_deduction/_non_dependants.py | 18 ++++---- .../housing_benefit_non_dep_gross_income.py | 41 +++++++++---------- .../uc_benefit_cap_welfare_benefits.py | 4 +- ...ncil_tax_reduction_non_dep_gross_income.py | 41 +++++++++---------- 10 files changed, 80 insertions(+), 83 deletions(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 34d291f0da..13ce85cfd4 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -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. +The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). 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 a7b9638754..1c8bfa4f8d 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 @@ -603,7 +603,8 @@ 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). + # DWP tells councils to count earnings and any other income (HB guidance + # A5.542). period: 2025 input: social_security_income: 0 @@ -626,9 +627,9 @@ 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. + # DWP tells councils to count earnings and other income (HB guidance + # A5.542), including savings interest (A5.570). This case supplies an + # exempt-interest total, so the model counts that total once. period: 2025 input: social_security_income: 0 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 c9cc019a0d..4e51f02aab 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 @@ -1262,9 +1262,9 @@ 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)). + # DWP subtracts allowable employment expenses from employed earnings + # (UC Regs 2013 reg 55(3)(a)), 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] @@ -1307,9 +1307,9 @@ 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 case - # holds the minimum income floor off, so only the loss rule decides. + # DWP adds the trades' profits and losses before applying the nil floor + # (UC Regs 2013 reg 57(2)), so the loss cancels the profit. The inputs + # turn off the minimum income floor. housing_benefit_non_dep_deduction_exempt: [false, true] council_tax_reduction_non_dep_deduction_exempt: [false, true] housing_benefit_non_dep_deductions: [0, 0] @@ -1352,8 +1352,8 @@ country: WALES local_authority: CARDIFF output: - # 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 + # DWP attributes the company's income to its owner as self-employed + # earnings (UC Regs 2013 reg 77(3)(b)), 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] @@ -1405,9 +1405,9 @@ country: WALES local_authority: CARDIFF output: - # 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 + # DWP applies the minimum income floor to a member of a couple only while + # the couple's earnings fall below the couple threshold (UC Regs 2013 reg + # 62(3)). The partner's earned income exceeds it, so the non-dependant # 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 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 ec89611965..b9a056f778 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 @@ -723,7 +723,8 @@ 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). + # DWP tells councils to count earnings and any other income (HB guidance + # A5.542). period: 2025 input: social_security_income: 0 @@ -746,9 +747,9 @@ 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. + # DWP tells councils to count earnings and other income (HB guidance + # A5.542), including savings interest (A5.570). This case supplies an + # exempt-interest total, so the model counts that total once. period: 2025 input: social_security_income: 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 d3bbe37bea..b660578932 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 @@ -5,8 +5,8 @@ # 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 give the household a +# Scotland (10.95). The cases hold the family's awards, which a single +# non-dependant's gross income would count, at zero. English cases give the household a # pension-age applicant, since England's national scheme covers pension-age # applicants only, and the model works out each family's scheme. diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index c819ec3632..556283beb2 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -247,8 +247,8 @@ def households(draw): 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 inputs name each family's UC claimants: is_uc_claimant is 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 = {}, {}, {}, [] @@ -1335,10 +1335,11 @@ def test_earned_income_exemption_reads_universal_credit(families, year): if case is None: continue member, settled = case - # 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. + # For each claimant the award assesses, compare the exemption with + # Universal Credit's earned-income calculation (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) 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 565dba21cd..565738d075 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 @@ -24,7 +24,8 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco 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, child benefit, Income Support, income-based JSA, income-related ESA and - Pension Credit, and its Universal Credit where either member claims it. + Pension Credit, and its Universal Credit award (universal_credit_award) + 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 @@ -66,14 +67,13 @@ def universal_credit_award(benunit, period): def has_earned_income(person, period): - """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.""" + """Read whether Universal Credit counts positive earned income for the + person (uc_individual_earned_income). 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 deducts allowable expenses, tax, National Insurance and + relievable pension contributions (reg 55(3), (5)); attributes company + income to its owner (reg 77(3)); offsets trading losses (reg 57(2)); and + applies the floor under reg 62, a couple's floor included.""" return person("uc_individual_earned_income", period) > 0 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 e591cca6ab..0faeda2b2a 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 @@ -18,31 +18,27 @@ class housing_benefit_non_dep_gross_income(Variable): documentation = ( "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "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 " + "pension contributions. DWP tells councils to count earnings and any " + "other income (HB guidance A5.542), to count a self-employed " + "non-dependant's total income before business expenses (A5.551) and " + "to add savings interest to earnings (A5.570). The model holds " + "self-employment and property profits rather than receipts, so it " + "adds each profit and counts a loss as nil, and a loss never reduces " "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." + "social security income sources; the non-taxable periodic benefits " + "the model represents separately; and other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends. 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. Benefit-unit awards, such as Child Benefit, tax credits " + "and Universal Credit, stay out of it. The deduction formula adds " + "family awards to the claimant and partner's joint income and applies " + "the remunerative-work test." ) def formula(person, period, parameters): @@ -55,7 +51,8 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest: ISA interest unless the inputs supply a total. + # Count ISA interest unless the inputs supply an exempt-interest + # 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_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py index e2025325d9..bb60e1a70f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -31,11 +31,11 @@ class uc_benefit_cap_welfare_benefits(Variable): "The welfare benefits the Universal Credit cap counts: those in " "benefit_cap_welfare_benefits, for the same single person or couple, " "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Universal " - "Credit therefore reads no Housing Benefit, and the Housing Benefit " + "Credit then reads no Housing Benefit, so the Housing Benefit " "non-dependant deductions can read universal_credit. Where the " "inputs supply benefit_cap_welfare_benefits, whose total includes " "Housing Benefit, this subtracts housing_benefit_pre_benefit_cap " - "from it." + "from it, and Universal Credit reads Housing Benefit." ) definition_period = YEAR unit = GBP 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 fe3e109e02..01e88a693e 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 @@ -22,31 +22,27 @@ class council_tax_reduction_non_dep_gross_income(Variable): documentation = ( "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "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 " + "pension contributions. DWP tells councils to count earnings and any " + "other income (HB guidance A5.542), to count a self-employed " + "non-dependant's total income before business expenses (A5.551) and " + "to add savings interest to earnings (A5.570). The model holds " + "self-employment and property profits rather than receipts, so it " + "adds each profit and counts a loss as nil, and a loss never reduces " "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." + "social security income sources; the non-taxable periodic benefits " + "the model represents separately; and other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends. 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. Benefit-unit awards, such as Child Benefit, tax credits " + "and Universal Credit, stay out of it. The deduction formula adds " + "family awards to the claimant and partner's joint income and applies " + "the remunerative-work test." ) def formula(person, period, parameters): @@ -59,7 +55,8 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest: ISA interest unless the inputs supply a total. + # Count ISA interest unless the inputs supply an exempt-interest + # total. "tax_free_savings_income", "other_investment_income", "dividend_income", From 02cc0896f5c3ca3efed822ba265f96216be3c453 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 10:56:26 -0500 Subject: [PATCH 36/43] Give the cap and passport comments their actors Comment-only: the council, DWP and Universal Credit take the verbs that be-verbs and passives held in the comments this round touched. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_deduction_properties.py | 4 ++-- policyengine_uk/utils/benefit_cap.py | 7 +++---- .../housing_benefit_on_passporting_benefit.py | 4 ++-- .../housing_benefit_non_dep_deductions.py | 2 +- .../benefit_cap/uc_benefit_cap_reduction.py | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 556283beb2..7d2acc30fd 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -1072,8 +1072,8 @@ def apply(self): scale=st.floats(0, 1), ) ) -# 4,000 of wages plus the 14,753 capped award is 360.63 a week, in the 348 -# band; a tenth less award gives 332.26 a week, in the 266 band. +# 4,000 of wages plus the 14,753 capped award come to 360.63 a week, in the +# 348 band; a tenth less award gives 332.26 a week, in the 266 band. BAND_CROSSING = dict( employment_income=4_000.0, universal_credit_pre_benefit_cap=15_000.0, scale=0.9 ) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index 82e28af299..21de4a9efc 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -41,10 +41,9 @@ def benefit_cap_couple(benunit, period): (UC Regs 2013 reg 78(2)). Otherwise it is Housing Benefit's claimant and partner (is_claimant_or_partner; HB Regs 2006 regs 2(1), 75A). Both caps count this couple's welfare benefits (benefit_cap_welfare_benefits, and - uc_benefit_cap_welfare_benefits without Housing Benefit): a family - receiving both awards is capped only through Universal Credit (UC Regs - 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so its couple is the - Universal Credit couple. The two couples are the same unless + uc_benefit_cap_welfare_benefits without Housing Benefit). Universal Credit + alone caps a family receiving both awards (UC Regs 2013 reg 80(2A); HB + Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two couples are the same unless is_uc_claimant is entered. """ person = benunit.members diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 50ad361384..9c390ed462 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -54,8 +54,8 @@ def formula(benunit, period, parameters): p.northern_ireland, p.great_britain, ) - # Universal Credit before the benefit cap and deductions: entitlement - # counts whether or not it is paid. The amount paid + # Read Universal Credit before the benefit cap and deductions: + # entitlement counts whether or not DWP pays it. The amount paid # (universal_credit) reads Housing Benefit where the inputs supply # benefit_cap_welfare_benefits (uc_benefit_cap_welfare_benefits). on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb 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 42054b797b..4061946eaa 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 @@ -41,7 +41,7 @@ def formula(benunit, period, parameters): non_dependant = ( person("is_non_dependant_of_household_head", period) & ~joint_occupier ) - # With no non-dependant there is nothing to deduct, so the formula + # With no non-dependant the council deducts nothing, so the formula # reads no income. Universal Credit reads Housing Benefit where the # inputs supply benefit_cap_welfare_benefits # (uc_benefit_cap_welfare_benefits); a household of one family can 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 a6fde7766a..3571e6fb82 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 @@ -30,8 +30,8 @@ def formula(benunit, period, parameters): # Reg 80(2A): where the welfare benefit is Housing Benefit, "the # amount to be used is nil". benefit_cap_welfare_benefits counts it # for the Housing Benefit cap (HB Regs 2006 reg 75C(2)(b)); - # uc_benefit_cap_welfare_benefits leaves it out, so a family paid - # both is capped on its other benefits. + # uc_benefit_cap_welfare_benefits leaves it out, so Universal Credit + # caps a family paid both on its other benefits. welfare_benefits = benunit("uc_benefit_cap_welfare_benefits", period) excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) reduction = max_(excess - benunit("uc_childcare_element", period), 0) From f025c0d50b89b3df6f1aefab62b2611958fb6343 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 15:43:29 -0500 Subject: [PATCH 37/43] Require Housing Benefit beside a supplied cap total uc_benefit_cap_welfare_benefits subtracts housing_benefit_pre_benefit_cap from a supplied benefit_cap_welfare_benefits only when the inputs supply it too, and raises ValueError otherwise. Universal Credit then calculates no Housing Benefit on any baseline route, so housing_benefit_non_dep_deductions loses its early return. A model value (set_model_value) supplies neither. Tests: the mixed-population contract, the population-consistent totals, the cap-list uniqueness check, and the ninth supplied-total YAML case now supplies the Housing Benefit it calculated (nil). Co-Authored-By: Claude Opus 5.5 --- .../test_benefit_cap_lists_unique.py | 27 + .../gov/dwp/benefit_cap_by_scheme.yaml | 1 + .../test_non_dependant_cycle_contracts.py | 548 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 49 +- ...n_dependant_population_consistent_total.py | 104 ++++ policyengine_uk/utils/benefit_cap.py | 4 +- policyengine_uk/utils/supplied_inputs.py | 1 + .../gov/dwp/benefit_cap_welfare_benefits.py | 18 +- .../housing_benefit_on_passporting_benefit.py | 4 +- .../housing_benefit_non_dep_deductions.py | 7 - .../uc_benefit_cap_welfare_benefits.py | 38 +- 11 files changed, 757 insertions(+), 44 deletions(-) create mode 100644 policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py create mode 100644 policyengine_uk/tests/test_non_dependant_cycle_contracts.py create mode 100644 policyengine_uk/tests/test_non_dependant_population_consistent_total.py diff --git a/policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py b/policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py new file mode 100644 index 0000000000..c4bb47b45b --- /dev/null +++ b/policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py @@ -0,0 +1,27 @@ +from collections import Counter + +import pytest + +from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( + CLAIMANT_OR_PARTNER_BENEFITS, + FAMILY_BENEFITS, +) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_welfare_benefits import ( + UC_FAMILY_BENEFITS, +) +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + FAMILY_GROSS_INCOME_BENEFITS, +) + + +@pytest.mark.parametrize( + "names", + [ + CLAIMANT_OR_PARTNER_BENEFITS + FAMILY_BENEFITS, + CLAIMANT_OR_PARTNER_BENEFITS + UC_FAMILY_BENEFITS, + FAMILY_GROSS_INCOME_BENEFITS, + ], + ids=["housing-benefit-cap", "universal-credit-cap", "non-dependant-family-income"], +) +def test_each_name_occurs_once(names): + assert not {name: n for name, n in Counter(names).items() if n != 1} diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml index 53e746bdf2..09bbd8efd9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml @@ -320,6 +320,7 @@ uc_benefit_cap: 22_020 uc_childcare_element: 9_000 universal_credit_pre_benefit_cap: 15_000 + housing_benefit_pre_benefit_cap: 0 output: # Excess 7,980 < childcare 9,000. uc_benefit_cap_reduction: 0 diff --git a/policyengine_uk/tests/test_non_dependant_cycle_contracts.py b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py new file mode 100644 index 0000000000..5e6418db00 --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py @@ -0,0 +1,548 @@ +"""Universal Credit and Housing Benefit when the inputs supply cap totals. + +Housing Benefit's non-dependant deductions read universal_credit, so no +baseline formula on Universal Credit's route may calculate Housing Benefit. +The Universal Credit cap counts Housing Benefit as nil (UC Regs 2013 reg +80(2A)): its basket, uc_benefit_cap_welfare_benefits, adds up the other +welfare benefits itself. Inputs that supply benefit_cap_welfare_benefits, a +total that includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, +which the basket subtracts, or supply the basket itself. + +Contract (an invariant for every population): either the inputs leave +benefit_cap_welfare_benefits to the model, or they supply +housing_benefit_pre_benefit_cap or uc_benefit_cap_welfare_benefits beside it; +otherwise the basket raises ValueError, whether or not any household holds a +non-dependant. A value a scenario stores as the model's own (set_model_value) +supplies neither input. Under the contract, each household's Universal Credit +and Housing Benefit equal its results alone. A reform that makes Universal +Credit read calculated Housing Benefit adds a cycle, and the engine raises +CycleError. + +Two independent households: a single Universal Credit claimant with £20,000 +before a £14,753 cap and no rent; and a 70-year-old paying £6,000 rent with a +40-year-old non-dependant who earns £9,000 at 20 hours and claims no +Universal Credit. Alone, the first gets £14,753 of Universal Credit, and the +council deducts £19.65 x 52 = £1,021.80 (HB Regs 2006 reg 74(2)(a), 2025-26) +from the second's rent, leaving £4,978.20 of Housing Benefit. +""" + +import copy + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.errors import CycleError +from policyengine_core.reforms import Reform +from policyengine_core.simulations import Simulation + +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.utils.supplied_inputs import set_model_value +from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( + benefit_cap_welfare_benefits as CombinedTotal, +) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_welfare_benefits import ( + uc_benefit_cap_welfare_benefits as Basket, +) +from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit_pre_benefit_cap import ( + universal_credit_pre_benefit_cap as PreCap, +) + +YEAR = 2025 +UC_MIXED = [14_753.0, 0.0, 0.0] +HB_MIXED = [0.0, 4_978.20, 0.0] +FIRST_REQUESTS = [ + "universal_credit", + "housing_benefit", + "housing_benefit_non_dep_deductions", +] +NO_AWARDS = dict( + child_benefit=0.0, + child_tax_credit=0.0, + working_tax_credit=0.0, + income_support=0.0, + jsa_income=0.0, + esa_income=0.0, + pension_credit=0.0, + uc_deductions=0.0, + uc_deduction_rate=0.0, + uc_childcare_element=0.0, +) + + +@pytest.fixture(scope="module") +def system(): + return CountryTaxBenefitSystem() + + +def situation(which="mixed", year=YEAR, full_uc=False): + """The claimant's household ("single"), the non-dependant's + ("nondep"), or both ("mixed"), with no cap total supplied.""" + people, units, households = {}, {}, {} + if which in ("single", "mixed"): + people["uc"] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=1_000.0, + weekly_hours=20.0, + ) + units["uc"] = dict( + members=["uc"], + **NO_AWARDS, + would_claim_uc=True, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=False, + ) + if not full_uc: + units["uc"]["universal_credit_pre_benefit_cap"] = 20_000.0 + households["single"] = dict( + members=["uc"], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=0.0, + tenure_type="RENT_PRIVATELY", + ) + if which in ("nondep", "mixed"): + people["applicant"] = dict(age=70, current_education="NOT_IN_EDUCATION") + people["nondep"] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=9_000.0, + weekly_hours=20.0, + ) + for name in ["applicant", "nondep"]: + units[name] = dict( + members=[name], + **NO_AWARDS, + would_claim_uc=False, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=name == "applicant", + ) + if not full_uc: + units[name]["universal_credit_pre_benefit_cap"] = 0.0 + households["nondep"] = dict( + members=["applicant", "nondep"], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=6_000.0, + tenure_type="RENT_PRIVATELY", + ) + return keyed(dict(people=people, benunits=units, households=households), year) + + +def keyed(data, year): + """Key every input by year.""" + for entities in data.values(): + for entity in entities.values(): + for name, value in entity.items(): + if name != "members" and not isinstance(value, dict): + entity[name] = {year: value} + return data + + +def supply(data, variable, values, year=YEAR): + """Supply ``variable`` for every benefit unit, from ``values`` by unit + name (nil for a unit it leaves out).""" + for name, unit in data["benunits"].items(): + unit[variable] = {year: float(values.get(name, 0.0))} + return data + + +def make(system, data, year=YEAR, trace=False): + return Simulation( + tax_benefit_system=system, + situation=copy.deepcopy(data), + default_input_period=year, + default_calculation_period=year, + trace=trace, + ) + + +def with_totals(which, **inputs): + """The population with the combined total its Universal Credit and + Housing Benefit imply supplied, and any other inputs.""" + data = situation(which) + supply(data, "benefit_cap_welfare_benefits", {"uc": 20_000.0}) + for variable, values in inputs.items(): + supply(data, variable, values) + return data + + +def traced_names(simulation): + return {key.split("<")[0] for key in simulation.tracer.get_serialized_flat_trace()} + + +@pytest.mark.parametrize("first", FIRST_REQUESTS) +def test_a_supplied_universal_credit_basket_keeps_households_independent(system, first): + """With the combined total and Universal Credit's basket supplied, each + household gets its results alone, whatever the first request.""" + basket = {"uc": 20_000.0} + alone = [ + make(system, with_totals(which, uc_benefit_cap_welfare_benefits=basket)) + for which in ["single", "nondep"] + ] + mixed = make(system, with_totals("mixed", uc_benefit_cap_welfare_benefits=basket)) + mixed.calculate(first, YEAR) + for name, expected in [ + ("universal_credit", UC_MIXED), + ("housing_benefit", HB_MIXED), + ]: + result = mixed.calculate(name, YEAR) + np.testing.assert_allclose(result, expected, atol=0.01, err_msg=name) + np.testing.assert_allclose( + result, + np.concatenate([sim.calculate(name, YEAR) for sim in alone]), + atol=0.01, + err_msg=name, + ) + np.testing.assert_allclose( + mixed.calculate("housing_benefit_non_dep_deductions", YEAR), + [0.0, 1_021.80, 0.0], + atol=0.01, + ) + + +@pytest.mark.parametrize("first", FIRST_REQUESTS) +def test_a_supplied_housing_benefit_gives_its_arithmetic(system, first): + """With the combined total and Housing Benefit supplied, the basket is + their difference, and Universal Credit and Housing Benefit follow from + it. The claimant's total of £26,000 includes £6,000 of Housing Benefit + (as in specified accommodation): the basket counts £20,000, £5,247 over + the cap, so the award comes to £14,753, and HB reg 75F(1)(g) leaves the + Housing Benefit uncapped. The applicant's total is their £4,978.20 of + Housing Benefit, and pension age puts them outside the cap.""" + data = situation("mixed") + supply( + data, + "benefit_cap_welfare_benefits", + {"uc": 26_000.0, "applicant": 4_978.20}, + ) + supply( + data, + "housing_benefit_pre_benefit_cap", + {"uc": 6_000.0, "applicant": 4_978.20}, + ) + sim = make(system, data) + sim.calculate(first, YEAR) + np.testing.assert_allclose( + sim.calculate("uc_benefit_cap_welfare_benefits", YEAR), + [20_000.0, 0.0, 0.0], + atol=0.01, + ) + np.testing.assert_allclose( + sim.calculate("universal_credit", YEAR), UC_MIXED, atol=0.01 + ) + np.testing.assert_allclose( + sim.calculate("housing_benefit", YEAR), [6_000.0, 4_978.20, 0.0], atol=0.01 + ) + # The deduction still reads the non-dependant's wages and Universal + # Credit; the supplied Housing Benefit does not read the deduction. + np.testing.assert_allclose( + sim.calculate("housing_benefit_non_dep_deductions", YEAR), + [0.0, 1_021.80, 0.0], + atol=0.01, + ) + + +@pytest.mark.parametrize("which", ["mixed", "single"]) +def test_a_supplied_total_alone_raises(system, which): + """A supplied combined total without housing_benefit_pre_benefit_cap or + uc_benefit_cap_welfare_benefits raises ValueError, with or without a + non-dependant in the population, and the message names both inputs.""" + sim = make(system, with_totals(which)) + with pytest.raises(ValueError) as error: + sim.calculate("universal_credit", YEAR) + message = str(error.value) + assert "housing_benefit_pre_benefit_cap" in message + assert "uc_benefit_cap_welfare_benefits" in message + assert "benefit_cap_welfare_benefits" in message + + +def test_a_reformed_total_leaves_universal_credit_on_its_basket(system): + """A reform that gives benefit_cap_welfare_benefits a new formula changes + the Housing Benefit cap's total, and Universal Credit's basket still adds + up the benefits itself.""" + + def formula(benunit, period, parameters): + return CombinedTotal.formula(benunit, period, parameters) + 1_000 + + class ReformedTotal(Reform): + def apply(self): + self.update_variable( + type( + "benefit_cap_welfare_benefits", + (CombinedTotal,), + dict(formula=formula), + ) + ) + + baseline = make(system, situation("mixed")) + reformed = make(ReformedTotal(system), situation("mixed")) + np.testing.assert_allclose( + reformed.calculate("benefit_cap_welfare_benefits", YEAR), + baseline.calculate("benefit_cap_welfare_benefits", YEAR) + 1_000, + atol=0.01, + ) + for name in ["uc_benefit_cap_welfare_benefits", "universal_credit"]: + np.testing.assert_allclose( + reformed.calculate(name, YEAR), + baseline.calculate(name, YEAR), + atol=0.01, + err_msg=name, + ) + np.testing.assert_allclose( + reformed.calculate("universal_credit", YEAR), UC_MIXED, atol=0.01 + ) + + +def test_model_values_supply_neither_input(system): + """set_model_value stores a value as the model's own, so it supplies + neither the combined total nor Housing Benefit. A model value of the + total leaves Universal Credit's basket adding up the benefits itself; a + supplied total with a model value of Housing Benefit still raises.""" + baseline = make(system, situation("mixed")) + basket = baseline.calculate("uc_benefit_cap_welfare_benefits", YEAR) + stored_total = make(system, situation("mixed")) + set_model_value( + stored_total, + "benefit_cap_welfare_benefits", + YEAR, + np.array([30_000.0, 0.0, 0.0]), + ) + np.testing.assert_allclose( + stored_total.calculate("uc_benefit_cap_welfare_benefits", YEAR), + basket, + atol=0.01, + ) + np.testing.assert_allclose( + stored_total.calculate("universal_credit", YEAR), UC_MIXED, atol=0.01 + ) + stored_housing_benefit = make(system, with_totals("mixed")) + set_model_value( + stored_housing_benefit, + "housing_benefit_pre_benefit_cap", + YEAR, + np.array([0.0, 4_978.20, 0.0]), + ) + with pytest.raises(ValueError, match="housing_benefit_pre_benefit_cap"): + stored_housing_benefit.calculate("universal_credit", YEAR) + + +UC_HOUSEHOLD = st.fixed_dictionaries( + dict( + kind=st.just("uc"), + universal_credit_pre_benefit_cap=st.floats(0, 40_000), + uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0]), + child_benefit=st.sampled_from([0.0, 1_500.0]), + employment_income=st.floats(0, 20_000), + ) +) +NON_DEPENDANT_HOUSEHOLD = st.fixed_dictionaries( + dict( + kind=st.just("nondep"), + rent=st.sampled_from([3_000.0, 6_000.0]), + employment_income=st.floats(0, 30_000), + weekly_hours=st.sampled_from([0.0, 20.0, 35.0]), + universal_credit_pre_benefit_cap=st.sampled_from([0.0, 5_000.0, 15_000.0]), + ) +) + + +def generated_population(households): + """Each generated household, with its Universal Credit basket and the + combined total it implies supplied for every benefit unit.""" + people, units, homes, basket = {}, {}, {}, {} + for h, household in enumerate(households): + if household["kind"] == "uc": + claimant = f"uc_{h}" + people[claimant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=household["employment_income"], + weekly_hours=20.0, + ) + units[claimant] = dict( + members=[claimant], + **{**NO_AWARDS, "child_benefit": household["child_benefit"]}, + would_claim_uc=True, + uc_benefit_cap=household["uc_benefit_cap"], + benunit_is_rent_liable=False, + universal_credit_pre_benefit_cap=household[ + "universal_credit_pre_benefit_cap" + ], + ) + basket[claimant] = ( + household["universal_credit_pre_benefit_cap"] + + household["child_benefit"] + ) + homes[f"home_{h}"] = dict( + members=[claimant], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=0.0, + tenure_type="RENT_PRIVATELY", + ) + else: + applicant, non_dependant = f"applicant_{h}", f"nondep_{h}" + on_uc = household["universal_credit_pre_benefit_cap"] > 0 + people[applicant] = dict(age=70, current_education="NOT_IN_EDUCATION") + people[non_dependant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=household["employment_income"], + weekly_hours=household["weekly_hours"], + ) + units[applicant] = dict( + members=[applicant], + **NO_AWARDS, + would_claim_uc=False, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=True, + universal_credit_pre_benefit_cap=0.0, + ) + units[non_dependant] = dict( + members=[non_dependant], + **NO_AWARDS, + would_claim_uc=on_uc, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=False, + universal_credit_pre_benefit_cap=household[ + "universal_credit_pre_benefit_cap" + ], + ) + basket[non_dependant] = household["universal_credit_pre_benefit_cap"] + homes[f"home_{h}"] = dict( + members=[applicant, non_dependant], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=household["rent"], + tenure_type="RENT_PRIVATELY", + ) + data = keyed(dict(people=people, benunits=units, households=homes), YEAR) + # The supplied units hold no Housing Benefit in the cap: the claimants + # pay no rent, and the applicants have reached pension age. + supply(data, "uc_benefit_cap_welfare_benefits", basket) + supply(data, "benefit_cap_welfare_benefits", basket) + return data + + +@settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + households=st.lists( + st.one_of(UC_HOUSEHOLD, NON_DEPENDANT_HOUSEHOLD), min_size=1, max_size=3 + ) +) +def test_each_household_gets_its_results_alone(system, households): + """For any mix of independent households, with Universal Credit's basket + supplied, each household's Universal Credit, Housing Benefit and + non-dependant deductions equal its results alone.""" + mixed = make(system, generated_population(households)) + alone = [ + make(system, generated_population([household])) for household in households + ] + for name in [ + "universal_credit", + "housing_benefit", + "housing_benefit_non_dep_deductions", + ]: + np.testing.assert_allclose( + mixed.calculate(name, YEAR), + np.concatenate([sim.calculate(name, YEAR) for sim in alone]), + atol=0.01, + err_msg=name, + ) + + +def test_computed_universal_credit_and_local_schemes(system): + """With nothing supplied, Universal Credit's traced calculation reads no + Housing Benefit variable but the reported award, and the national and + local Council Tax Reduction schemes then read Universal Credit.""" + # Each local scheme's parameters start in 2026-27. + year = 2026 + template = situation("mixed", year=year, full_uc=True) + template["people"]["uc"]["housing_benefit_reported"] = {year: 6_000.0} + data = {group: {} for group in template} + for authority in [ + "MERTON", + "KINGSTON_UPON_THAMES", + "NEWHAM", + "WESTMINSTER", + "OXFORD", + ]: + prefix = authority.lower() + "_" + for group, entities in template.items(): + for name, entity in entities.items(): + copied = copy.deepcopy(entity) + if "members" in copied: + copied["members"] = [ + prefix + member for member in copied["members"] + ] + if group == "benunits": + # A reported Housing Benefit award otherwise makes + # claims_all_entitled_benefits false, so this family + # would not claim CTR by default. + copied["would_claim_council_tax_reduction"] = {year: True} + if group == "households": + copied["local_authority"] = {year: authority} + copied["council_tax"] = {year: 2_000.0} + data[group][prefix + name] = copied + sim = make(system, data, year=year, trace=True) + universal_credit = sim.calculate("universal_credit", year) + names = traced_names(sim) + assert universal_credit[0] > 0 + assert "uc_maximum_amount" in names + assert "housing_benefit_reported" in names + assert not { + name + for name in names + if name.startswith("housing_benefit") and name != "housing_benefit_reported" + } + for name in [ + "housing_benefit", + "council_tax_reduction", + "merton_council_tax_reduction", + "kingston_upon_thames_council_tax_reduction", + "newham_council_tax_reduction", + "westminster_council_tax_reduction", + "oxford_council_tax_reduction", + ]: + values = sim.calculate(name, year) + if name.endswith("_council_tax_reduction"): + assert values.max() > 0, (name, values) + + +@pytest.mark.parametrize( + "variable,parent", + [ + ("uc_benefit_cap_welfare_benefits", Basket), + ("universal_credit_pre_benefit_cap", PreCap), + ], +) +def test_a_reform_reading_calculated_housing_benefit_raises_cycle_error( + system, variable, parent +): + """A reform that adds calculated Housing Benefit to Universal Credit's + basket or award makes Universal Credit and the deductions depend on each + other, and the engine raises CycleError.""" + + def formula(benunit, period, parameters): + return parent.formula(benunit, period, parameters) + benunit( + "housing_benefit", period + ) + + replacement = type(variable, (parent,), dict(formula=formula)) + + class CyclicReform(Reform): + def apply(self): + self.update_variable(replacement) + + data = situation("mixed") + for unit in data["benunits"].values(): + unit.pop(variable, None) + with pytest.raises(CycleError): + make(CyclicReform(system), data).calculate("housing_benefit", YEAR) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 7d2acc30fd..be63ac1f6e 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -68,9 +68,12 @@ income and that award. The test skips incomes within a penny a week of an edge, since the inputs hold float32 values. 12. The Universal Credit cap counts the Housing Benefit cap's welfare - benefits other than Housing Benefit (UC Regs 2013 reg 80(2A)), so - universal_credit reads no Housing Benefit unless the inputs supply - benefit_cap_welfare_benefits. A non-dependant family's gross income + benefits other than Housing Benefit (UC Regs 2013 reg 80(2A)), and no + baseline formula on universal_credit's route calculates Housing Benefit: + its traced calculation reads no Housing Benefit variable but the reported + award. Inputs that supply benefit_cap_welfare_benefits, a total that + includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, + or the basket raises ValueError. A non-dependant family's gross income counts its award after the cap and before debt deductions: universal_credit plus uc_deductions, which equals the award before the cap less the cap reduction applied, under current law and under an 85% protected minimum @@ -78,9 +81,9 @@ universal_credit, each household's deductions equal those of the same household with the reformed award supplied, and the Housing Benefit deduction is the band of wages plus that award, so it moves when the - award crosses a band. With a supplied total, a calculated Housing - Benefit and a non-dependant on Universal Credit, the engine raises - CycleError. + award crosses a band. (test_non_dependant_cycle_contracts.py checks the + supplied-total contract over mixed populations, and that a reform making + Universal Credit read calculated Housing Benefit raises CycleError.) 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 @@ -1132,19 +1135,16 @@ def award_households(households, awards=None, scaled=True): ) -def test_universal_credit_reads_housing_benefit_only_in_a_supplied_total(): - """Invariant 12: Universal Credit's calculation reads no Housing Benefit, - so the deductions can read universal_credit. A supplied - benefit_cap_welfare_benefits includes Housing Benefit, so Universal - Credit takes out housing_benefit_pre_benefit_cap. Where the simulation - calculates that Housing Benefit for a household with a non-dependant on - Universal Credit, the award and the deduction depend on each other, and - the engine raises.""" - from policyengine_core.errors import CycleError - +def test_universal_credit_reads_no_calculated_housing_benefit(): + """Invariant 12: Universal Credit's traced calculation reads no Housing + Benefit variable but the reported award, so the deductions can read + universal_credit. Inputs that supply benefit_cap_welfare_benefits, a + total that includes Housing Benefit, also supply + housing_benefit_pre_benefit_cap, and Universal Credit reads that input + without calculating Housing Benefit. A supplied total without it raises + ValueError.""" year = 2025 - households = [BAND_CROSSING] - situation = award_households(households, scaled=False) + situation = award_households([BAND_CROSSING], scaled=False) sim = core_simulation(situation, year, trace=True) sim.calculate("universal_credit", year) read = {key.split("<")[0] for key in sim.tracer.get_serialized_flat_trace()} @@ -1158,8 +1158,19 @@ def test_universal_credit_reads_housing_benefit_only_in_a_supplied_total(): unit["benefit_cap_welfare_benefits"] = { str(year): unit["universal_credit_pre_benefit_cap"][str(year)] } - with pytest.raises(CycleError): + with pytest.raises(ValueError, match="housing_benefit_pre_benefit_cap"): core_simulation(situation, year).calculate("housing_benefit", year) + for unit in situation["benunits"].values(): + unit["housing_benefit_pre_benefit_cap"] = {str(year): 0.0} + sim = core_simulation(situation, year, trace=True) + sim.calculate("universal_credit", year) + read = {key.split("<")[0] for key in sim.tracer.get_serialized_flat_trace()} + assert not [ + name + for name in read + if name.startswith("housing_benefit") + and name not in ("housing_benefit_reported", "housing_benefit_pre_benefit_cap") + ], read @PROPERTY_SETTINGS diff --git a/policyengine_uk/tests/test_non_dependant_population_consistent_total.py b/policyengine_uk/tests/test_non_dependant_population_consistent_total.py new file mode 100644 index 0000000000..074d84ad24 --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_population_consistent_total.py @@ -0,0 +1,104 @@ +"""Supplied cap totals that agree with each household's own calculation. + +Each household's combined total and Housing Benefit come from its own +simulation with nothing supplied. Supplying both for a population of those +households gives every household the results it gets alone, whatever the +first request asks for. Supplying the totals without the Housing Benefit +raises ValueError (test_non_dependant_cycle_contracts.py states the +contract). +""" + +import numpy as np +import pytest + +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.tests.test_non_dependant_cycle_contracts import ( + FIRST_REQUESTS, + YEAR, + make, + situation, + supply, +) + +HOUSEHOLDS = ["single", "nondep"] + + +@pytest.fixture(scope="module") +def system(): + return CountryTaxBenefitSystem() + + +@pytest.fixture(scope="module") +def standalone(system): + """Each household's own totals and results, with nothing supplied.""" + results = {} + for which in HOUSEHOLDS: + sim = make(system, situation(which)) + units = list(situation(which)["benunits"]) + results[which] = { + name: dict(zip(units, sim.calculate(name, YEAR).tolist())) + for name in [ + "benefit_cap_welfare_benefits", + "housing_benefit_pre_benefit_cap", + "universal_credit", + "housing_benefit", + ] + } + return results + + +def consistent(standalone, *variables): + """The mixed population with ``variables`` supplied at each household's + own values.""" + data = situation("mixed") + for variable in variables: + values = {} + for which in HOUSEHOLDS: + values.update(standalone[which][variable]) + supply(data, variable, values) + return data + + +def expected(standalone, name): + return [value for which in HOUSEHOLDS for value in standalone[which][name].values()] + + +def test_each_household_alone(standalone): + # The claimant pays no rent, so the total is the £20,000 of Universal + # Credit; the applicant's total is their own Housing Benefit, before a + # cap that pension age keeps away. + assert expected(standalone, "benefit_cap_welfare_benefits") == pytest.approx( + [20_000.0, 4_978.20, 0.0], abs=0.01 + ) + assert expected(standalone, "housing_benefit_pre_benefit_cap") == pytest.approx( + [0.0, 4_978.20, 0.0], abs=0.01 + ) + assert expected(standalone, "universal_credit") == pytest.approx( + [14_753.0, 0.0, 0.0], abs=0.01 + ) + + +@pytest.mark.parametrize("first", FIRST_REQUESTS) +def test_consistent_totals_keep_households_independent(system, standalone, first): + sim = make( + system, + consistent( + standalone, + "benefit_cap_welfare_benefits", + "housing_benefit_pre_benefit_cap", + ), + ) + sim.calculate(first, YEAR) + for name in ["universal_credit", "housing_benefit"]: + np.testing.assert_allclose( + sim.calculate(name, YEAR), + expected(standalone, name), + atol=0.01, + err_msg=name, + ) + + +def test_consistent_totals_without_housing_benefit_raise(system, standalone): + sim = make(system, consistent(standalone, "benefit_cap_welfare_benefits")) + with pytest.raises(ValueError, match="housing_benefit_pre_benefit_cap"): + sim.calculate("housing_benefit", YEAR) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index 21de4a9efc..b2a2b8f49a 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -43,8 +43,8 @@ def benefit_cap_couple(benunit, period): count this couple's welfare benefits (benefit_cap_welfare_benefits, and uc_benefit_cap_welfare_benefits without Housing Benefit). Universal Credit alone caps a family receiving both awards (UC Regs 2013 reg 80(2A); HB - Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two couples are the same unless - is_uc_claimant is entered. + Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two + couples are the same unless is_uc_claimant is entered. """ person = benunit.members on_universal_credit = benunit("universal_credit_pre_benefit_cap", period) > 0 diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index 5dec9471f1..a6b71589fd 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -68,6 +68,7 @@ "housing_benefit_applicable_income_childcare_element", "housing_benefit_applicable_income_disregard", "housing_benefit_childcare_earnings_limit", + "housing_benefit_pre_benefit_cap", "housing_benefit_specified_or_temporary_accommodation_disregard", "in_HE", "other_local_bus_trips", diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index 3471ff1c1e..8e27281a25 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -57,12 +57,18 @@ class benefit_cap_welfare_benefits(Variable): "another member of the benefit unit claims in their own right do not " "count. The total includes Universal Credit and Housing Benefit " "before the cap (universal_credit_pre_benefit_cap, " - "housing_benefit_pre_benefit_cap). Universal Credit counts Housing " - "Benefit as nil (UC Regs 2013 reg. 80(2A)), so " - "uc_benefit_cap_reduction subtracts it; an entered total should " - "include it. Widowed parent's allowance counts; maternity allowance " - "and the other bereavement benefits, also welfare benefits, are not " - "modelled here." + "housing_benefit_pre_benefit_cap). Universal Credit's cap reads " + "uc_benefit_cap_welfare_benefits, which totals the same benefits " + "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Inputs that " + "supply this total include Housing Benefit in it, and also supply " + "housing_benefit_pre_benefit_cap, which " + "uc_benefit_cap_welfare_benefits subtracts, or supply " + "uc_benefit_cap_welfare_benefits itself; otherwise Universal Credit's " + "cap raises ValueError. A reform of this variable changes the Housing " + "Benefit cap. To change the Universal Credit cap's basket, replace " + "uc_benefit_cap_welfare_benefits. Widowed parent's allowance counts; " + "maternity allowance and the other bereavement benefits, also welfare " + "benefits, are not modelled here." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 9c390ed462..e6480cfb82 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -55,9 +55,7 @@ def formula(benunit, period, parameters): p.great_britain, ) # Read Universal Credit before the benefit cap and deductions: - # entitlement counts whether or not DWP pays it. The amount paid - # (universal_credit) reads Housing Benefit where the inputs supply - # benefit_cap_welfare_benefits (uc_benefit_cap_welfare_benefits). + # entitlement counts whether or not DWP pays it. on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb on_legacy_benefit = benunit( "in_receipt_of_income_support_jsa_ib_or_esa_ir", period 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 4061946eaa..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 @@ -41,13 +41,6 @@ def formula(benunit, period, parameters): non_dependant = ( person("is_non_dependant_of_household_head", period) & ~joint_occupier ) - # With no non-dependant the council deducts nothing, so the formula - # reads no income. Universal Credit reads Housing Benefit where the - # inputs supply benefit_cap_welfare_benefits - # (uc_benefit_cap_welfare_benefits); a household of one family can - # then still calculate both. - if not non_dependant.any(): - return benunit.empty_array() deductions = ( person("household_benefits_individual_non_dep_deduction", period) * non_dependant diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py index bb60e1a70f..821e08e13d 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -1,7 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.benefit_cap import benefit_cap_couple from policyengine_uk.utils.benefit_unit import award_of_couple -from policyengine_uk.utils.supplied_inputs import stored_as_input +from policyengine_uk.utils.supplied_inputs import supplied_input from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( CLAIMANT_OR_PARTNER_BENEFITS, FAMILY_BENEFITS, @@ -23,6 +23,18 @@ ] +def supplied_total_without_housing_benefit(period) -> str: + return ( + f"The inputs supply benefit_cap_welfare_benefits for {period}, a total " + "that includes Housing Benefit, which the Universal Credit cap counts " + "as nil (UC Regs 2013 reg. 80(2A)). Supply " + "housing_benefit_pre_benefit_cap for the same period, for the cap to " + "subtract, or supply uc_benefit_cap_welfare_benefits. The model doesn't " + "calculate that Housing Benefit for Universal Credit, because the " + "Housing Benefit non-dependant deductions read universal_credit." + ) + + class uc_benefit_cap_welfare_benefits(Variable): value_type = float entity = BenUnit @@ -30,12 +42,14 @@ class uc_benefit_cap_welfare_benefits(Variable): documentation = ( "The welfare benefits the Universal Credit cap counts: those in " "benefit_cap_welfare_benefits, for the same single person or couple, " - "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Universal " - "Credit then reads no Housing Benefit, so the Housing Benefit " - "non-dependant deductions can read universal_credit. Where the " + "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Where the " "inputs supply benefit_cap_welfare_benefits, whose total includes " - "Housing Benefit, this subtracts housing_benefit_pre_benefit_cap " - "from it, and Universal Credit reads Housing Benefit." + "Housing Benefit, this subtracts the supplied " + "housing_benefit_pre_benefit_cap. Inputs that supply the combined " + "total also supply housing_benefit_pre_benefit_cap or this variable; " + "otherwise the formula raises ValueError. No baseline formula on " + "Universal Credit's route calculates Housing Benefit, so the Housing " + "Benefit non-dependant deductions can read universal_credit." ) definition_period = YEAR unit = GBP @@ -46,7 +60,17 @@ class uc_benefit_cap_welfare_benefits(Variable): ) def formula(benunit, period, parameters): - if stored_as_input(benunit, "benefit_cap_welfare_benefits", period): + # A supplied combined total includes Housing Benefit, and the inputs + # supply that Housing Benefit too. Calculating it here would make + # Universal Credit depend on Housing Benefit, whose non-dependant + # deductions read universal_credit. A value a scenario stores as the + # model's own (set_model_value) supplies neither. + if supplied_input(benunit, "benefit_cap_welfare_benefits", period) is not None: + if ( + supplied_input(benunit, "housing_benefit_pre_benefit_cap", period) + is None + ): + raise ValueError(supplied_total_without_housing_benefit(period)) return benunit("benefit_cap_welfare_benefits", period) - benunit( "housing_benefit_pre_benefit_cap", period ) From 1bbd87d7ebf71b929c2b89ae42a8de2c4461cd6d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 15:57:52 -0500 Subject: [PATCH 38/43] Say who deducts in the non-dependant prose The council deducts the amounts from the claimant's award; the non-dependant pays nothing. Comments, documentation strings and test docstrings now name the council. Each YAML file parses to the same cases. Co-Authored-By: Claude Opus 5.5 --- ...benefits_individual_non_dep_deduction.yaml | 12 +++++----- ...using_benefit_non_dep_deduction_chain.yaml | 4 ++-- .../housing_benefit_non_dep_deductions.yaml | 8 +++---- ...ing_benefit_non_dep_remunerative_work.yaml | 2 +- .../non_dependant_deduction_review_cases.yaml | 22 ++++++++--------- .../claimant_family_scheme.yaml | 4 ++-- ...eduction_individual_non_dep_deduction.yaml | 4 ++-- ...ncil_tax_reduction_non_dep_deductions.yaml | 6 ++--- ...tion_non_dep_deductions_mixed_schemes.yaml | 2 +- ...il_tax_reduction_non_dep_gross_income.yaml | 4 ++-- ...x_reduction_non_dep_remunerative_work.yaml | 4 ++-- .../council_tax_reduction_pension_credit.yaml | 4 ++-- ...test_non_dependant_deduction_properties.py | 24 +++++++++---------- ...sharers_boarders_and_lodgers_properties.py | 4 ++-- .../non_dep_deduction/_non_dependants.py | 16 ++++++------- ...d_benefits_individual_non_dep_deduction.py | 12 +++++----- ...ousing_benefit_non_dep_deduction_exempt.py | 6 ++--- .../housing_benefit_non_dep_deductions.py | 8 +++---- ...efit_non_dep_deductions_claimant_exempt.py | 2 +- ...duction_applicant_has_non_dep_exemption.py | 2 +- ..._reduction_individual_non_dep_deduction.py | 10 ++++---- 21 files changed, 80 insertions(+), 80 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 6ecfaa2b49..863c91d88a 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 @@ -216,8 +216,8 @@ 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)). + # reg 74(4): joint income 400 a week is in the 365 band; the council would + # deduct 20.40 for the partner outside remunerative work (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 @@ -277,8 +277,8 @@ 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. + # reg 6(6): the council treats a person on income-related ESA as outside + # remunerative work and deducts 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 @@ -310,8 +310,8 @@ 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). + # under the 192 band (20.40); for the young person, under 16 hours, the council deducts 20.40 + # unless they are exempt (eligibility is an input here). household_benefits_individual_non_dep_deduction: [1_060.80, 1_060.80] # 2025-26 bands and deductions (HB Regs 2006 reg 74 as up-rated by SI 2025/295 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 index 0b510e985c..f3f5835c1d 100644 --- 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 @@ -3,8 +3,8 @@ # 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). + # A full-time student lives with the claimant; HB Regs 2006 + # reg 74(7)(c)-(d) exempts the student from a deduction. period: 2025 absolute_error_margin: 0.01 input: 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 8b11ac331d..88b1a2db3e 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 @@ -170,8 +170,8 @@ # 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 -# reg 74(2)), so the adult child pays the reg 74(1)(b) amount, as one with no -# income would. +# reg 74(2)), so the council deducts the reg 74(1)(b) amount for the adult child, +# as it would for one with no income. - name: A non-dependant's investment income does not raise the Housing Benefit deduction period: 2025 absolute_error_margin: 0.01 @@ -250,7 +250,7 @@ # (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)). + # (reg 74(3)). The son works 2 hours, so the council deducts 20.40 for him (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 @@ -305,7 +305,7 @@ # 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 + # deduction for the couple. The son does no work, so the council deducts 20.40 for him (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 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 7d0dde3004..f6bde9e1e8 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,6 +1,6 @@ # 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 pays 20.40 * 52 = 1,060.80 in 2026-27, +# For a non-worker or 15-hour worker, the council deducts 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 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 4e51f02aab..633c7c7dfb 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 @@ -240,8 +240,8 @@ 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)). + # DWP deducts relievable pension contributions from employed earnings + # (UC Regs 2013 reg 55(5)(a)), 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] @@ -282,8 +282,8 @@ 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. + # DWP treats a person to whom the floor applies as having earned income + # equal to it (UC Regs 2013 reg 62(2)). 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] @@ -704,8 +704,8 @@ 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. + # exemption applies (HB reg 74(8)(b); CTR para 8(8)(c)). The lodger does no + # remunerative work, so the council deducts 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] @@ -807,7 +807,7 @@ # 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. + # the council deducts one lowest amount for the couple, 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] @@ -858,7 +858,7 @@ 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 + # support" is exempt. For the partner outside remunerative work, the council deducts 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] @@ -999,9 +999,9 @@ 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). + # s.1(1)). With no claim, the council deducts for both: the parent's 200 a week is + # under the CTR 279 band (5.20), and for the young person outside remunerative + # work, the council deducts 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/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml index cece32c759..7dc97e3c1e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml @@ -251,8 +251,8 @@ # The adult child's own scheme (Maidstone's working-age one) is not # simulated, but the household's bill is: the head's pensioner claim # covers all of it, less the non-dependant deduction for the adult child, - # who is not in remunerative work, not a student and has no award, so - # pays the lowest amount (SI 2012/2885 Sch 1 para 8(1)(b): 5.20 * 52 = + # who is not in remunerative work, not a student and has no award. The + # council deducts the lowest amount (SI 2012/2885 Sch 1 para 8(1)(b): 5.20 * 52 = # 270.40). The child's report cannot be added on top. council_tax_reduction_scheme_supported: [true, false] council_tax_reduction_household_has_simulated_claim: true diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml index e013c7b3ac..d96e582366 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml @@ -1,7 +1,7 @@ # The national Council Tax Reduction non-dependant deduction bands normal # gross weekly income, including investment income, but only for a -# non-dependant in remunerative work (SI 2012/2885 Sch 1 para 8(1)-(2)); one -# not in remunerative work pays the lowest amount whatever their income. The +# non-dependant in remunerative work (SI 2012/2885 Sch 1 para 8(1)-(2)); the council +# deducts the lowest amount for one outside remunerative work whatever their income. The # England pensioner scheme's 2025-26 scale (SI 2025/39): under £266 a week # 5.00; £266 10.20; £463 12.80; £577 and above 15.35. 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 abf7d73ac4..71c342a701 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 @@ -5,7 +5,7 @@ # 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 council deducts the lowest amount for a non-dependant outside remunerative work (under 16 hours). # The applicant is the benefit unit with the household's oldest member, so # working-age applicants here are 60. @@ -1161,8 +1161,8 @@ 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. + # para 8(3)-(4): joint 300 a week is in the 279 band (10.60); for the partner + # outside remunerative work the council would deduct 5.20; it deducts only the higher. council_tax_reduction_individual_non_dep_deduction: [0, 551.20, 270.40] council_tax_reduction_non_dep_deductions: [551.20, 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 index 171fac905c..f2f1382d1d 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 @@ -4,7 +4,7 @@ # 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. +# band 10.95. - name: England, working-age head and pension-age sharer, deduction from the sharer's pensioner claim period: 2026 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 b9a056f778..064cdf59f2 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 @@ -5,8 +5,8 @@ # maintenance; the listed disability payments are disregarded. # Hand arithmetic: wages 9,000 + statutory pay 520 = 9,520, just above # 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 +# the first band's lower edge, so the council deducts the scheme's lowest +# amount for the non-dependant: 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)). 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 b660578932..c3dd3a9753 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,8 +1,8 @@ # 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. 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 +# work. Under each scheme's 2026-27 scale, the council deducts the lowest amount +# for a non-dependant outside remunerative work (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). The cases hold the family's awards, which a single 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 336f49fa0d..1a4f081d36 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 @@ -251,8 +251,8 @@ guarantee_credit: [3_376, 0] in_receipt_of_guarantee_credit: [true, false] council_tax_reduction_applicable_income: [0, 5_000] - # 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. + # SI 2012/2885 Sch 1 para 8(1)(b): the son has no hours, so the council deducts + # the 2026-27 lowest amount, 5.20 a week, for him outside remunerative work. council_tax_reduction_non_dep_deductions: [270.40, 0] # 2,000 - 270.40 simulated_council_tax_reduction_benunit: [1_729.60, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index be63ac1f6e..b2c29e3787 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -13,12 +13,12 @@ 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 or, for Housing - Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)) pays the lowest amount, - whatever their income. +2. Bounds: for an eligible non-dependant who is not exempt, the council + deducts between the scale's lowest and highest weekly amounts. In English + working-age households (local schemes) the national CTR deduction is 0. +3. For a non-dependant outside remunerative work (under 16 hours or, for Housing + Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)), the council deducts the lowest + amount whatever their income. 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. @@ -56,10 +56,10 @@ 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 - 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 +11. Banding: for a non-dependant in remunerative work who is neither exempt nor + on IS, JSA(IB) or ESA(IR), the council deducts 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) and Pension Credit, and its @@ -1291,8 +1291,8 @@ def test_deduction_follows_a_reformed_award(households): 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 + """Invariant 13: compare the Universal Credit exemption's earned-income test + with 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): 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 0162003f6b..56150a0845 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 @@ -290,8 +290,8 @@ def test_non_dependants(population): ) hb_total = sim.map_result(hb_individual, "person", "household") assert hb_total.max() > 0 - # 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 + # The council deducts once for each non-dependant couple, the higher amount + # (HB Regs 2006 reg 74(3)), and once 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"] 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 565738d075..c50885abd1 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 @@ -20,16 +20,16 @@ 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, + """Calculate normal weekly gross income for the deduction bands. The council + bands a claimant or partner 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, child benefit, Income Support, income-based JSA, income-related ESA and Pension Credit, and its Universal Credit award (universal_credit_award) - 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.""" + where either member claims it. The council bands each other member of the + benefit unit separately on their own income, adding Universal Credit + where that member claims it alone. 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"] 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 d9389129d4..162816d04f 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 @@ -10,12 +10,12 @@ class household_benefits_individual_non_dep_deduction(Variable): entity = Person label = "Housing Benefit individual non-dependent deduction" documentation = ( - "A non-dependant in remunerative work pays the deduction for the band " + "For a non-dependant in remunerative work, the council deducts the amount for the band " "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, " + "include their lower edge. For a non-dependant outside remunerative work, " "including one on Income Support, income-based JSA or income-related " - "ESA (reg 6(6)), pays the lowest amount." + "ESA (reg 6(6)), the council deducts the lowest amount." ) definition_period = YEAR unit = GBP @@ -38,9 +38,9 @@ def formula(person, period, parameters): person("weekly_hours", period) >= p.remunerative_work_hours ) & ~is_on_legacy_income_related_benefit(person, period) # Reg 74(2), or pension-age reg 55(2): each band runs from "not less - # than" its lower edge. Reg 74(1)(b), or reg 55(1)(b): a non-dependant - # outside remunerative work pays the scale's lowest amount, whatever - # their income. + # than" its lower edge. Reg 74(1)(b), or reg 55(1)(b): for a non-dependant + # outside remunerative work the council deducts the scale's lowest + # amount whatever their income. 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 9ed6cf1730..f653095d4a 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 @@ -37,9 +37,9 @@ def formula(person, period, parameters): 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. The award is the claimants' it - # assesses. + # Read entitlement before the cap and debt deductions for the claimants + # the award assesses. DWP can reduce the payment to nil while + # entitlement still qualifies for the exemption. entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 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 40710ed628..85002fb197 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 @@ -25,12 +25,12 @@ class housing_benefit_non_dep_deductions(Variable): ) def formula(benunit, period, parameters): - # Deductions are made for non-dependants residing with the claimant + # The council deducts for non-dependants residing with the claimant # (HB Regs 2006 reg 74; HB (SPC) Regs 2006 reg 55). Joint occupiers, # boarders, lodgers and the landlord's household are not - # non-dependants (reg 3(2)(d)-(e), 3(4)). A couple pays one deduction, - # the higher (reg 74(3)). A non-dependant of more than one joint - # occupier is apportioned between them by their shares of the + # non-dependants (reg 3(2)(d)-(e), 3(4)). For a couple the council deducts once, + # the higher amount (reg 74(3)). For a non-dependant of more than one joint + # occupier, the council apportions the deduction 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 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 5b612d8181..b8ed5eb93f 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 @@ -6,7 +6,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): entity = BenUnit label = "Exempt from all Housing Benefit non-dependant deductions" documentation = ( - "No deduction is made for any non-dependant if the claimant or partner " + "The council makes no deduction 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 " diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py index dde5d3fc01..f2483e0bbf 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py @@ -6,7 +6,7 @@ class council_tax_reduction_applicant_has_non_dep_exemption(Variable): entity = BenUnit label = "No non-dependant deductions from this family's Council Tax Reduction" documentation = ( - "Whether no deduction is made for any non-dependant from this family's " + "Whether the council makes no deduction for any non-dependant from this family's " "Council Tax Reduction because the applicant or partner is blind, or " "receives in respect of themselves Attendance Allowance, the care " "component of Disability Living Allowance, the daily living component " 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 90e46a9208..0956c62437 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 @@ -11,14 +11,14 @@ class council_tax_reduction_individual_non_dep_deduction(Variable): 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 " + "For a non-dependant in remunerative work, the council deducts the amount for the band " "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, " + "include their lower edge. For a non-dependant outside remunerative work, " "including one on Income Support, income-based JSA or income-related " - "ESA, pays the lowest amount; an exempt non-dependant pays nothing. English working-age local schemes have their own " - "variables. Reported under the scheme of the household head's family; " - "each applicant's deductions use its own scheme." + "ESA, the council deducts the lowest amount; for an exempt non-dependant it deducts nothing. English working-age local schemes have their own " + "variables. The person-level variable reports the scheme of the household head's family; " + "each applicant's deductions follow their own scheme." ) definition_period = YEAR unit = GBP From 922d0bd09236df494c34755d7d6251425826552c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 16:23:39 -0500 Subject: [PATCH 39/43] Name the supplied-total contract in the changelog 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 13ce85cfd4..47f0f18c41 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). +The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). Inputs that supply the combined total, `benefit_cap_welfare_benefits`, also supply `housing_benefit_pre_benefit_cap` or `uc_benefit_cap_welfare_benefits`; otherwise the Universal Credit cap raises `ValueError`. From 95fbf301495e4d39d4a6cd4991f060dbf6b17fa3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 17:39:21 -0500 Subject: [PATCH 40/43] Name the actors in the supplied-total docstrings Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_cycle_contracts.py | 15 ++++++++------- .../gov/dwp/benefit_cap_welfare_benefits.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_cycle_contracts.py b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py index 5e6418db00..7be5feccd5 100644 --- a/policyengine_uk/tests/test_non_dependant_cycle_contracts.py +++ b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py @@ -8,7 +8,7 @@ total that includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, which the basket subtracts, or supply the basket itself. -Contract (an invariant for every population): either the inputs leave +The contract holds for every population: either the inputs leave benefit_cap_welfare_benefits to the model, or they supply housing_benefit_pre_benefit_cap or uc_benefit_cap_welfare_benefits beside it; otherwise the basket raises ValueError, whether or not any household holds a @@ -18,12 +18,13 @@ Credit read calculated Housing Benefit adds a cycle, and the engine raises CycleError. -Two independent households: a single Universal Credit claimant with £20,000 -before a £14,753 cap and no rent; and a 70-year-old paying £6,000 rent with a -40-year-old non-dependant who earns £9,000 at 20 hours and claims no -Universal Credit. Alone, the first gets £14,753 of Universal Credit, and the -council deducts £19.65 x 52 = £1,021.80 (HB Regs 2006 reg 74(2)(a), 2025-26) -from the second's rent, leaving £4,978.20 of Housing Benefit. +The tests use two independent households: a single Universal Credit +claimant with £20,000 before a £14,753 cap and no rent; and a 70-year-old +paying £6,000 rent with a 40-year-old non-dependant who earns £9,000 at 20 +hours and claims no Universal Credit. Alone, the first gets £14,753 of +Universal Credit. For the second's non-dependant the council deducts +£19.65 x 52 = £1,021.80 (HB Regs 2006 regs 70 and 74(2)(a), 2025-26), +leaving £4,978.20 of Housing Benefit. """ import copy diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index 8e27281a25..44ffe44b1e 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -60,7 +60,7 @@ class benefit_cap_welfare_benefits(Variable): "housing_benefit_pre_benefit_cap). Universal Credit's cap reads " "uc_benefit_cap_welfare_benefits, which totals the same benefits " "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Inputs that " - "supply this total include Housing Benefit in it, and also supply " + "supply this total include Housing Benefit in it. They also supply " "housing_benefit_pre_benefit_cap, which " "uc_benefit_cap_welfare_benefits subtracts, or supply " "uc_benefit_cap_welfare_benefits itself; otherwise Universal Credit's " From 2667976b828e2b622ebc4360971892609d0bca3a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 03:31:38 -0500 Subject: [PATCH 41/43] Release the scaled award system's cache after the module test_test_module_caches_released fails on a module-level cache that no module-scoped fixture clears. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_deduction_properties.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index b2c29e3787..754f213b26 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -1067,6 +1067,14 @@ def apply(self): return ScaledAward(SYSTEM) +@pytest.fixture(scope="module", autouse=True) +def release_scaled_award_system(): + # The system also keeps its last simulation alive. Drop it once this + # module's tests are done, instead of for the rest of the test process. + yield + scaled_award_system.cache_clear() + + AWARD_YEAR = 2025 AWARD_HOUSEHOLD = st.fixed_dictionaries( dict( From c2c55086e27507567190a664d2e0e84e42d24981 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 14:50:59 +0000 Subject: [PATCH 42/43] State the per-benefit-unit contract for a supplied cap total Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JPH2P5XFEgwSLLAb1 --- changelog.d/2004.fixed.md | 2 +- ...test_non_dependant_deduction_properties.py | 16 +++++++------- policyengine_uk/utils/benefit_cap.py | 2 +- .../gov/dwp/benefit_cap_welfare_benefits.py | 16 +++++++------- .../uc_benefit_cap_welfare_benefits.py | 21 ++++++++++++------- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 47f0f18c41..2087926885 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). Inputs that supply the combined total, `benefit_cap_welfare_benefits`, also supply `housing_benefit_pre_benefit_cap` or `uc_benefit_cap_welfare_benefits`; otherwise the Universal Credit cap raises `ValueError`. +The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). A caller who supplies the combined total, `benefit_cap_welfare_benefits`, must also supply `housing_benefit_pre_benefit_cap` or `uc_benefit_cap_welfare_benefits` for every benefit unit; otherwise the model raises `ValueError` when it calculates the Universal Credit cap. diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 754f213b26..0be4a4ea89 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -72,8 +72,8 @@ baseline formula on universal_credit's route calculates Housing Benefit: its traced calculation reads no Housing Benefit variable but the reported award. Inputs that supply benefit_cap_welfare_benefits, a total that - includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, - or the basket raises ValueError. A non-dependant family's gross income + includes Housing Benefit, also supply housing_benefit_pre_benefit_cap or + uc_benefit_cap_welfare_benefits, or the basket raises ValueError. A non-dependant family's gross income counts its award after the cap and before debt deductions: universal_credit plus uc_deductions, which equals the award before the cap less the cap reduction applied, under current law and under an 85% protected minimum @@ -1069,8 +1069,9 @@ def apply(self): @pytest.fixture(scope="module", autouse=True) def release_scaled_award_system(): - # The system also keeps its last simulation alive. Drop it once this - # module's tests are done, instead of for the rest of the test process. + # The system also keeps its last simulation alive. Clear the cache after + # this module's last test, so the system doesn't stay in memory for the + # rest of the test process. yield scaled_award_system.cache_clear() @@ -1148,9 +1149,10 @@ def test_universal_credit_reads_no_calculated_housing_benefit(): Benefit variable but the reported award, so the deductions can read universal_credit. Inputs that supply benefit_cap_welfare_benefits, a total that includes Housing Benefit, also supply - housing_benefit_pre_benefit_cap, and Universal Credit reads that input - without calculating Housing Benefit. A supplied total without it raises - ValueError.""" + housing_benefit_pre_benefit_cap or uc_benefit_cap_welfare_benefits, or + the basket raises ValueError. Where they supply + housing_benefit_pre_benefit_cap, Universal Credit reads that input + without calculating Housing Benefit.""" year = 2025 situation = award_households([BAND_CROSSING], scaled=False) sim = core_simulation(situation, year, trace=True) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index b2a2b8f49a..e91e2fa09c 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -44,7 +44,7 @@ def benefit_cap_couple(benunit, period): uc_benefit_cap_welfare_benefits without Housing Benefit). Universal Credit alone caps a family receiving both awards (UC Regs 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two - couples are the same unless is_uc_claimant is entered. + couples match unless the inputs set is_uc_claimant. """ person = benunit.members on_universal_credit = benunit("universal_credit_pre_benefit_cap", period) > 0 diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index 44ffe44b1e..3533f15608 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -53,7 +53,7 @@ class benefit_cap_welfare_benefits(Variable): "is its claimants (is_uc_assessed_claimant), including the other " "member of a couple where one member claims as a single person; " "otherwise it is the claimant and partner (is_claimant_or_partner). " - "The two are the same unless is_uc_claimant is entered. Benefits " + "The two couples match unless the inputs set is_uc_claimant. Benefits " "another member of the benefit unit claims in their own right do not " "count. The total includes Universal Credit and Housing Benefit " "before the cap (universal_credit_pre_benefit_cap, " @@ -63,12 +63,14 @@ class benefit_cap_welfare_benefits(Variable): "supply this total include Housing Benefit in it. They also supply " "housing_benefit_pre_benefit_cap, which " "uc_benefit_cap_welfare_benefits subtracts, or supply " - "uc_benefit_cap_welfare_benefits itself; otherwise Universal Credit's " - "cap raises ValueError. A reform of this variable changes the Housing " - "Benefit cap. To change the Universal Credit cap's basket, replace " - "uc_benefit_cap_welfare_benefits. Widowed parent's allowance counts; " - "maternity allowance and the other bereavement benefits, also welfare " - "benefits, are not modelled here." + "uc_benefit_cap_welfare_benefits itself, with a value for every " + "benefit unit; otherwise the model raises ValueError when it " + "calculates Universal Credit's cap. A reform of this variable changes " + "the Housing Benefit cap. To change the Universal Credit cap's basket, " + "replace uc_benefit_cap_welfare_benefits. Widowed parent's allowance " + "counts. The model has no variable for maternity allowance or the " + "other bereavement benefits, which Parliament also lists as welfare " + "benefits (Welfare Reform Act 2012 s. 96(10))." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py index 821e08e13d..d373a2387c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -26,12 +26,16 @@ def supplied_total_without_housing_benefit(period) -> str: return ( f"The inputs supply benefit_cap_welfare_benefits for {period}, a total " - "that includes Housing Benefit, which the Universal Credit cap counts " - "as nil (UC Regs 2013 reg. 80(2A)). Supply " - "housing_benefit_pre_benefit_cap for the same period, for the cap to " - "subtract, or supply uc_benefit_cap_welfare_benefits. The model doesn't " - "calculate that Housing Benefit for Universal Credit, because the " - "Housing Benefit non-dependant deductions read universal_credit." + "that includes Housing Benefit, which DWP counts as nil for the " + "Universal Credit cap (UC Regs 2013 reg. 80(2A)). Supply " + "housing_benefit_pre_benefit_cap for the same period, for the model to " + "subtract, or supply uc_benefit_cap_welfare_benefits, with a value for " + "every benefit unit: a situation that sets either for some benefit " + "units stores nil for the rest. The model doesn't calculate that " + "Housing Benefit for Universal Credit, because the Housing Benefit " + "non-dependant deductions read universal_credit. A reform that " + "neutralizes housing_benefit_pre_benefit_cap drops a supplied value; " + "supply uc_benefit_cap_welfare_benefits instead." ) @@ -46,8 +50,9 @@ class uc_benefit_cap_welfare_benefits(Variable): "inputs supply benefit_cap_welfare_benefits, whose total includes " "Housing Benefit, this subtracts the supplied " "housing_benefit_pre_benefit_cap. Inputs that supply the combined " - "total also supply housing_benefit_pre_benefit_cap or this variable; " - "otherwise the formula raises ValueError. No baseline formula on " + "total also supply housing_benefit_pre_benefit_cap or this variable, " + "with a value for every benefit unit; otherwise the formula raises " + "ValueError. No baseline formula on " "Universal Credit's route calculates Housing Benefit, so the Housing " "Benefit non-dependant deductions can read universal_credit." ) From 4ec0693f7a01dc4347c249b9946db7a68be1e3a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:02:06 +0000 Subject: [PATCH 43/43] Rewrap the invariant 12 docstring paragraph Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JPH2P5XFEgwSLLAb1 --- ...test_non_dependant_deduction_properties.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 0be4a4ea89..5a172b2bad 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -73,17 +73,18 @@ its traced calculation reads no Housing Benefit variable but the reported award. Inputs that supply benefit_cap_welfare_benefits, a total that includes Housing Benefit, also supply housing_benefit_pre_benefit_cap or - uc_benefit_cap_welfare_benefits, or the basket raises ValueError. A non-dependant family's gross income - counts its award after the cap and before debt deductions: universal_credit - plus uc_deductions, which equals the award before the cap less the cap - reduction applied, under current law and under an 85% protected minimum - floor; the floor never lowers an award. Under a reform that scales - universal_credit, each household's deductions equal those of the same - household with the reformed award supplied, and the Housing Benefit - deduction is the band of wages plus that award, so it moves when the - award crosses a band. (test_non_dependant_cycle_contracts.py checks the - supplied-total contract over mixed populations, and that a reform making - Universal Credit read calculated Housing Benefit raises CycleError.) + uc_benefit_cap_welfare_benefits, or the basket raises ValueError. A + non-dependant family's gross income counts its award after the cap and + before debt deductions: universal_credit plus uc_deductions, which equals + the award before the cap less the cap reduction applied, under current law + and under an 85% protected minimum floor; the floor never lowers an award. + Under a reform that scales universal_credit, each household's deductions + equal those of the same household with the reformed award supplied, and + the Housing Benefit deduction is the band of wages plus that award, so it + moves when the award crosses a band. + (test_non_dependant_cycle_contracts.py checks the supplied-total contract + over mixed populations, and that a reform making Universal Credit read + calculated Housing Benefit raises CycleError.) 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