From 73abf3f4a82501f3648f1a37e1a15540b7621b4c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 14:39:49 -0400 Subject: [PATCH 1/8] Charge non-dependant deductions for non-dependants within the claimant's benefit unit A member of a benefit unit aged 16 or over who is not the claimant, partner or a qualifying young person (UC) or child or young person (HB, CTR) is a non-dependant of the renter (UC Regs 2013 Sch 4 para 9; HB Regs 2006 and HB (SPC) Regs 2006 reg 3; SI 2012/2885 reg 9 and the Welsh and Scottish equivalents). The deductions now charge them: - UC: to the renter's own claim; non-dependants in other families stay with the household head's claim (para 9(2)(f)). Adds the para 15 renter exemption and limits the para 16 Pension Credit exemption to the claimant or partner who receives it. - HB: apportioned between joint occupiers by rent share (reg 74(5)); a boarder or lodger bears their own. - National CTR: pooled across the claiming families by joint-liability share; only claiming families bear deductions. Fixes #2014. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2014.fixed.md | 1 + .../family/benefit_unit_non_dependants.yaml | 592 ++++++++++++++++++ ...individual_non_dep_deduction_eligible.yaml | 24 +- .../uc_individual_non_dep_deduction.yaml | 5 + ...individual_non_dep_deduction_eligible.yaml | 38 +- ...t_benefit_unit_non_dependant_properties.py | 381 +++++++++++ ...sharers_boarders_and_lodgers_properties.py | 8 +- ...t_individual_non_dep_deduction_eligible.py | 18 +- .../housing_benefit_non_dep_deductions.py | 39 +- ..._unit_non_dependant_for_legacy_benefits.py | 43 ++ ...unit_non_dependant_for_universal_credit.py | 36 ++ ...c_individual_non_dep_deduction_eligible.py | 21 +- .../uc_non_dep_deduction_exempt.py | 9 +- .../uc_non_dep_deductions.py | 35 +- .../uc_non_dep_deductions_renter_exempt.py | 31 + ...n_individual_non_dep_deduction_eligible.py | 31 +- ...ouncil_tax_reduction_non_dep_deductions.py | 17 +- 17 files changed, 1282 insertions(+), 47 deletions(-) create mode 100644 changelog.d/2014.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml create mode 100644 policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py create mode 100644 policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_universal_credit.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions_renter_exempt.py diff --git a/changelog.d/2014.fixed.md b/changelog.d/2014.fixed.md new file mode 100644 index 0000000000..b3c0b44145 --- /dev/null +++ b/changelog.d/2014.fixed.md @@ -0,0 +1 @@ +- Charge Universal Credit, Housing Benefit and national Council Tax Reduction non-dependant deductions for members of the claimant's own benefit unit who are not the claimant, partner or a child or young person, such as an adult son (UC Regs 2013 Sch 4 para 9; HB Regs 2006 and HB (SPC) Regs 2006 reg 3; SI 2012/2885 reg 9 and the Welsh and Scottish equivalents), and apply the Universal Credit renter exemption from housing cost contributions (Sch 4 para 15). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml new file mode 100644 index 0000000000..3efb8dabd7 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -0,0 +1,592 @@ +# Non-dependants within the claimant's own benefit unit. +# +# A benefit unit can include someone aged 16 or over who is neither the +# claimant or partner nor a child or young person the claimant is responsible +# for, such as an adult son or a 19-year-old who has left education. In law +# that person is a non-dependant of the renter (UC Regs 2013 Sch 4 para +# 9(1)(c) and (2); HB Regs 2006 and HB (SPC) Regs 2006 reg 3; CTR SI +# 2012/2885 reg 9, WSI 2013/3029 reg 9, SSI 2021/249 reg 8), so their family's +# claim bears a deduction for them, subject to each scheme's exemptions. +# +# Amounts for 2026-27: +# - UC housing cost contribution £96.55 a month (Sch 4 para 14(1)), aged 21 +# or over (para 16(2)(a)): £1,158.60 a year. +# - HB non-dependant aged 18 or over not in remunerative work £20.40 a week +# (reg 74(1)(b)): £1,060.80 a year. In remunerative work with gross income +# of £300 a week: £64.35 (reg 74(2)(c)), £3,346.20 a year. The HB cases +# give part-time earners £50 a week for 10 hours, below remunerative work +# (16 hours, reg 6) and inside the lowest band. + +- name: UC, an adult son in the renter's benefit unit bears a housing cost contribution + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + is_benefit_unit_non_dependant_for_universal_credit: [false, true] + uc_individual_non_dep_deduction: [0, 1_158.60] + uc_non_dep_deductions: 1_158.60 + +- name: UC, no contribution for a non-dependant in the benefit unit under 21 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + # Left education: not a qualifying young person. + age: 19 + is_claimant_or_partner: false + is_qualifying_young_person_for_universal_credit: false + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + is_benefit_unit_non_dependant_for_universal_credit: [false, true] + uc_non_dep_deductions: 0 + +- name: UC, a qualifying young person the renter is responsible for is not a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + daughter: + age: 19 + is_claimant_or_partner: false + is_qualifying_young_person_for_universal_credit: true + benunits: + benunit: + members: [parent, daughter] + households: + household: + members: [parent, daughter] + output: + is_benefit_unit_non_dependant_for_universal_credit: [false, false] + uc_non_dep_deductions: 0 + +# Sch 4 para 9(2)(g): a qualifying young person for whom no one in the +# renter's extended benefit unit is responsible is not a non-dependant. +- name: UC, a qualifying young person looked after by a local authority is not a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + carer: + age: 50 + is_household_head: true + is_claimant_or_partner: true + young_person: + age: 17 + is_claimant_or_partner: false + is_qualifying_young_person_for_universal_credit: true + is_looked_after_by_local_authority: true + benunits: + benunit: + members: [carer, young_person] + households: + household: + members: [carer, young_person] + output: + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + is_benefit_unit_non_dependant_for_universal_credit: [false, false] + uc_non_dep_deductions: 0 + +# Sch 4 para 15: no contribution for any non-dependant if the renter, or +# either joint renter of a couple, receives the PIP daily living component. +- name: UC, no contribution where the renter receives the PIP daily living component + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + pip_dl: 3_000 + son: + age: 25 + is_claimant_or_partner: false + aunt: + # A non-dependant in another family. + age: 30 + benunits: + benunit_1: + members: [parent, son] + benunit_2: + members: [aunt] + households: + household: + members: [parent, son, aunt] + output: + uc_non_dep_deductions_renter_exempt: [true, false] + uc_non_dep_deductions: [0, 0] + +- name: UC, no contribution where the renter's partner is blind + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent_1: + age: 52 + is_household_head: true + is_claimant_or_partner: true + parent_2: + age: 50 + is_claimant_or_partner: true + is_blind: true + son: + age: 25 + is_claimant_or_partner: false + benunits: + benunit: + members: [parent_1, parent_2, son] + households: + household: + members: [parent_1, parent_2, son] + output: + uc_non_dep_deductions: 0 + +# Sch 4 para 16(2)(d): no contribution for a non-dependant receiving +# Attendance Allowance. +- name: UC, no contribution for a non-dependant in the benefit unit receiving Attendance Allowance + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + attendance_allowance: 3_000 + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + uc_non_dep_deduction_exempt: [false, true] + uc_non_dep_deductions: 0 + +# Pension Credit is the claimant's award: an adult son in a family on Pension +# Credit is not receiving it, so para 16(2)(b) does not exempt him. +- name: UC, a family's Pension Credit does not exempt the non-dependant in its benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 70 + is_claimant_or_partner: true + son: + age: 40 + is_claimant_or_partner: false + benunits: + benunit: + members: [parent, son] + pension_credit: 2_000 + output: + uc_non_dep_deduction_exempt: [true, false] + +- name: UC, the head's family bears non-dependants in its own benefit unit and in other families + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + aunt: + age: 60 + is_claimant_or_partner: true + benunits: + benunit_1: + members: [parent, son] + benunit_2: + members: [aunt] + households: + household: + members: [parent, son, aunt] + output: + uc_non_dep_deductions: [2_317.20, 0] + +# A non-dependant family's own adult member is a non-dependant of the +# household head, counted once, in the head's claim (Sch 4 para 9(2)(f)). +- name: UC, an adult in a non-dependant family is the household head's non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + is_claimant_or_partner: true + daughter: + age: 45 + is_claimant_or_partner: true + grandson: + age: 23 + is_claimant_or_partner: false + benunits: + benunit_1: + members: [head] + benunit_2: + members: [daughter, grandson] + households: + household: + members: [head, daughter, grandson] + output: + uc_non_dep_deductions: [2_317.20, 0] + +# A sharer of the rent bears the non-dependant in its own benefit unit; the +# household head's family does not (para 9(2)(f)). +- name: UC, a sharer of the rent bears the non-dependant in its own benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + sharer: + age: 48 + is_claimant_or_partner: true + sharer_son: + age: 26 + is_claimant_or_partner: false + benunits: + benunit_1: + members: [head] + benunit_2: + members: [sharer, sharer_son] + liable_for_share_of_household_rent: true + households: + household: + members: [head, sharer, sharer_son] + rent: 12_000 + output: + uc_non_dep_deductions: [0, 1_158.60] + +- name: UC, the housing costs element nets off the contribution for an adult son + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + benunits: + benunit: + members: [parent, son] + benunit_tenure_type: RENT_FROM_COUNCIL + benunit_rent: 6_000 + households: + household: + members: [parent, son] + output: + # £6,000 - £1,158.60. + uc_housing_costs_element: 4_841.40 + +- name: HB, an adult son in the claimant's benefit unit working part time + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 2_600 + weekly_hours: 10 + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + is_benefit_unit_non_dependant_for_legacy_benefits: [false, true] + housing_benefit_non_dep_deductions: 1_060.80 + +- name: HB, an adult daughter in the claimant's benefit unit working full time on £300 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + daughter: + age: 22 + is_claimant_or_partner: false + employment_income: 15_600 + weekly_hours: 37.5 + benunits: + benunit: + members: [parent, daughter] + households: + household: + members: [parent, daughter] + output: + housing_benefit_non_dep_deductions: 3_346.20 + +- name: HB, no deduction for a non-dependant in the benefit unit under 18 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 17 + is_claimant_or_partner: false + is_child_or_qualifying_young_person_for_child_benefit: false + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + is_benefit_unit_non_dependant_for_legacy_benefits: [false, true] + housing_benefit_non_dep_deductions: 0 + +# HB reg 19: a Child Benefit qualifying young person is in the claimant's +# family, not a non-dependant. +- name: HB, a young person in the claimant's family is not a non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + daughter: + age: 19 + is_claimant_or_partner: false + is_child_or_qualifying_young_person_for_child_benefit: true + benunits: + benunit: + members: [parent, daughter] + households: + household: + members: [parent, daughter] + output: + is_benefit_unit_non_dependant_for_legacy_benefits: [false, false] + housing_benefit_non_dep_deductions: 0 + +# HB reg 74(5): a non-dependant of two joint occupiers is apportioned by their +# shares of the rent, whichever family the non-dependant belongs to. +- name: HB, joint occupiers share the deduction for an adult son in one of their families + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 2_600 + weekly_hours: 10 + sharer: + age: 48 + is_claimant_or_partner: true + benunits: + benunit_1: + members: [head, son] + benunit_2: + members: [sharer] + liable_for_share_of_household_rent: true + households: + household: + members: [head, son, sharer] + rent: 12_000 + output: + share_of_household_rent: [0.5, 0.5] + housing_benefit_non_dep_deductions: [530.40, 530.40] + +# A lodger's family has no share of the household's rent: it bears only the +# non-dependant in its own benefit unit. +- name: HB, a lodger bears the non-dependant in their own benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 60 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 45 + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + lodger_son: + age: 21 + is_claimant_or_partner: false + employment_income: 2_600 + weekly_hours: 10 + benunits: + benunit_1: + members: [head] + benunit_2: + members: [lodger, lodger_son] + households: + household: + members: [head, lodger, lodger_son] + rent: 9_000 + output: + housing_benefit_non_dep_deductions: [0, 1_060.80] + +- name: CTR, the claimant's family bears the non-dependant in its benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deductions: 300 + +- name: CTR, no deduction for a non-dependant in the benefit unit under 18 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 17 + is_claimant_or_partner: false + is_child_or_qualifying_young_person_for_child_benefit: false + benunits: + benunit: + members: [parent, son] + households: + household: + members: [parent, son] + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] + council_tax_reduction_non_dep_deductions: 0 + +# SI 2012/2885 Sch 1 para 8(5): a non-dependant of two jointly liable people +# is apportioned equally between them, wherever the non-dependant's family. +- name: CTR, jointly liable families share the deduction for an adult son in one of them + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + council_tax_reduction_individual_non_dep_deduction: 300 + sharer: + age: 48 + is_claimant_or_partner: true + benunits: + benunit_1: + members: [head, son] + benunit_2: + members: [sharer] + liable_for_share_of_household_rent: true + households: + household: + members: [head, son, sharer] + rent: 12_000 + output: + council_tax_reduction_joint_liability_share: [0.5, 0.5] + council_tax_reduction_non_dep_deductions: [150, 150] + +# Only a family that claims bears deductions; the non-dependant's own family +# does not. +- name: CTR, a non-dependant family with an adult member bears no deductions itself + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + is_claimant_or_partner: true + daughter: + age: 45 + is_claimant_or_partner: true + council_tax_reduction_individual_non_dep_deduction: 200 + grandson: + age: 23 + is_claimant_or_partner: false + council_tax_reduction_individual_non_dep_deduction: 100 + benunits: + benunit_1: + members: [head] + benunit_2: + members: [daughter, grandson] + households: + household: + members: [head, daughter, grandson] + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true, true] + council_tax_reduction_non_dep_deductions: [300, 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..3d8f2812d9 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 @@ -1,7 +1,7 @@ - name: Eligible period: 2025 input: - benunit_is_rent_liable: false + is_non_dependant_of_household_head: true age: 18 output: housing_benefit_individual_non_dep_deduction_eligible: true @@ -9,7 +9,7 @@ - name: Age ineligible period: 2025 input: - benunit_is_rent_liable: false + is_non_dependant_of_household_head: true age: 17 output: housing_benefit_individual_non_dep_deduction_eligible: false @@ -21,3 +21,23 @@ age: 18 output: housing_benefit_individual_non_dep_deduction_eligible: false + +# HB Regs 2006 reg 3: a member of the claimant's own benefit unit who is not +# the claimant, partner or a child or young person. +- name: Non-dependant within the benefit unit + period: 2025 + input: + is_non_dependant_of_household_head: false + is_benefit_unit_non_dependant_for_legacy_benefits: true + age: 19 + output: + housing_benefit_individual_non_dep_deduction_eligible: true + +- name: Non-dependant within the benefit unit, under 18 + period: 2025 + input: + is_non_dependant_of_household_head: false + is_benefit_unit_non_dependant_for_legacy_benefits: true + age: 17 + output: + housing_benefit_individual_non_dep_deduction_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction.yaml index 1f7f3eada3..75d21ea9ce 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction.yaml @@ -6,6 +6,8 @@ person: age: 22 uc_non_dep_deduction_exempt: false + # A member of another family who is not liable for rent. + is_non_dependant_of_household_head: true benunits: benunit: members: person @@ -21,6 +23,7 @@ person: age: 22 uc_non_dep_deduction_exempt: true + is_non_dependant_of_household_head: true benunits: benunit: members: person @@ -36,6 +39,8 @@ person: age: 20 uc_non_dep_deduction_exempt: false + # A member of another family who is not liable for rent. + is_non_dependant_of_household_head: true benunits: benunit: members: person diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.yaml index 11fe57ddce..99888fb21a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.yaml @@ -4,7 +4,7 @@ input: age: 21 uc_non_dep_deduction_exempt: false - benunit_is_rent_liable: false + is_non_dependant_of_household_head: true output: uc_individual_non_dep_deduction_eligible: true @@ -14,6 +14,40 @@ input: age: 20 uc_non_dep_deduction_exempt: false - benunit_is_rent_liable: false + is_non_dependant_of_household_head: true + output: + uc_individual_non_dep_deduction_eligible: false + +- name: Exempt + period: 2020 + absolute_error_margin: 0 + input: + age: 21 + uc_non_dep_deduction_exempt: true + is_non_dependant_of_household_head: true + output: + uc_individual_non_dep_deduction_eligible: false + +# UC Regs 2013 Sch 4 para 9(1)(c): a member of the renter's own benefit unit +# who is not the claimant, partner or a qualifying young person. +- name: Non-dependant within the benefit unit + period: 2020 + absolute_error_margin: 0 + input: + age: 25 + uc_non_dep_deduction_exempt: false + is_non_dependant_of_household_head: false + is_benefit_unit_non_dependant_for_universal_credit: true + output: + uc_individual_non_dep_deduction_eligible: true + +- name: The renter is not their own non-dependant + period: 2020 + absolute_error_margin: 0 + input: + age: 25 + uc_non_dep_deduction_exempt: false + is_non_dependant_of_household_head: false + is_benefit_unit_non_dependant_for_universal_credit: false output: uc_individual_non_dep_deduction_eligible: false diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py new file mode 100644 index 0000000000..1702d26e6e --- /dev/null +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -0,0 +1,381 @@ +"""Property-based tests for non-dependants within a benefit unit. + +A benefit unit can include members aged 16 or over who are neither the +claimant or partner nor a qualifying young person: in law they are +non-dependants of the renter (UC Regs 2013 Sch 4 para 9; HB Regs 2006 reg 3; +CTR SI 2012/2885 reg 9 and equivalents). Invariants, for any generated +population of households: + +1. Oracle: each family's Universal Credit, Housing Benefit and Council Tax + Reduction non-dependant deductions equal an independent calculation from + the generated household structure: who is a non-dependant, of whom, and + how a non-dependant of several joint occupiers is apportioned. +2. Conservation: every Housing Benefit and Council Tax Reduction + non-dependant deduction is borne in full, once, across the household's + families; every Universal Credit contribution is borne by exactly one + family unless that family's renter is exempt. +3. Exemptions: a family whose renter is exempt under Sch 4 para 15 has no + Universal Credit deductions; no deduction is charged for a non-dependant + under 21 (UC) or under 18 (HB, CTR), or for a qualifying young person. +4. No-op: in households without non-dependants inside a benefit unit, the + deductions equal the previous formulas, which charged only members of + other families. +5. Bedrooms: every Universal Credit non-dependant within a benefit unit has a + bedroom in the size criteria (para 10(1)(c)), so the deduction and the + bedroom use one definition. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +# Situation inputs without a period key are set for 2025, so the properties +# are checked in 2025, when the contribution was £93.02 a month. +YEAR = 2025 +UC_CONTRIBUTION = 93.02 * 12 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Members of a benefit unit who are not the claimant or partner. +# dependant: a qualifying young person the claimant is responsible for; +# looked_after: a qualifying young person placed by a local authority, for +# whom no one in the family is responsible; +# non_dependant: aged 16 or over and not a qualifying young person. +KINDS = ["dependant", "looked_after", "non_dependant"] +ROLES = ["sharer", "lodger", "non_dependant"] + + +@st.composite +def extra_member(draw): + kind = draw(st.sampled_from(KINDS)) + age = draw(st.integers(16, 19) if kind != "non_dependant" else st.integers(16, 60)) + return dict( + kind=kind, + age=age, + earnings=draw(st.sampled_from([0, 2_600, 15_600, 30_000])), + hours=draw(st.sampled_from([0, 10, 37.5])), + attendance_allowance=draw(st.booleans()), + ) + + +@st.composite +def family(draw, role): + # The household head's family has the household's oldest members, so it + # is unambiguously the head's (and the council tax claimant's) family. + adult_age = st.integers(61, 90) if role == "head" else st.integers(25, 59) + return dict( + role=role, + adult_ages=draw(st.lists(adult_age, min_size=1, max_size=2)), + extras=draw(st.lists(extra_member(), min_size=0, max_size=2)), + child=draw(st.booleans()), + renter_pip=draw(st.booleans()), + payment=draw(st.floats(1_000, 10_000, allow_nan=False)), + ) + + +@st.composite +def household(draw): + head = draw(family("head")) + others = [ + draw(family(draw(st.sampled_from(ROLES)))) + for _ in range(draw(st.integers(0, 2))) + ] + return dict( + families=[head] + others, + rent=draw(st.floats(3_000, 20_000, allow_nan=False)), + tenure=draw(st.sampled_from(["RENT_FROM_COUNCIL", "RENT_PRIVATELY"])), + ) + + +population = st.lists(household(), min_size=1, max_size=5) + + +def build(population, convert=None): + """One situation for the whole population, and a record per person and + per family for the oracle. convert maps extra members' kinds to others.""" + people, benunits, homes = {}, {}, {} + person_rows, family_rows = [], [] + for h, house in enumerate(population): + members = [] + for f, fam in enumerate(house["families"]): + name = f"h{h}_f{f}" + ids = [] + head_family = f == 0 + # The head's family has the household's oldest member. + for i, age in enumerate(fam["adult_ages"]): + pid = f"{name}_adult_{i}" + pip = fam["renter_pip"] and i == 0 + people[pid] = { + "age": age, + "is_claimant_or_partner": True, + "is_household_head": head_family and i == 0, + "pip_dl": 3_000.0 if pip else 0.0, + } + ids.append(pid) + person_rows.append( + dict( + household=h, + family=len(family_rows), + kind="adult", + age=age, + disability_benefit=pip, + ) + ) + for i, extra in enumerate(fam["extras"]): + kind = (convert or {}).get(extra["kind"], extra["kind"]) + pid = f"{name}_extra_{i}" + young = kind != "non_dependant" + people[pid] = { + "age": min(extra["age"], 19) if young else extra["age"], + "is_claimant_or_partner": False, + "is_household_head": False, + "is_qualifying_young_person_for_universal_credit": young, + "is_child_or_qualifying_young_person_for_child_benefit": young, + "is_looked_after_by_local_authority": kind == "looked_after", + "employment_income": float(extra["earnings"]), + "weekly_hours": float(extra["hours"]), + "attendance_allowance": ( + 3_000.0 if extra["attendance_allowance"] else 0.0 + ), + } + ids.append(pid) + person_rows.append( + dict( + household=h, + family=len(family_rows), + kind=kind, + age=people[pid]["age"], + disability_benefit=extra["attendance_allowance"], + ) + ) + if fam["child"]: + pid = f"{name}_child" + people[pid] = { + "age": 8, + "is_claimant_or_partner": False, + "is_household_head": False, + } + ids.append(pid) + person_rows.append( + dict( + household=h, + family=len(family_rows), + kind="child", + age=8, + disability_benefit=False, + ) + ) + benunit = {"members": ids} + if fam["role"] == "sharer": + benunit["liable_for_share_of_household_rent"] = True + if fam["role"] == "lodger": + people[ids[0]]["rent_paid_as_lodger"] = fam["payment"] + benunits[name] = benunit + members.extend(ids) + family_rows.append( + dict( + household=h, + role=fam["role"], + renter_pip=fam["renter_pip"], + n_liable=len(fam["adult_ages"]), + ) + ) + homes[f"h{h}"] = { + "members": members, + "rent": house["rent"], + "tenure_type": house["tenure"], + "brma": "MAIDSTONE", + } + situation = {"people": people, "benunits": benunits, "households": homes} + return situation, person_rows, family_rows + + +def calc(sim, variable, map_to=None): + return np.asarray(sim.calculate(variable, YEAR, map_to=map_to), dtype=float) + + +def oracle(person_rows, family_rows, uc, hb, ctr, pension_credit, claims, ctr_share): + """Expected deductions per family from the generated structure. + + uc, hb and ctr are each person's individual deduction from the model; + the oracle decides independently who is whose non-dependant. + """ + n = len(family_rows) + exp_uc, exp_hb, exp_ctr = np.zeros(n), np.zeros(n), np.zeros(n) + expected_uc_individual = np.zeros(len(person_rows)) + households = sorted({f["household"] for f in family_rows}) + for h in households: + fams = [i for i, f in enumerate(family_rows) if f["household"] == h] + head = fams[0] + # Families liable for the household's rent: the head's and sharers'. + liable = [i for i in fams if family_rows[i]["role"] in ("head", "sharer")] + total_liable = sum(family_rows[i]["n_liable"] for i in liable) + share = { + i: (family_rows[i]["n_liable"] / total_liable if i in liable else 0.0) + for i in fams + } + for p, row in enumerate(person_rows): + if row["household"] != h: + continue + f = row["family"] + role = family_rows[f]["role"] + other_family = role == "non_dependant" + own_family = row["kind"] == "non_dependant" + # UC: a member of a family not liable for rent is the head's + # non-dependant; a non-QYP member aged 16+ of any other family is + # their own family's. + # Para 16: Attendance Allowance or PIP daily living, or Pension + # Credit, which only a claimant or partner receives. + uc_exempt = row["disability_benefit"] or ( + row["kind"] == "adult" and pension_credit[f] > 0 + ) + uc_non_dependant = other_family or own_family + if uc_non_dependant and row["age"] >= 21 and not uc_exempt: + expected_uc_individual[p] = UC_CONTRIBUTION + bearer = head if other_family else f + if uc_non_dependant and not family_rows[bearer]["renter_pip"]: + exp_uc[bearer] += uc[p] + # HB: pooled across joint occupiers by share; a lodger's own + # non-dependants fall on the lodger. + if other_family or (own_family and share[f] > 0): + for i in fams: + exp_hb[i] += share[i] * hb[p] + elif own_family: + exp_hb[f] += hb[p] + # CTR: every non-dependant is pooled across the claiming families. + if other_family or own_family: + for i in fams: + exp_ctr[i] += claims[i] * ctr_share[i] * ctr[p] + return exp_uc, exp_hb, exp_ctr, expected_uc_individual + + +def run(population, convert=None): + situation, person_rows, family_rows = build(population, convert) + sim = Simulation(situation=situation) + values = dict( + uc=calc(sim, "uc_individual_non_dep_deduction"), + hb=calc(sim, "household_benefits_individual_non_dep_deduction"), + ctr=calc(sim, "council_tax_reduction_individual_non_dep_deduction"), + pension_credit=calc(sim, "pension_credit"), + claims=calc(sim, "council_tax_reduction_claimant_benunit"), + ctr_share=calc(sim, "council_tax_reduction_joint_liability_share"), + ) + return sim, person_rows, family_rows, values + + +@PROPERTY_SETTINGS +@given(population) +def test_deductions_match_the_oracle(population): + sim, person_rows, family_rows, v = run(population) + exp_uc, exp_hb, exp_ctr, exp_uc_individual = oracle(person_rows, family_rows, **v) + # 1. Oracle. + assert np.allclose(v["uc"], exp_uc_individual, atol=0.01) + assert np.allclose(calc(sim, "uc_non_dep_deductions"), exp_uc, atol=0.01) + assert np.allclose( + calc(sim, "housing_benefit_non_dep_deductions"), exp_hb, atol=0.01 + ) + assert np.allclose( + calc(sim, "council_tax_reduction_non_dep_deductions"), exp_ctr, atol=0.01 + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_conservation_and_exemptions(population): + sim, person_rows, family_rows, v = run(population) + ages = np.array([row["age"] for row in person_rows]) + kinds = np.array([row["kind"] for row in person_rows]) + # 2. Conservation: HB and CTR deductions are borne once in full. + hb_families = sim.map_result( + calc(sim, "housing_benefit_non_dep_deductions"), "benunit", "household" + ) + hb_people = sim.map_result(v["hb"], "person", "household") + assert np.allclose(hb_families, hb_people, atol=0.01) + claim_shares = sim.map_result(v["claims"] * v["ctr_share"], "benunit", "household") + ctr_families = sim.map_result( + calc(sim, "council_tax_reduction_non_dep_deductions"), "benunit", "household" + ) + ctr_people = sim.map_result(v["ctr"], "person", "household") + assert np.allclose(ctr_families, claim_shares * ctr_people, atol=0.01) + # UC: each contribution is borne once, except where the bearer is exempt. + uc_families = sim.map_result( + calc(sim, "uc_non_dep_deductions"), "benunit", "household" + ) + uc_people = sim.map_result(v["uc"], "person", "household") + assert np.all(uc_families <= uc_people + 0.01) + # 3. Exemptions. + renter_exempt = calc(sim, "uc_non_dep_deductions_renter_exempt") > 0 + assert np.all(calc(sim, "uc_non_dep_deductions")[renter_exempt] == 0) + assert np.all(v["uc"][ages < 21] == 0) + assert np.all(v["hb"][ages < 18] == 0) + assert np.all(v["ctr"][ages < 18] == 0) + # Nobody in a family liable for rent is charged except its own + # non-dependants. + not_non_dependant = kinds != "non_dependant" + liable_family = np.array( + [family_rows[row["family"]]["role"] != "non_dependant" for row in person_rows] + ) + for amounts in (v["uc"], v["hb"], v["ctr"]): + assert np.all(amounts[not_non_dependant & liable_family] == 0) + + +@PROPERTY_SETTINGS +@given(population) +def test_no_op_without_non_dependants_in_a_benefit_unit(population): + every_kind_dependant = {kind: "dependant" for kind in KINDS} + sim, person_rows, family_rows, v = run(population, every_kind_dependant) + # 4. The previous formulas: only members of other families, charged to + # the head's family (UC), by rent share (HB) or by claim share (CTR). + person_household = np.array([row["household"] for row in person_rows]) + family_household = np.array([row["household"] for row in family_rows]) + other = calc(sim, "is_non_dependant_of_household_head") > 0 + + def other_families(amounts): + by_household = np.bincount( + person_household, + weights=amounts * other, + minlength=family_household.max() + 1, + ) + return by_household[family_household] + + head_family = np.array([row["role"] == "head" for row in family_rows]) + renter_exempt = calc(sim, "uc_non_dep_deductions_renter_exempt") > 0 + assert np.allclose( + calc(sim, "uc_non_dep_deductions"), + np.where(renter_exempt, 0, head_family * other_families(v["uc"])), + atol=0.01, + ) + assert np.allclose( + calc(sim, "housing_benefit_non_dep_deductions"), + calc(sim, "share_of_household_rent") * other_families(v["hb"]), + atol=0.01, + ) + assert np.allclose( + calc(sim, "council_tax_reduction_non_dep_deductions"), + v["claims"] * v["ctr_share"] * other_families(v["ctr"]), + atol=0.01, + ) + + +@PROPERTY_SETTINGS +@given(population) +def test_every_non_dependant_in_a_benefit_unit_has_a_bedroom(population): + sim, person_rows, family_rows, v = run(population) + # 5. Para 10(1)(b)-(c): a bedroom for each responsible qualifying young + # person and each non-dependant within the unit; the rest of the count + # (renter, children, other families' non-dependants) is unchanged when + # those members are made dependants instead. + in_unit = calc(sim, "is_benefit_unit_non_dependant_for_universal_credit") + kinds = np.array([row["kind"] for row in person_rows]) + assert np.array_equal(in_unit > 0, kinds == "non_dependant") + rooms = calc(sim, "LHA_allowed_bedrooms") + as_dependants, *_ = run(population, {"non_dependant": "dependant"}) + rooms_as_dependants = calc(as_dependants, "LHA_allowed_bedrooms") + # A non-dependant has a bedroom as a responsible qualifying young person + # would, so the count is the same either way. + assert np.array_equal(rooms, rooms_as_dependants) 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 c3b3efca5a..823c384da4 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 @@ -12,9 +12,11 @@ 3. Tenure: a boarder's or lodger's family rents privately, is never in social housing, and is LHA-eligible. 4. Non-dependants: a family liable for rent is never a non-dependant; only - the household head's family has Universal Credit non-dependant - deductions; a boarder's or lodger's family has no Housing Benefit or - Council Tax Reduction non-dependant deductions. + the household head's family has Universal Credit deductions for + non-dependants in other families; a boarder's or lodger's family has no + Housing Benefit or Council Tax Reduction deductions for them. (The + generated families have no non-dependants within their own benefit unit, + which test_benefit_unit_non_dependant_properties covers.) 5. Category: the Universal Credit LHA category does not depend on the household input is_shared_accommodation; the Housing Benefit category is the shared rate for any family entitled to one bedroom that lacks 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..164a42b907 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 @@ -5,10 +5,24 @@ class housing_benefit_individual_non_dep_deduction_eligible(Variable): value_type = bool entity = Person label = "eligible person for the Housing Benefit non-dependent deduction" + documentation = ( + "A non-dependant aged 18 or over: either a member of another family " + "who is not liable for rent (see is_non_dependant_of_household_head) " + "or a member of their own benefit unit who is not the claimant, " + "partner or a child or young person (see " + "is_benefit_unit_non_dependant_for_legacy_benefits)." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "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 + non_dependant = person("is_non_dependant_of_household_head", period) | person( + "is_benefit_unit_non_dependant_for_legacy_benefits", period + ) age_eligible = person("age", period) >= p.age_threshold - return age_eligible & ~rent_liable + return non_dependant & age_eligible 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 e5e9e3a933..03adebf196 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,11 +5,20 @@ class housing_benefit_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "non-dependent deductions" + documentation = ( + "Deductions for the claimant's non-dependants: members of other " + "families of the household who are not liable for rent, and members " + "of a family liable for the rent who are not its claimant, partner or " + "a child or young person. A non-dependant of several joint occupiers " + "is apportioned between them by their shares of the rent; a boarder " + "or lodger bears only the non-dependants in their own family." + ) definition_period = YEAR unit = GBP reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/3", "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", ) @@ -17,14 +26,26 @@ def formula(benunit, period, parameters): # Deductions are made 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 non-dependant of more - # than one joint 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. + # non-dependants (reg 3(2)(d)-(e), 3(4)). person = benunit.members - deductions = person( - "household_benefits_individual_non_dep_deduction", period - ) * person("is_non_dependant_of_household_head", period) + deductions = person("household_benefits_individual_non_dep_deduction", period) + other_family = person("is_non_dependant_of_household_head", period) + # A member of a rent-liable family who is not in its claimant's + # family under reg 3(2)(a) and (c) is a non-dependant too. + own_family = ( + person("is_benefit_unit_non_dependant_for_legacy_benefits", period) + & ~other_family + ) + # A non-dependant of more than one joint occupier is apportioned + # between them by their shares of the payments (reg 74(5); SPC reg + # 55(5)): one living with joint occupiers resides with each of them, + # whichever family they belong to. A boarder or lodger has no share + # of the household's rent and bears only their own family's. + joint_occupier = person.benunit("share_of_household_rent", period) > 0 + shared = other_family | (own_family & joint_occupier) share = benunit("share_of_household_rent", period) - return share * benunit.max(person.household.sum(deductions)) + from_joint_occupation = share * benunit.max( + person.household.sum(deductions * shared) + ) + from_own_family = benunit.sum(deductions * own_family * ~joint_occupier) + return from_joint_occupation + from_own_family diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py new file mode 100644 index 0000000000..e1449d56e8 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py @@ -0,0 +1,43 @@ +from policyengine_uk.model_api import * + + +class is_benefit_unit_non_dependant_for_legacy_benefits(Variable): + value_type = bool + entity = Person + label = "Non-dependant within the benefit unit for Housing Benefit and Council Tax Reduction" + documentation = ( + "A member of the benefit unit who is neither the claimant or partner " + "nor a child or young person: someone aged 16 or over who is not a " + "Child Benefit qualifying young person. The benefit unit follows the " + "survey's family, which can include an adult child or a 16 to 19 year " + "old who is not a qualifying young person. Under the Housing Benefit " + "and Council Tax Reduction schemes such a person is not in the " + "claimant's family (SSCBA 1992 s.137(1), with young person defined " + "by Child Benefit) but normally resides with the claimant, so is a " + "non-dependant. A child or young person is either in the family or, " + "if placed with the claimant by a local authority, excluded from " + "being a non-dependant." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/3", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/19", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/21", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/3", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/9", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/8", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/3", + "https://www.legislation.gov.uk/ukpga/1992/4/section/137", + ) + + def formula(person, period, parameters): + # HB Regs 2006 and HB (SPC) Regs 2006 reg 3(2)(a), (c) with regs 19 + # and 21(3); SI 2012/2885 and WSI 2013/3029 reg 9(2)(a), (c); SSI + # 2021/249 reg 8(2)(a)-(b); SSI 2012/319 reg 3. A Child Benefit child + # is under 16. + claimant_or_partner = person("is_claimant_or_partner", period) + child_or_young_person = person( + "is_child_or_qualifying_young_person_for_child_benefit", period + ) + return ~claimant_or_partner & ~child_or_young_person diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_universal_credit.py b/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_universal_credit.py new file mode 100644 index 0000000000..bc6b71d000 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_universal_credit.py @@ -0,0 +1,36 @@ +from policyengine_uk.model_api import * + + +class is_benefit_unit_non_dependant_for_universal_credit(Variable): + value_type = bool + entity = Person + label = "Non-dependant within the benefit unit for Universal Credit" + documentation = ( + "A member of the benefit unit, aged 16 or over, who is neither the " + "claimant or partner nor a qualifying young person. The benefit unit " + "follows the survey's family, which can include an adult child or a " + "16 to 19 year old who is not a qualifying young person. In Universal " + "Credit law such a person is not in the claimant's family but is a " + "non-dependant of the renter: they normally live in the accommodation " + "and are none of the persons excluded by Sch 4 para 9(2). A qualifying " + "young person the renter is responsible for is a member of the " + "renter's extended benefit unit, and one no one is responsible for, " + "for example one looked after by a local authority, is excluded by " + "para 9(2)(g). A child, under 16, is always one or the other." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", + "https://www.legislation.gov.uk/ukpga/2012/5/section/40", + ) + + def formula(person, period, parameters): + # UC Regs 2013 Sch 4 para 9(1)(a)-(b) and (2)(a), (g); WRA 2012 s.40. + not_a_child = ~person("is_child_for_universal_credit", period) + claimant_or_partner = person("is_claimant_or_partner", period) + qualifying_young_person = person( + "is_qualifying_young_person_for_universal_credit", period + ) + return not_a_child & ~claimant_or_partner & ~qualifying_young_person diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py index abd9869256..d47928de45 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_individual_non_dep_deduction_eligible.py @@ -5,14 +5,27 @@ class uc_individual_non_dep_deduction_eligible(Variable): value_type = bool entity = Person label = "Eligible person for the Universal Credit non-dependent deduction" + documentation = ( + "A non-dependant of a renter, aged 21 or over and not otherwise " + "exempt: either a member of another family who is not liable for rent " + "(see is_non_dependant_of_household_head) or a member of their own " + "benefit unit who is not the claimant, partner or a qualifying young " + "person (see is_benefit_unit_non_dependant_for_universal_credit)." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/13", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/16", + ) def formula(person, period, parameters): - not_rent_liable = ~person.benunit("benunit_is_rent_liable", period) p = parameters( period ).gov.dwp.universal_credit.elements.housing.non_dep_deduction - age = person("age", period) - age_eligible = age >= p.age_threshold + non_dependant = person("is_non_dependant_of_household_head", period) | person( + "is_benefit_unit_non_dependant_for_universal_credit", period + ) + age_eligible = person("age", period) >= p.age_threshold exempt = person("uc_non_dep_deduction_exempt", period) - return ~exempt & age_eligible & not_rent_liable + return non_dependant & age_eligible & ~exempt diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py index 432cfb0936..512d2e32b3 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py @@ -11,13 +11,18 @@ class uc_non_dep_deduction_exempt(Variable): "receiving Pension Credit, the middle or higher rate of the DLA care " "component, the PIP daily living component or Attendance Allowance, or " "who is entitled to Carer's Allowance (underlying entitlement included, " - "so the carer test is is_carer_for_benefits rather than receipt)." + "so the carer test is is_carer_for_benefits rather than receipt). " + "Pension Credit is awarded to a benefit unit's claimant and partner, " + "so another adult in the unit is not receiving it." ) reference = "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/16" def formula(person, period, parameters): + receives_pension_credit = person("is_claimant_or_partner", period) & ( + person.benunit("pension_credit", period) > 0 + ) return ( - (person.benunit("pension_credit", period) > 0) + receives_pension_credit | person("dla_sc_middle_plus", period) | (person("pip_dl", period) > 0) | (person("attendance_allowance", period) > 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py index eca4abcd2a..20509cd891 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions.py @@ -5,23 +5,42 @@ class uc_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "Universal Credit non-dependent deductions" + documentation = ( + "Housing cost contributions for the renter's non-dependants: those in " + "other families of the household, for the household head's family " + "only, and those within the renter's own benefit unit. None if the " + "renter or a joint renter is exempt." + ) definition_period = YEAR unit = GBP reference = ( "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/9", "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/13", + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/15", ) def formula(benunit, period, parameters): # A housing cost contribution is deducted for each non-dependant in # the renter's extended benefit unit (UC Regs 2013 Sch 4 para 13). - # Non-dependants live outside the household head's family and are - # not liable for rent, and only the household head's family has them - # (para 9(2)(d)-(f)): a sharer, boarder or lodger has none from the - # household head's family. person = benunit.members - deductions = person("uc_individual_non_dep_deduction", period) * person( - "is_non_dependant_of_household_head", period - ) + deductions = person("uc_individual_non_dep_deduction", period) + # Non-dependants from other families live outside the household + # head's family and are not liable for rent. A non-dependant counts + # in one claim only (para 9(2)(f)), which the model gives to the + # household head's family, so a sharer, boarder or lodger has none + # from other families. + other_family = person("is_non_dependant_of_household_head", period) head_family = benunit.any(person("is_household_head", period)) - return head_family * benunit.max(person.household.sum(deductions)) + from_other_families = head_family * benunit.max( + person.household.sum(deductions * other_family) + ) + # A member of the renter's own benefit unit who is not the claimant, + # partner or a qualifying young person is a non-dependant of the + # renter (para 9(1)(c) and (2)(a), (g)). + own_family = ( + person("is_benefit_unit_non_dependant_for_universal_credit", period) + & ~other_family + ) + from_own_family = benunit.sum(deductions * own_family) + renter_exempt = benunit("uc_non_dep_deductions_renter_exempt", period) + return where(renter_exempt, 0, from_other_families + from_own_family) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions_renter_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions_renter_exempt.py new file mode 100644 index 0000000000..187b0ec1ab --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deductions_renter_exempt.py @@ -0,0 +1,31 @@ +from policyengine_uk.model_api import * + + +class uc_non_dep_deductions_renter_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Universal Credit renter exempt from all housing cost contributions" + documentation = ( + "No housing cost contribution is deducted for any non-dependant if the " + "renter, or either joint renter of a couple, is blind or receives " + "Attendance Allowance, the middle or highest rate of the DLA care " + "component or the PIP daily living component. Scottish disability " + "assistance (adult and pension age disability payments, child " + "disability payment, Scottish adult DLA) has no separate input, and " + "entitlement suspended in hospital is not modelled." + ) + definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/15" + + def formula(benunit, period, parameters): + # UC Regs 2013 Sch 4 para 15: "joint renters" are joint claimants + # (para 1(2)), so the claimant and partner. + person = benunit.members + renter = person("is_claimant_or_partner", period) + qualifying = ( + person("is_blind", period) + | person("dla_sc_middle_plus", period) + | (person("attendance_allowance", period) > 0) + | (person("pip_dl", period) > 0) + ) + return benunit.any(renter & qualifying) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 459598f8d3..2ef332d2d7 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -6,23 +6,28 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): entity = Person label = "eligible person for CTR non-dependent deduction" documentation = ( - "An adult outside the claimant's family who is not liable for rent. " - "Someone jointly and severally liable for the council tax with the " - "claimant (a joint tenant or other sharer of the rent) and someone " - "liable to the claimant on a commercial basis (a boarder or lodger) " - "is not a non-dependant." + "A non-dependant aged 18 or over: an adult outside the claimant's " + "family who is not liable for rent, or a member of a benefit unit who " + "is not its claimant, partner or a child or young person (see " + "is_benefit_unit_non_dependant_for_legacy_benefits). Someone jointly " + "and severally liable for the council tax with the claimant (a joint " + "tenant or other sharer of the rent) and someone liable to the " + "claimant on a commercial basis (a boarder or lodger) is not a " + "non-dependant; an adult in their benefit unit who is not liable is." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", - "https://www.legislation.gov.uk/wsi/2013/3029", - "https://www.legislation.gov.uk/ssi/2021/249", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/9", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/8", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/3", ) def formula(person, period, parameters): - # SI 2012/2885 reg 9(2)(d)-(e) and the Welsh and Scottish equivalents. - return ( - (person("age", period) >= 18) - & ~person.benunit("benunit_contains_household_head", period) - & ~person.benunit("benunit_is_rent_liable", period) - ) + # SI 2012/2885 reg 9(2)(a), (c)-(e) and the Welsh and Scottish + # equivalents; deductions are for non-dependants aged 18 or over. + other_family = ~person.benunit( + "benunit_contains_household_head", period + ) & ~person.benunit("benunit_is_rent_liable", period) + own_family = person("is_benefit_unit_non_dependant_for_legacy_benefits", period) + return (person("age", period) >= 18) & (other_family | own_family) 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 6241abf4c7..e45f392c5d 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 @@ -5,16 +5,29 @@ class council_tax_reduction_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "CTR non-dependent deductions" + documentation = ( + "Deductions for the applicant's non-dependants, including those in " + "the applicant's own benefit unit, for a family that claims. A " + "non-dependant of two or more jointly liable people is apportioned " + "equally between them." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/9", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + ) def formula(benunit, period, parameters): + # Every eligible person in the household is a non-dependant of each + # claiming family: the claimant's own family has only its benefit-unit + # non-dependants, and jointly liable sharers are not non-dependants. 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) # A non-dependant of two or more jointly liable people is apportioned # equally between them (SI 2012/2885 Sch 1 para 8(5)). share = benunit("council_tax_reduction_joint_liability_share", period) - return (deductions_in_household - deductions_in_benunit) * share + claims = benunit("council_tax_reduction_claimant_benunit", period) + return claims * deductions_in_household * share From 93e810efb5ab4d80d0840fc7c912d9c61da5567c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 15:12:30 -0400 Subject: [PATCH 2/8] Test that a disabled child does not exempt the UC renter from housing cost contributions UC Regs 2013 Sch 4 para 15(1)(a) turns on the renter's own circumstances; found by a mutation that applied the exemption to every family member. Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index 3efb8dabd7..f4b44e503d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -169,6 +169,34 @@ output: uc_non_dep_deductions: 0 +# Para 15(1)(a) exempts "any renter who is a single person to whom +# sub-paragraph (2) applies": the renter's own disability, not a child's. +- name: UC, a disabled child in the renter's family does not exempt the renter + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + child: + age: 10 + is_claimant_or_partner: false + dla_sc_category: MIDDLE + benunits: + benunit: + members: [parent, son, child] + households: + household: + members: [parent, son, child] + output: + uc_non_dep_deductions_renter_exempt: false + uc_non_dep_deductions: 1_158.60 + # Sch 4 para 16(2)(d): no contribution for a non-dependant receiving # Attendance Allowance. - name: UC, no contribution for a non-dependant in the benefit unit receiving Attendance Allowance From 547b9693722099ede55d28b566d89c38d2316211 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 15:21:42 -0400 Subject: [PATCH 3/8] Pin the child age floor and HB/CTR eligibility in the in-unit tests Mutation testing (15 one-line mutations of the new logic) left the in-unit age floor surviving the YAML tests, and dropping in-unit non-dependants from HB or CTR eligibility surviving the properties. The YAML case now checks a child's classification, and the oracle checks HB and CTR eligibility independently. All 15 mutations are now killed by both. Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 3 +++ ...t_benefit_unit_non_dependant_properties.py | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index f4b44e503d..fc8ef67bbd 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -194,6 +194,9 @@ household: members: [parent, son, child] output: + # A child under 16 is never a non-dependant (WRA 2012 s.40). + is_benefit_unit_non_dependant_for_universal_credit: [false, true, false] + is_benefit_unit_non_dependant_for_legacy_benefits: [false, true, false] uc_non_dep_deductions_renter_exempt: false uc_non_dep_deductions: 1_158.60 diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index 1702d26e6e..acdc9821f5 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -7,9 +7,10 @@ population of households: 1. Oracle: each family's Universal Credit, Housing Benefit and Council Tax - Reduction non-dependant deductions equal an independent calculation from - the generated household structure: who is a non-dependant, of whom, and - how a non-dependant of several joint occupiers is apportioned. + Reduction non-dependant deductions, and who is eligible for an individual + deduction, equal an independent calculation from the generated household + structure: who is a non-dependant, of whom, and how a non-dependant of + several joint occupiers is apportioned. 2. Conservation: every Housing Benefit and Council Tax Reduction non-dependant deduction is borne in full, once, across the household's families; every Universal Credit contribution is borne by exactly one @@ -282,6 +283,23 @@ def test_deductions_match_the_oracle(population): assert np.allclose( calc(sim, "council_tax_reduction_non_dep_deductions"), exp_ctr, atol=0.01 ) + # HB and CTR eligibility: a non-dependant aged 18 or over, either in a + # family not liable for rent or within their own benefit unit. + expected_eligible = np.array( + [ + row["age"] >= 18 + and ( + family_rows[row["family"]]["role"] == "non_dependant" + or row["kind"] == "non_dependant" + ) + for row in person_rows + ] + ) + for variable in [ + "housing_benefit_individual_non_dep_deduction_eligible", + "council_tax_reduction_individual_non_dep_deduction_eligible", + ]: + assert np.array_equal(calc(sim, variable) > 0, expected_eligible), variable @PROPERTY_SETTINGS From 155766bd865a0b9e02171da981f1bb23f04dfa70 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 02:30:51 -0400 Subject: [PATCH 4/8] Charge a joint occupier's own non-dependant to that occupier alone A non-dependant in a family's own benefit unit is that family's alone, not shared between joint occupiers: DWP guidance treats a joint tenant's sister as "Sarah's non-dependant" only (LHA Guidance Manual 2.093, example 2) and says "if a person is a non-dependant of only one of the joint occupiers, take the whole of the deduction from that joint occupier's entitlement" (HBGM A5 5.622). Reg 74(5) and CTR Sch 1 para 8(5) apportionment now apply only to non-dependants from other families. A lodger's family is one commercial arrangement for CTR too, so its adult son is no one's non-dependant there. Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 81 ++++++++++++++++-- ...t_benefit_unit_non_dependant_properties.py | 82 +++++++++++++------ .../housing_benefit_non_dep_deductions.py | 30 ++++--- ...n_individual_non_dep_deduction_eligible.py | 20 +++-- ...ouncil_tax_reduction_non_dep_deductions.py | 32 +++++--- 5 files changed, 178 insertions(+), 67 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index fc8ef67bbd..c4ec323a09 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -448,9 +448,10 @@ is_benefit_unit_non_dependant_for_legacy_benefits: [false, false] housing_benefit_non_dep_deductions: 0 -# HB reg 74(5): a non-dependant of two joint occupiers is apportioned by their -# shares of the rent, whichever family the non-dependant belongs to. -- name: HB, joint occupiers share the deduction for an adult son in one of their families +# A joint occupier's own adult son is that occupier's non-dependant only (LHA +# Guidance Manual 2.093, example 2), so reg 74(5) apportionment does not apply +# and his family bears the whole deduction, though it pays half the rent. +- name: HB, a joint tenant's adult son is that tenant's non-dependant only period: 2026 absolute_error_margin: 0.01 input: @@ -479,7 +480,7 @@ rent: 12_000 output: share_of_household_rent: [0.5, 0.5] - housing_benefit_non_dep_deductions: [530.40, 530.40] + housing_benefit_non_dep_deductions: [1_060.80, 0] # A lodger's family has no share of the household's rent: it bears only the # non-dependant in its own benefit unit. @@ -559,9 +560,9 @@ council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] council_tax_reduction_non_dep_deductions: 0 -# SI 2012/2885 Sch 1 para 8(5): a non-dependant of two jointly liable people -# is apportioned equally between them, wherever the non-dependant's family. -- name: CTR, jointly liable families share the deduction for an adult son in one of them +# An applicant's own adult son is that applicant's non-dependant only, so SI +# 2012/2885 Sch 1 para 8(5) apportionment does not apply. +- name: CTR, a jointly liable applicant's adult son is that applicant's non-dependant only period: 2026 absolute_error_margin: 0.01 input: @@ -589,7 +590,71 @@ rent: 12_000 output: council_tax_reduction_joint_liability_share: [0.5, 0.5] - council_tax_reduction_non_dep_deductions: [150, 150] + council_tax_reduction_non_dep_deductions: [300, 0] + +# Para 8(5) still apportions a non-dependant from another family between the +# jointly liable people. +- name: CTR, jointly liable families share the deduction for another family's adult + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + sharer: + age: 48 + is_claimant_or_partner: true + friend: + age: 30 + is_claimant_or_partner: true + council_tax_reduction_individual_non_dep_deduction: 300 + benunits: + benunit_1: + members: [head] + benunit_2: + members: [sharer] + liable_for_share_of_household_rent: true + benunit_3: + members: [friend] + households: + household: + members: [head, sharer, friend] + rent: 12_000 + output: + council_tax_reduction_non_dep_deductions: [150, 150, 0] + +# A lodger's family is one commercial arrangement with the applicant (SI +# 2012/2885 reg 9(2)(e)), so the lodger's adult son is no one's non-dependant. +- name: CTR, a lodger's adult son is not the applicant's non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 60 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 45 + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + lodger_son: + age: 21 + is_claimant_or_partner: false + benunits: + benunit_1: + members: [head] + benunit_2: + members: [lodger, lodger_son] + households: + household: + members: [head, lodger, lodger_son] + rent: 9_000 + output: + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false] + council_tax_reduction_non_dep_deductions: [0, 0] # Only a family that claims bears deductions; the non-dependant's own family # does not. diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index acdc9821f5..d2f07529b9 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -10,11 +10,14 @@ Reduction non-dependant deductions, and who is eligible for an individual deduction, equal an independent calculation from the generated household structure: who is a non-dependant, of whom, and how a non-dependant of - several joint occupiers is apportioned. -2. Conservation: every Housing Benefit and Council Tax Reduction - non-dependant deduction is borne in full, once, across the household's - families; every Universal Credit contribution is borne by exactly one - family unless that family's renter is exempt. + several joint occupiers is apportioned (another family's by rent or + liability share; a family's own non-dependant is that family's alone). +2. Conservation: every Housing Benefit non-dependant deduction is borne in + full, once, across the household's families; a Council Tax Reduction + claiming family bears its own non-dependants in full and another family's + in proportion to its joint-liability share; every Universal Credit + contribution is borne by exactly one family unless that family's renter + is exempt. 3. Exemptions: a family whose renter is exempt under Sch 4 para 15 has no Universal Credit deductions; no deduction is charged for a non-dependant under 21 (UC) or under 18 (HB, CTR), or for a qualifying young person. @@ -241,17 +244,22 @@ def oracle(person_rows, family_rows, uc, hb, ctr, pension_credit, claims, ctr_sh bearer = head if other_family else f if uc_non_dependant and not family_rows[bearer]["renter_pip"]: exp_uc[bearer] += uc[p] - # HB: pooled across joint occupiers by share; a lodger's own - # non-dependants fall on the lodger. - if other_family or (own_family and share[f] > 0): + # HB: another family's non-dependant is pooled across joint + # occupiers by rent share; a family's own non-dependant is its + # alone (LHA Guidance Manual 2.093), a lodger's included. + if other_family: for i in fams: exp_hb[i] += share[i] * hb[p] elif own_family: exp_hb[f] += hb[p] - # CTR: every non-dependant is pooled across the claiming families. - if other_family or own_family: + # CTR: another family's non-dependant is pooled across the + # claiming families; a claiming family's own is its alone; a + # lodger's family is no one's. + if other_family: for i in fams: exp_ctr[i] += claims[i] * ctr_share[i] * ctr[p] + elif own_family and role in ("head", "sharer"): + exp_ctr[f] += ctr[p] return exp_uc, exp_hb, exp_ctr, expected_uc_individual @@ -283,23 +291,34 @@ def test_deductions_match_the_oracle(population): assert np.allclose( calc(sim, "council_tax_reduction_non_dep_deductions"), exp_ctr, atol=0.01 ) + # HB and CTR eligibility: a non-dependant aged 18 or over, either in a # family not liable for rent or within their own benefit unit. - expected_eligible = np.array( - [ - row["age"] >= 18 - and ( - family_rows[row["family"]]["role"] == "non_dependant" - or row["kind"] == "non_dependant" - ) - for row in person_rows - ] + def eligible(own_family_roles): + return np.array( + [ + row["age"] >= 18 + and ( + family_rows[row["family"]]["role"] == "non_dependant" + or ( + row["kind"] == "non_dependant" + and family_rows[row["family"]]["role"] in own_family_roles + ) + ) + for row in person_rows + ] + ) + + # HB: any family's own non-dependants; CTR: only a claiming family's (the + # household head's or a sharer's), not a boarder's or lodger's. + assert np.array_equal( + calc(sim, "housing_benefit_individual_non_dep_deduction_eligible") > 0, + eligible(("head", "sharer", "lodger", "non_dependant")), + ) + assert np.array_equal( + calc(sim, "council_tax_reduction_individual_non_dep_deduction_eligible") > 0, + eligible(("head", "sharer", "non_dependant")), ) - for variable in [ - "housing_benefit_individual_non_dep_deduction_eligible", - "council_tax_reduction_individual_non_dep_deduction_eligible", - ]: - assert np.array_equal(calc(sim, variable) > 0, expected_eligible), variable @PROPERTY_SETTINGS @@ -314,12 +333,23 @@ def test_conservation_and_exemptions(population): ) hb_people = sim.map_result(v["hb"], "person", "household") assert np.allclose(hb_families, hb_people, atol=0.01) + # CTR: a claiming family's own non-dependant is borne by it in full; + # another family's is borne by the claiming families in proportion to + # their joint-liability shares (which follow the regulations' per-person + # wording, so they need not sum to one). claim_shares = sim.map_result(v["claims"] * v["ctr_share"], "benunit", "household") ctr_families = sim.map_result( calc(sim, "council_tax_reduction_non_dep_deductions"), "benunit", "household" ) - ctr_people = sim.map_result(v["ctr"], "person", "household") - assert np.allclose(ctr_families, claim_shares * ctr_people, atol=0.01) + own_of_claimant = np.array( + [ + row["kind"] == "non_dependant" and v["claims"][row["family"]] > 0 + for row in person_rows + ] + ) + ctr_own = sim.map_result(v["ctr"] * own_of_claimant, "person", "household") + ctr_other = sim.map_result(v["ctr"] * ~own_of_claimant, "person", "household") + assert np.allclose(ctr_families, claim_shares * ctr_other + ctr_own, atol=0.01) # UC: each contribution is borne once, except where the bearer is exempt. uc_families = sim.map_result( calc(sim, "uc_non_dep_deductions"), "benunit", "household" 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 03adebf196..f3f93c6dfe 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 @@ -9,9 +9,10 @@ class housing_benefit_non_dep_deductions(Variable): "Deductions for the claimant's non-dependants: members of other " "families of the household who are not liable for rent, and members " "of a family liable for the rent who are not its claimant, partner or " - "a child or young person. A non-dependant of several joint occupiers " - "is apportioned between them by their shares of the rent; a boarder " - "or lodger bears only the non-dependants in their own family." + "a child or young person. A non-dependant from another family is " + "apportioned between joint occupiers by their shares of the rent. A " + "non-dependant in a family's own benefit unit is that family's alone, " + "including a boarder's or lodger's." ) definition_period = YEAR unit = GBP @@ -20,6 +21,8 @@ class housing_benefit_non_dep_deductions(Variable): "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", "https://www.legislation.gov.uk/uksi/2006/214/regulation/3", "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + "https://assets.publishing.service.gov.uk/media/5a758238ed915d6faf2b38a2/lha-guidance-manual.pdf#page=33", + "https://assets.publishing.service.gov.uk/media/5a7ce2b840f0b6629523c64c/hbgm-a5-calculating-benefit.pdf", ) def formula(benunit, period, parameters): @@ -38,14 +41,17 @@ def formula(benunit, period, parameters): ) # A non-dependant of more than one joint occupier is apportioned # between them by their shares of the payments (reg 74(5); SPC reg - # 55(5)): one living with joint occupiers resides with each of them, - # whichever family they belong to. A boarder or lodger has no share - # of the household's rent and bears only their own family's. - joint_occupier = person.benunit("share_of_household_rent", period) > 0 - shared = other_family | (own_family & joint_occupier) + # 55(5)). A member of a joint occupier's own family unit is that + # occupier's non-dependant only (LHA Guidance Manual 2.093, example + # 2: a joint tenant's sister "is treated as Sarah's non-dependant"), + # and "if a person is a non-dependant of only one of the joint + # occupiers, take the whole of the deduction from that joint + # occupier's entitlement" (HBGM A5 5.622). Their family bears the + # whole deduction, whatever its share of the rent; so does a boarder + # or lodger. share = benunit("share_of_household_rent", period) - from_joint_occupation = share * benunit.max( - person.household.sum(deductions * shared) + from_other_families = share * benunit.max( + person.household.sum(deductions * other_family) ) - from_own_family = benunit.sum(deductions * own_family * ~joint_occupier) - return from_joint_occupation + from_own_family + from_own_family = benunit.sum(deductions * own_family) + return from_other_families + from_own_family diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 2ef332d2d7..f235efc452 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -7,13 +7,12 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): label = "eligible person for CTR non-dependent deduction" documentation = ( "A non-dependant aged 18 or over: an adult outside the claimant's " - "family who is not liable for rent, or a member of a benefit unit who " - "is not its claimant, partner or a child or young person (see " - "is_benefit_unit_non_dependant_for_legacy_benefits). Someone jointly " - "and severally liable for the council tax with the claimant (a joint " - "tenant or other sharer of the rent) and someone liable to the " - "claimant on a commercial basis (a boarder or lodger) is not a " - "non-dependant; an adult in their benefit unit who is not liable is." + "family who is not liable for rent, or a member of a claiming " + "family's benefit unit who is not its claimant, partner or a child or " + "young person (see is_benefit_unit_non_dependant_for_legacy_benefits). " + "Someone jointly and severally liable for the council tax with the " + "claimant (a joint tenant or other sharer of the rent) and a boarder " + "or lodger, with their family, is not a non-dependant." ) definition_period = YEAR reference = ( @@ -29,5 +28,10 @@ def formula(person, period, parameters): other_family = ~person.benunit( "benunit_contains_household_head", period ) & ~person.benunit("benunit_is_rent_liable", period) - own_family = person("is_benefit_unit_non_dependant_for_legacy_benefits", period) + # A member of a claiming family's own benefit unit who is not in its + # claimant's family. A boarder's or lodger's family is one commercial + # arrangement, so its members are no one's non-dependants. + own_family = person( + "is_benefit_unit_non_dependant_for_legacy_benefits", period + ) & person.benunit("council_tax_reduction_claimant_benunit", period) return (person("age", period) >= 18) & (other_family | own_family) 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 e45f392c5d..d2498f1126 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 @@ -6,10 +6,9 @@ class council_tax_reduction_non_dep_deductions(Variable): entity = BenUnit label = "CTR non-dependent deductions" documentation = ( - "Deductions for the applicant's non-dependants, including those in " - "the applicant's own benefit unit, for a family that claims. A " - "non-dependant of two or more jointly liable people is apportioned " - "equally between them." + "Deductions for a claiming family's non-dependants: those in its own " + "benefit unit, in full, and those in other families, apportioned " + "equally between the jointly liable people." ) definition_period = YEAR unit = GBP @@ -19,15 +18,22 @@ class council_tax_reduction_non_dep_deductions(Variable): ) def formula(benunit, period, parameters): - # Every eligible person in the household is a non-dependant of each - # claiming family: the claimant's own family has only its benefit-unit - # non-dependants, and jointly liable sharers are not non-dependants. - deductions = benunit.members( + person = benunit.members + deductions = person( "council_tax_reduction_individual_non_dep_deduction", period ) - deductions_in_household = benunit.max(benunit.members.household.sum(deductions)) - # A non-dependant of two or more jointly liable people is apportioned - # equally between them (SI 2012/2885 Sch 1 para 8(5)). - share = benunit("council_tax_reduction_joint_liability_share", period) claims = benunit("council_tax_reduction_claimant_benunit", period) - return claims * deductions_in_household * share + # A non-dependant in a claiming family's own benefit unit is that + # applicant's alone (as for HB: LHA Guidance Manual 2.093, example 2). + own_family = person( + "is_benefit_unit_non_dependant_for_legacy_benefits", period + ) & person.benunit("council_tax_reduction_claimant_benunit", period) + from_own_family = benunit.sum(deductions * own_family) + # A non-dependant from another family is a non-dependant of each + # jointly liable person, apportioned equally between them (SI + # 2012/2885 Sch 1 para 8(5)). + share = benunit("council_tax_reduction_joint_liability_share", period) + from_other_families = share * benunit.max( + person.household.sum(deductions * ~own_family) + ) + return claims * (from_other_families + from_own_family) From 2d546285355ec9b7846a610cded321106ae2f022 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 04:34:56 -0400 Subject: [PATCH 5/8] Apportion every non-dependant between joint occupiers, as the regulations say MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review r2 (F3, F4, F5) found three departures from the text. F3. 155766bd8 charged a joint occupier's own adult son wholly to that occupier, citing LHA Guidance Manual 2.093 example 2 and HBGM A5 5.622. HB reg 3(1) and (4) and CTR reg 9(1) make him a non-dependant of every joint occupier he shares accommodation with: none of reg 3(2)(a)-(f) excludes him in relation to the other occupier. Reg 74(5) and CTR Sch 1 para 8(5) then apportion him. The LHA example concerns the size criteria and a sister, whom the model already treats as another family's non-dependant and apportions; 5.622 is conditional. The old rule also made the result depend on whether the survey codes the son in his parent's benefit unit or his own. HB and national CTR now pool every non-dependant, in any family, and apportion by rent share (HB) or joint-liability share (CTR). F4. A lodger's adult son was no one's non-dependant for CTR. Reg 9(2)(e) excludes only the person liable to pay the applicant, as HB reg 3(2)(e)(i) does; the household extension in (iii) is for the landlord's side. He is now eligible and is the householder's non-dependant for CTR and HB, and still the lodger's own for HB, in full, since they are not joint occupiers. F5. Widening the shared CTR eligibility made a jointly liable sharer's local claim bear the head's own son (Newham: £489.84 in 2025). The local aggregation now counts only other families' non-dependants, as before, so this change is neutral for the five local schemes until each council's aggregation is brought into line. YAML: joint tenants share one tenant's son by rent share (and get the same result with him coded in his own unit); jointly liable applicants share one's son equally; a lodger's son is the householder's non-dependant (HB and CTR) and the lodger's (HB); Newham sharer does not bear the head's son. Property oracle and conservation invariants restated for pooling. Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 116 +++++++++++++++--- ...t_benefit_unit_non_dependant_properties.py | 87 +++++++------ .../housing_benefit_non_dep_deductions.py | 38 +++--- .../council_tax_reduction/_legacy.py | 10 +- ...n_individual_non_dep_deduction_eligible.py | 25 ++-- ...ouncil_tax_reduction_non_dep_deductions.py | 31 ++--- 6 files changed, 197 insertions(+), 110 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index c4ec323a09..51d2b8e129 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -448,10 +448,10 @@ is_benefit_unit_non_dependant_for_legacy_benefits: [false, false] housing_benefit_non_dep_deductions: 0 -# A joint occupier's own adult son is that occupier's non-dependant only (LHA -# Guidance Manual 2.093, example 2), so reg 74(5) apportionment does not apply -# and his family bears the whole deduction, though it pays half the rent. -- name: HB, a joint tenant's adult son is that tenant's non-dependant only +# A joint tenant's adult son normally resides with both joint tenants (reg +# 3(1) and (4)) and is in neither's family under reg 3(2)(a), so he is a +# non-dependant of each, apportioned by their shares of the rent (reg 74(5)). +- name: HB, joint tenants share the deduction for one tenant's adult son period: 2026 absolute_error_margin: 0.01 input: @@ -480,11 +480,47 @@ rent: 12_000 output: share_of_household_rent: [0.5, 0.5] - housing_benefit_non_dep_deductions: [1_060.80, 0] + housing_benefit_non_dep_deductions: [530.40, 530.40] -# A lodger's family has no share of the household's rent: it bears only the -# non-dependant in its own benefit unit. -- name: HB, a lodger bears the non-dependant in their own benefit unit +# The same son coded as his own benefit unit gives the same deductions: the +# result does not depend on how the survey groups him. +- name: HB, joint tenants share the deduction for an adult son in his own benefit unit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: true + employment_income: 2_600 + weekly_hours: 10 + sharer: + age: 48 + is_claimant_or_partner: true + benunits: + benunit_1: + members: [head] + benunit_2: + members: [sharer] + liable_for_share_of_household_rent: true + benunit_3: + members: [son] + households: + household: + members: [head, son, sharer] + rent: 12_000 + output: + housing_benefit_non_dep_deductions: [530.40, 530.40, 0] + +# A lodger's adult son is the lodger's non-dependant, and the householder's +# too: reg 3(2)(e)(i) excludes only the person liable to pay the householder. +# The lodger is not a joint occupier with the householder, so reg 74(5) does +# not apportion him and each bears the whole deduction. +- name: HB, a lodger's adult son is a non-dependant of the lodger and of the householder period: 2026 absolute_error_margin: 0.01 input: @@ -512,7 +548,7 @@ members: [head, lodger, lodger_son] rent: 9_000 output: - housing_benefit_non_dep_deductions: [0, 1_060.80] + housing_benefit_non_dep_deductions: [1_060.80, 1_060.80] - name: CTR, the claimant's family bears the non-dependant in its benefit unit period: 2026 @@ -560,9 +596,10 @@ council_tax_reduction_individual_non_dep_deduction_eligible: [false, false] council_tax_reduction_non_dep_deductions: 0 -# An applicant's own adult son is that applicant's non-dependant only, so SI -# 2012/2885 Sch 1 para 8(5) apportionment does not apply. -- name: CTR, a jointly liable applicant's adult son is that applicant's non-dependant only +# An applicant's adult son normally resides with both jointly liable people +# and is in neither's family (SI 2012/2885 reg 9(1), (2)(a)), so the deduction +# is apportioned equally between them (Sch 1 para 8(5)). +- name: CTR, jointly liable applicants share the deduction for one applicant's adult son period: 2026 absolute_error_margin: 0.01 input: @@ -590,7 +627,7 @@ rent: 12_000 output: council_tax_reduction_joint_liability_share: [0.5, 0.5] - council_tax_reduction_non_dep_deductions: [300, 0] + council_tax_reduction_non_dep_deductions: [150, 150] # Para 8(5) still apportions a non-dependant from another family between the # jointly liable people. @@ -625,9 +662,10 @@ output: council_tax_reduction_non_dep_deductions: [150, 150, 0] -# A lodger's family is one commercial arrangement with the applicant (SI -# 2012/2885 reg 9(2)(e)), so the lodger's adult son is no one's non-dependant. -- name: CTR, a lodger's adult son is not the applicant's non-dependant +# SI 2012/2885 reg 9(2)(e) excludes only the person liable to pay the +# applicant, so a lodger's adult son who pays nothing himself is the +# applicant's non-dependant. +- name: CTR, a lodger's adult son is the applicant's non-dependant period: 2026 absolute_error_margin: 0.01 input: @@ -643,6 +681,7 @@ lodger_son: age: 21 is_claimant_or_partner: false + council_tax_reduction_individual_non_dep_deduction: 300 benunits: benunit_1: members: [head] @@ -653,8 +692,8 @@ members: [head, lodger, lodger_son] rent: 9_000 output: - council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, false] - council_tax_reduction_non_dep_deductions: [0, 0] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true] + council_tax_reduction_non_dep_deductions: [300, 0] # Only a family that claims bears deductions; the non-dependant's own family # does not. @@ -686,3 +725,44 @@ output: council_tax_reduction_individual_non_dep_deduction_eligible: [false, true, true] council_tax_reduction_non_dep_deductions: [300, 0] + +# The English working-age local schemes keep their own aggregation, which +# charges only other families' non-dependants. Widening the shared +# eligibility must not make a jointly liable sharer bear the head's own son. +- name: Local CTR (Newham), a jointly liable sharer does not bear the head's own in-unit son + period: 2025 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + son: + age: 25 + is_claimant_or_partner: false + employment_income: 20_000 + weekly_hours: 37.5 + sharer: + age: 48 + is_claimant_or_partner: true + benunits: + benunit_1: + members: [head, son] + would_claim_council_tax_reduction: true + benunit_2: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_council_tax_reduction: true + households: + household: + members: [head, son, sharer] + rent: 12_000 + region: LONDON + local_authority: NEWHAM + tenure_type: RENT_PRIVATELY + council_tax: 1_800 + output: + council_tax_reduction_joint_liability_share: [0.5, 0.5] + newham_council_tax_reduction_non_dep_deductions: [0, 0] + newham_council_tax_reduction: [630, 630] diff --git a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py index d2f07529b9..45a5ab4714 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -10,14 +10,14 @@ Reduction non-dependant deductions, and who is eligible for an individual deduction, equal an independent calculation from the generated household structure: who is a non-dependant, of whom, and how a non-dependant of - several joint occupiers is apportioned (another family's by rent or - liability share; a family's own non-dependant is that family's alone). + several joint occupiers is apportioned (by rent or liability share, + whichever family the non-dependant is in). 2. Conservation: every Housing Benefit non-dependant deduction is borne in - full, once, across the household's families; a Council Tax Reduction - claiming family bears its own non-dependants in full and another family's - in proportion to its joint-liability share; every Universal Credit - contribution is borne by exactly one family unless that family's renter - is exempt. + full across the joint occupiers, and once more by a boarder or lodger for + a non-dependant in its own benefit unit; each Council Tax Reduction + claiming family bears every non-dependant in proportion to its + joint-liability share; every Universal Credit contribution is borne by + exactly one family unless that family's renter is exempt. 3. Exemptions: a family whose renter is exempt under Sch 4 para 15 has no Universal Credit deductions; no deduction is charged for a non-dependant under 21 (UC) or under 18 (HB, CTR), or for a qualifying young person. @@ -244,22 +244,22 @@ def oracle(person_rows, family_rows, uc, hb, ctr, pension_credit, claims, ctr_sh bearer = head if other_family else f if uc_non_dependant and not family_rows[bearer]["renter_pip"]: exp_uc[bearer] += uc[p] - # HB: another family's non-dependant is pooled across joint - # occupiers by rent share; a family's own non-dependant is its - # alone (LHA Guidance Manual 2.093), a lodger's included. - if other_family: + # HB: every non-dependant, in any family, is pooled across the + # joint occupiers by rent share; a boarder or lodger, who is not + # a joint occupier, also bears its own in full. + if other_family or own_family: for i in fams: exp_hb[i] += share[i] * hb[p] - elif own_family: - exp_hb[f] += hb[p] - # CTR: another family's non-dependant is pooled across the - # claiming families; a claiming family's own is its alone; a - # lodger's family is no one's. - if other_family: + if own_family and role == "lodger": + exp_hb[f] += hb[p] + # CTR: every non-dependant, a lodger's included, is pooled across + # the claiming families by joint-liability share, except that a + # family's own claimant or partner is not its non-dependant. + if other_family or own_family: for i in fams: + if i == f and not own_family: + continue exp_ctr[i] += claims[i] * ctr_share[i] * ctr[p] - elif own_family and role in ("head", "sharer"): - exp_ctr[f] += ctr[p] return exp_uc, exp_hb, exp_ctr, expected_uc_individual @@ -309,16 +309,15 @@ def eligible(own_family_roles): ] ) - # HB: any family's own non-dependants; CTR: only a claiming family's (the - # household head's or a sharer's), not a boarder's or lodger's. - assert np.array_equal( - calc(sim, "housing_benefit_individual_non_dep_deduction_eligible") > 0, - eligible(("head", "sharer", "lodger", "non_dependant")), - ) - assert np.array_equal( - calc(sim, "council_tax_reduction_individual_non_dep_deduction_eligible") > 0, - eligible(("head", "sharer", "non_dependant")), - ) + # Any family's own non-dependants, a boarder's or lodger's included. + for variable in ( + "housing_benefit_individual_non_dep_deduction_eligible", + "council_tax_reduction_individual_non_dep_deduction_eligible", + ): + assert np.array_equal( + calc(sim, variable) > 0, + eligible(("head", "sharer", "lodger", "non_dependant")), + ) @PROPERTY_SETTINGS @@ -327,29 +326,29 @@ def test_conservation_and_exemptions(population): sim, person_rows, family_rows, v = run(population) ages = np.array([row["age"] for row in person_rows]) kinds = np.array([row["kind"] for row in person_rows]) - # 2. Conservation: HB and CTR deductions are borne once in full. + # 2. Conservation. HB: each deduction is borne in full across the joint + # occupiers, and a lodger's own non-dependant once more by the lodger. hb_families = sim.map_result( calc(sim, "housing_benefit_non_dep_deductions"), "benunit", "household" ) - hb_people = sim.map_result(v["hb"], "person", "household") + lodgers_own = np.array( + [ + row["kind"] == "non_dependant" + and family_rows[row["family"]]["role"] == "lodger" + for row in person_rows + ] + ) + hb_people = sim.map_result(v["hb"] * (1 + lodgers_own), "person", "household") assert np.allclose(hb_families, hb_people, atol=0.01) - # CTR: a claiming family's own non-dependant is borne by it in full; - # another family's is borne by the claiming families in proportion to - # their joint-liability shares (which follow the regulations' per-person - # wording, so they need not sum to one). + # CTR: every non-dependant is borne by the claiming families in + # proportion to their joint-liability shares (which follow the + # regulations' per-person wording, so they need not sum to one). claim_shares = sim.map_result(v["claims"] * v["ctr_share"], "benunit", "household") ctr_families = sim.map_result( calc(sim, "council_tax_reduction_non_dep_deductions"), "benunit", "household" ) - own_of_claimant = np.array( - [ - row["kind"] == "non_dependant" and v["claims"][row["family"]] > 0 - for row in person_rows - ] - ) - ctr_own = sim.map_result(v["ctr"] * own_of_claimant, "person", "household") - ctr_other = sim.map_result(v["ctr"] * ~own_of_claimant, "person", "household") - assert np.allclose(ctr_families, claim_shares * ctr_other + ctr_own, atol=0.01) + ctr_people = sim.map_result(v["ctr"], "person", "household") + assert np.allclose(ctr_families, claim_shares * ctr_people, atol=0.01) # UC: each contribution is borne once, except where the bearer is exempt. uc_families = sim.map_result( calc(sim, "uc_non_dep_deductions"), "benunit", "household" 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 f3f93c6dfe..66fa7799c8 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 @@ -9,10 +9,10 @@ class housing_benefit_non_dep_deductions(Variable): "Deductions for the claimant's non-dependants: members of other " "families of the household who are not liable for rent, and members " "of a family liable for the rent who are not its claimant, partner or " - "a child or young person. A non-dependant from another family is " - "apportioned between joint occupiers by their shares of the rent. A " - "non-dependant in a family's own benefit unit is that family's alone, " - "including a boarder's or lodger's." + "a child or young person. Each non-dependant is apportioned between " + "joint occupiers by their shares of the rent, whichever family they " + "are in. A boarder or lodger also bears the whole deduction for a " + "non-dependant in its own benefit unit." ) definition_period = YEAR unit = GBP @@ -21,7 +21,6 @@ class housing_benefit_non_dep_deductions(Variable): "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", "https://www.legislation.gov.uk/uksi/2006/214/regulation/3", "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", - "https://assets.publishing.service.gov.uk/media/5a758238ed915d6faf2b38a2/lha-guidance-manual.pdf#page=33", "https://assets.publishing.service.gov.uk/media/5a7ce2b840f0b6629523c64c/hbgm-a5-calculating-benefit.pdf", ) @@ -39,19 +38,20 @@ def formula(benunit, period, parameters): person("is_benefit_unit_non_dependant_for_legacy_benefits", period) & ~other_family ) - # A non-dependant of more than one joint occupier is apportioned - # between them by their shares of the payments (reg 74(5); SPC reg - # 55(5)). A member of a joint occupier's own family unit is that - # occupier's non-dependant only (LHA Guidance Manual 2.093, example - # 2: a joint tenant's sister "is treated as Sarah's non-dependant"), - # and "if a person is a non-dependant of only one of the joint - # occupiers, take the whole of the deduction from that joint - # occupier's entitlement" (HBGM A5 5.622). Their family bears the - # whole deduction, whatever its share of the rent; so does a boarder - # or lodger. + # A non-dependant normally resides with each joint occupier (reg 3(1) + # and (4)), whichever family they belong to: another family's adult, + # a joint occupier's own adult son, or a boarder's or lodger's (reg + # 3(2)(e)(i) excludes only the person liable to pay the claimant). + # One who is a non-dependant of more than one joint occupier is + # apportioned between them by their shares of the payments (reg + # 74(5); SPC reg 55(5)). share = benunit("share_of_household_rent", period) - from_other_families = share * benunit.max( - person.household.sum(deductions * other_family) + from_joint_occupiers = share * benunit.max( + person.household.sum(deductions * (other_family | own_family)) ) - from_own_family = benunit.sum(deductions * own_family) - return from_other_families + from_own_family + # A boarder or lodger is not a joint occupier with the householder, + # so it also bears the whole deduction for a non-dependant in its own + # benefit unit. The model charges it for no one else. + joint_occupier = share > 0 + from_own_family = where(joint_occupier, 0, benunit.sum(deductions * own_family)) + return from_joint_occupiers + from_own_family diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index 966d230ff7..840f829137 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -74,7 +74,15 @@ def local_non_dep_deductions( individual_deduction_variable, one_deduction_for_uc_couples=True, ): - deductions = benunit.members(individual_deduction_variable, period) + # The local schemes still charge only the non-dependants of other + # families. Non-dependants within a family's own benefit unit, now + # eligible for the national schemes, are left out here until each + # council's aggregation is brought into line. + person = benunit.members + other_family = ~person.benunit( + "benunit_contains_household_head", period + ) & ~person.benunit("benunit_is_rent_liable", period) + deductions = person(individual_deduction_variable, period) * other_family deduction_for_benunit = benunit.max(deductions) if not one_deduction_for_uc_couples: has_uc = benunit("universal_credit", period) > 0 diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index f235efc452..2f72ade201 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -7,12 +7,13 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): label = "eligible person for CTR non-dependent deduction" documentation = ( "A non-dependant aged 18 or over: an adult outside the claimant's " - "family who is not liable for rent, or a member of a claiming " - "family's benefit unit who is not its claimant, partner or a child or " - "young person (see is_benefit_unit_non_dependant_for_legacy_benefits). " - "Someone jointly and severally liable for the council tax with the " - "claimant (a joint tenant or other sharer of the rent) and a boarder " - "or lodger, with their family, is not a non-dependant." + "family who is not liable for rent, or a member of any family's " + "benefit unit who is not its claimant, partner or a child or young " + "person (see is_benefit_unit_non_dependant_for_legacy_benefits), " + "including a boarder's or lodger's adult son. Someone jointly and " + "severally liable for the council tax with the claimant (a joint " + "tenant or other sharer of the rent) and a boarder or lodger who pays " + "the claimant are not non-dependants." ) definition_period = YEAR reference = ( @@ -28,10 +29,8 @@ def formula(person, period, parameters): other_family = ~person.benunit( "benunit_contains_household_head", period ) & ~person.benunit("benunit_is_rent_liable", period) - # A member of a claiming family's own benefit unit who is not in its - # claimant's family. A boarder's or lodger's family is one commercial - # arrangement, so its members are no one's non-dependants. - own_family = person( - "is_benefit_unit_non_dependant_for_legacy_benefits", period - ) & person.benunit("council_tax_reduction_claimant_benunit", period) - return (person("age", period) >= 18) & (other_family | own_family) + # A member of any family's benefit unit who is not in its claimant's + # family. Reg 9(2)(e) excludes only the person liable to pay the + # applicant, so a boarder's or lodger's adult son is a non-dependant. + in_unit = person("is_benefit_unit_non_dependant_for_legacy_benefits", period) + return (person("age", period) >= 18) & (other_family | in_unit) 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 d2498f1126..b2589bfae4 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 @@ -6,9 +6,9 @@ class council_tax_reduction_non_dep_deductions(Variable): entity = BenUnit label = "CTR non-dependent deductions" documentation = ( - "Deductions for a claiming family's non-dependants: those in its own " - "benefit unit, in full, and those in other families, apportioned " - "equally between the jointly liable people." + "Deductions for a claiming family's non-dependants, whether in its " + "own benefit unit or another family's, apportioned equally between " + "the jointly liable people." ) definition_period = YEAR unit = GBP @@ -23,17 +23,18 @@ def formula(benunit, period, parameters): "council_tax_reduction_individual_non_dep_deduction", period ) claims = benunit("council_tax_reduction_claimant_benunit", period) - # A non-dependant in a claiming family's own benefit unit is that - # applicant's alone (as for HB: LHA Guidance Manual 2.093, example 2). - own_family = person( + # Every eligible non-dependant normally resides with each liable + # person (SI 2012/2885 reg 9(1)): another family's adult, an + # applicant's own adult son, or a boarder's or lodger's (reg 9(2)(e) + # excludes only the person liable to pay the applicant). A + # non-dependant of two or more jointly liable people is apportioned + # equally between them (Sch 1 para 8(5)). Members of the applicant's + # own family are not its non-dependants (reg 9(2)(a)). + own_family_member = ~person( "is_benefit_unit_non_dependant_for_legacy_benefits", period - ) & person.benunit("council_tax_reduction_claimant_benunit", period) - from_own_family = benunit.sum(deductions * own_family) - # A non-dependant from another family is a non-dependant of each - # jointly liable person, apportioned equally between them (SI - # 2012/2885 Sch 1 para 8(5)). - share = benunit("council_tax_reduction_joint_liability_share", period) - from_other_families = share * benunit.max( - person.household.sum(deductions * ~own_family) ) - return claims * (from_other_families + from_own_family) + non_dependants = benunit.max(person.household.sum(deductions)) - benunit.sum( + deductions * own_family_member + ) + share = benunit("council_tax_reduction_joint_liability_share", period) + return claims * share * non_dependants From 8aaf5a505efb47697e9da59212e253f69bb2da80 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 04:41:53 -0400 Subject: [PATCH 6/8] Keep CTR eligibility and the local mask on one other-family definition The local schemes' aggregation masks the shared individual deductions to members of other families not liable for rent, so it stays as on base while the national eligibility also covers in-unit non-dependants. That mask must be exactly the eligibility's other-family term. #2009 changes the term to the claimant family, so that a sharer with no rent is a claimant and not a non-dependant (reg 9(2)(d)). in_other_non_liable_family now holds the term once, and both the eligibility and local_non_dep_deductions call it, so composing with #2009 is one edit. No change in behaviour. Co-Authored-By: Claude Opus 5.5 --- .../council_tax_reduction/_legacy.py | 18 ++++++++++++++---- ...on_individual_non_dep_deduction_eligible.py | 7 ++++--- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index 840f829137..ffbf70c6fd 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -4,6 +4,17 @@ ) +def in_other_non_liable_family(person, period): + """Whether each person is in a family other than the claimant's that is + not liable for the rent: all its members aged 18 or over are the + claimant's non-dependants (SI 2012/2885 reg 9(2)(a), (d)-(e)). The + eligibility for a non-dependant deduction and the local schemes' + aggregation both use this, so they stay on one definition.""" + return ~person.benunit("benunit_contains_household_head", period) & ~person.benunit( + "benunit_is_rent_liable", period + ) + + def is_full_time_student_non_dep(person, period): return (person("current_education", period) != EducationType.NOT_IN_EDUCATION) | ( person("in_HE", period) @@ -79,10 +90,9 @@ def local_non_dep_deductions( # eligible for the national schemes, are left out here until each # council's aggregation is brought into line. person = benunit.members - other_family = ~person.benunit( - "benunit_contains_household_head", period - ) & ~person.benunit("benunit_is_rent_liable", period) - deductions = person(individual_deduction_variable, period) * other_family + deductions = person(individual_deduction_variable, period) * ( + in_other_non_liable_family(person, period) + ) deduction_for_benunit = benunit.max(deductions) if not one_deduction_for_uc_couples: has_uc = benunit("universal_credit", period) > 0 diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py index 2f72ade201..2c39ee3daa 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction_eligible.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + in_other_non_liable_family, +) class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): @@ -26,9 +29,7 @@ class council_tax_reduction_individual_non_dep_deduction_eligible(Variable): def formula(person, period, parameters): # SI 2012/2885 reg 9(2)(a), (c)-(e) and the Welsh and Scottish # equivalents; deductions are for non-dependants aged 18 or over. - other_family = ~person.benunit( - "benunit_contains_household_head", period - ) & ~person.benunit("benunit_is_rent_liable", period) + other_family = in_other_non_liable_family(person, period) # A member of any family's benefit unit who is not in its claimant's # family. Reg 9(2)(e) excludes only the person liable to pay the # applicant, so a boarder's or lodger's adult son is a non-dependant. From e72f2b8fec4694414e32e89b779f04ec86e85a02 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 05:30:37 -0400 Subject: [PATCH 7/8] Test that a CTR applicant is never charged a deduction for itself Mutation check v3 found that dropping the applicant's own-family subtraction survived every test: with formula-computed eligibility the applicant's family never has an eligible member. An individual deduction set directly as an input does, and reg 9(2)(a) still keeps the applicant's own family out of its non-dependants, as base did. The YAML case pins it. Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml index 51d2b8e129..ffa7cbe70f 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/benefit_unit_non_dependants.yaml @@ -766,3 +766,33 @@ council_tax_reduction_joint_liability_share: [0.5, 0.5] newham_council_tax_reduction_non_dep_deductions: [0, 0] newham_council_tax_reduction: [630, 630] + +# SI 2012/2885 reg 9(2)(a): the applicant's own family are not its +# non-dependants, so an individual deduction set for the applicant (here as +# an input) is never charged to the applicant's own claim. +- name: CTR, an applicant is never charged a deduction for itself + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + council_tax_reduction_individual_non_dep_deduction: 300 + friend: + age: 30 + is_claimant_or_partner: true + council_tax_reduction_individual_non_dep_deduction: 200 + benunits: + benunit_1: + members: [head] + benunit_2: + members: [friend] + households: + household: + members: [head, friend] + rent: 12_000 + output: + council_tax_reduction_claimant_benunit: [true, false] + council_tax_reduction_non_dep_deductions: [200, 0] From 270412d3089ff419aa811b164e04782ed9dd6853 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 07:51:05 -0400 Subject: [PATCH 8/8] State the CTR other-family proxy and the in-unit benefits limitation Review r3: - R3-6: in_other_non_liable_family's docstring stated the proxy as if it were reg 9(2)(a), (d)-(e). It is a proxy: a family jointly liable for the council tax but paying no rent is wrongly included, which #2009's claimant-family test fixes. - R3-3: an in-unit non-dependant's own benefit awards are not modelled, so the HB reg 74(8) and CTR Sch 1 para 8(8) exemptions for a non-dependant on an income-related benefit or on UC with no earned income never apply to them. The documentation of is_benefit_unit_non_dependant_for_legacy_benefits now says so. No change in behaviour. Co-Authored-By: Claude Opus 5.5 --- ...enefit_unit_non_dependant_for_legacy_benefits.py | 5 ++++- .../council_tax_reduction/_legacy.py | 13 ++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py b/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py index e1449d56e8..7819dc646c 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_unit_non_dependant_for_legacy_benefits.py @@ -16,7 +16,10 @@ class is_benefit_unit_non_dependant_for_legacy_benefits(Variable): "by Child Benefit) but normally resides with the claimant, so is a " "non-dependant. A child or young person is either in the family or, " "if placed with the claimant by a local authority, excluded from " - "being a non-dependant." + "being a non-dependant. Such a person's own benefit awards are not " + "modelled, so the exemptions for a non-dependant on an income-related " + "benefit or on Universal Credit with no earned income (HB reg 74(8), " + "CTR Sch 1 para 8(8)) never apply to them." ) definition_period = YEAR reference = ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index ffbf70c6fd..fd35957f53 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -5,11 +5,14 @@ def in_other_non_liable_family(person, period): - """Whether each person is in a family other than the claimant's that is - not liable for the rent: all its members aged 18 or over are the - claimant's non-dependants (SI 2012/2885 reg 9(2)(a), (d)-(e)). The - eligibility for a non-dependant deduction and the local schemes' - aggregation both use this, so they stay on one definition.""" + """Whether each person is in a family that is neither the oldest adult's + nor liable for the rent. This stands in for SI 2012/2885 reg 9(2)(a) and + (d)-(e): such a family is outside the claimant's family, and not jointly + liable or paying the claimant. It is a proxy: a family jointly liable for + the council tax but paying no rent is wrongly included, which #2009's + claimant-family test fixes. The eligibility for a non-dependant deduction + and the local schemes' aggregation both use this, so they stay on one + definition.""" return ~person.benunit("benunit_contains_household_head", period) & ~person.benunit( "benunit_is_rent_liable", period )