From a9e1b4ff42f997e069d6fc09f6cd7cf6a6f27415 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:23:50 -0400 Subject: [PATCH 01/67] Give each benefit-unit member aged 16 or over a bedroom in the LHA size criteria Universal Credit: a qualifying young person the renter is responsible for has a bedroom (UC Regs 2013 Sch 4 para 10(1)(b)), and any other member aged 16 or over is a non-dependant with one (para 9(2), 10(1)(c)), except a qualifying young person no one is responsible for (para 9(2)(g)). Housing Benefit: every member aged 16 or over other than the claimant or partner is a person who is not a child (HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b); reg 2(1)). Co-Authored-By: Claude Opus 5.5 --- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 39 +++++++++++++++---- .../housing_benefit_LHA_allowed_bedrooms.py | 24 +++++++++--- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 14d4d0b3e7..4f91b49750 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -42,25 +42,50 @@ class LHA_allowed_bedrooms(Variable): label = "The number of bedrooms covered by LHA for the benefit unit" documentation = ( "Universal Credit size criteria: one bedroom for the renter or " - "couple, one for each non-dependant aged 16 or over, and the " - "children's bedrooms. Joint tenants and other sharers of the rent, " - "boarders and lodgers are not non-dependants, so they add no bedroom " - "to anyone's entitlement; only the household head's family has " - "non-dependants (see is_non_dependant_of_household_head)." + "couple, one for each member of the benefit unit aged 16 or over who " + "is not the claimant or partner, one for each non-dependant aged 16 or " + "over, and the children's bedrooms. A qualifying young person the " + "renter is responsible for has their own bedroom; any other member " + "aged 16 or over is a non-dependant and has one too, except a " + "qualifying young person no one is responsible for, such as one " + "looked after by a local authority. Joint tenants and other sharers " + "of the rent, boarders and lodgers are not non-dependants, so they " + "add no bedroom to anyone's entitlement; only the household head's " + "family has non-dependants from other families (see " + "is_non_dependant_of_household_head)." ) 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/10", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/4", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/5", ) def formula(benunit, period, parameters): person = benunit.members + # A child is a person under 16 (WRA 2012 s.40). aged_16_or_over = person("age", period) >= 16 - # UC Regs 2013 Sch 4 para 10(1)(c): a non-dependant who is not a child. + family_member = aged_16_or_over & ~person("is_claimant_or_partner", period) + # UC Regs 2013 Sch 4 para 10(1)(b): a qualifying young person for + # whom the renter is responsible (regs 4 and 5). + qualifying_young_person = family_member & person( + "is_child_or_qualifying_young_person_for_universal_credit", period + ) + # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a + # non-dependant, unless they are a qualifying young person for whom + # no one is responsible (para 9(2)(g); reg 4(6)). + family_non_dependant = family_member & ~person( + "is_qualifying_young_person_for_universal_credit", period + ) + family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) + # Para 10(1)(c): a non-dependant from another family who is not a + # child. non_dependant = aged_16_or_over & person( "is_non_dependant_of_household_head", period ) head_family = benunit.any(person("is_household_head", period)) non_dependants = head_family * benunit.max(person.household.sum(non_dependant)) - return 1 + non_dependants + bedrooms_for_children(benunit, period) + return ( + 1 + family_rooms + non_dependants + bedrooms_for_children(benunit, period) + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py index 005d00c653..12ff521ea5 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py @@ -13,25 +13,39 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): "couple, one for each other occupier aged 16 or over, and the " "children's bedrooms. Occupiers are everyone who lives in the " "dwelling as their home except a joint tenant outside the claimant's " - "household, so a householder's boarder or lodger adds a bedroom, as " - "does a non-dependant, but a sharer of the rent does not. A sharer's, " - "boarder's or lodger's own claim counts only their own family." + "household. So every member of the benefit unit aged 16 or over who " + "is not the claimant or partner adds a bedroom, such as a young " + "person in full-time education; so do a householder's boarder or " + "lodger and a non-dependant, but a sharer of the rent does not. A " + "sharer's, boarder's or lodger's own claim counts only their own " + "family." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/13D", "https://www.legislation.gov.uk/uksi/2006/214/regulation/13D", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/2", ) def formula(benunit, period, parameters): person = benunit.members + # A child is a person under 16 (HB Regs 2006 reg 2(1)). aged_16_or_over = person("age", period) >= 16 + # HB Regs 2006 reg 13D(3)(b): a person who is not a child, here a + # member of the benefit unit other than the claimant or partner. + family_rooms = benunit.sum( + aged_16_or_over & ~person("is_claimant_or_partner", period) + ) head_family = person.benunit.any(person("is_household_head", period)) sharer = person.benunit("liable_for_share_of_household_rent", period) - # HB Regs 2006 reg 13D(3), with "occupiers" as defined in 13D(12). + # Reg 13D(3)(b) for occupiers outside the family, as defined in + # 13D(12). other_occupier = aged_16_or_over & ~head_family & ~sharer is_head_family = benunit.any(person("is_household_head", period)) other_occupiers = is_head_family * benunit.max( person.household.sum(other_occupier) ) - return 1 + other_occupiers + bedrooms_for_children(benunit, period) + return ( + 1 + family_rooms + other_occupiers + bedrooms_for_children(benunit, period) + ) From 7c6f5f30965f8bbcd2c5d1fcbe21ec8d196cca3c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:33:22 -0400 Subject: [PATCH 02/67] Test the bedroom of each benefit-unit member aged 16 or over YAML cases for a qualifying young person aged 17 and 19, an 18-year-old non-dependant, a looked-after qualifying young person (no UC bedroom), a couple with a young person and two younger children (category E), a partner aged 19 (no extra bedroom) and a pensioner couple's grandchild under HB (SPC). Two LHA_claimant_composition cases asserted the old one-bedroom count for an apprentice non-dependant and an 18-year-old qualifying young person; both are now two bedrooms (Sch 4 para 10(1)(c) and (b)). Hypothesis properties: bedrooms never fall when a person aged 16-19 joins any family; the exact change when they do; and agreement with an independent count of the size criteria, children's rooms found by brute force. Fixes #2008 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2008.fixed.md | 1 + .../family/LHA_claimant_composition.yaml | 16 +- .../family/LHA_young_person_bedroom.yaml | 251 ++++++++++++++++++ ...est_lha_bedroom_young_people_properties.py | 247 +++++++++++++++++ 4 files changed, 509 insertions(+), 6 deletions(-) create mode 100644 changelog.d/2008.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml create mode 100644 policyengine_uk/tests/test_lha_bedroom_young_people_properties.py diff --git a/changelog.d/2008.fixed.md b/changelog.d/2008.fixed.md new file mode 100644 index 0000000000..b2adf85554 --- /dev/null +++ b/changelog.d/2008.fixed.md @@ -0,0 +1 @@ +- Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit and Housing Benefit LHA size criteria: a qualifying young person (UC Regs 2013 Sch 4 para 10(1)(b)) or other non-dependant (para 10(1)(c)), and for Housing Benefit any person who is not a child (HB Regs 2006 reg 13D(3)(b)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml index 2038e23465..915e00ae84 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_claimant_composition.yaml @@ -1,7 +1,7 @@ # UC Regulations 2013, Schedule 4 paragraphs 9 and 27-28: # https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/28 -# The existing bedroom calculation is unchanged: B = 1 bedroom, C = 2; -# A is the shared accommodation rate, overriding the bedroom calculation. +# B = 1 bedroom, C = 2; A is the shared accommodation rate, overriding the +# bedroom calculation. - name: Single 17-year-old renter is not their own dependent child period: 2025 input: @@ -63,8 +63,11 @@ is_responsible_for_child_or_qualifying_young_person_for_universal_credit: false lha_renter_has_non_dependant: true is_lha_shared_accommodation_rate_specified_renter: false - # No UC QYP, but a non-dependant blocks A. Existing bedroom formula = 1: B. - LHA_category: B + # No UC QYP (UC Regs 2013 reg 5(3): training under a contract of + # employment does not count), but a non-dependant blocks A. The + # non-dependant is not a child, so has a bedroom (Sch 4 para 10(1)(c)): + # 1 + 1 = 2: C. + LHA_category: C - name: Lone parent with an 18-year-old qualifying young person period: 2025 @@ -87,8 +90,9 @@ is_responsible_for_child_or_qualifying_young_person_for_universal_credit: true lha_renter_has_non_dependant: false is_lha_shared_accommodation_rate_specified_renter: false - # Responsibility for a UC QYP blocks A. Existing bedroom formula = 1: B. - LHA_category: B + # Responsibility for a UC QYP blocks A. The QYP has a bedroom (Sch 4 + # para 10(1)(b)): 1 + 1 = 2: C. + LHA_category: C - name: Couple aged 30 and 17 is not a single specified renter period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml new file mode 100644 index 0000000000..d1a3e83f8e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -0,0 +1,251 @@ +# A member of the benefit unit aged 16 or over who is not the claimant or +# partner has their own bedroom in the size criteria. +# +# Universal Credit: UC Regs 2013 Sch 4 para 10(1)(b), "a qualifying young +# person for whom the renter or either joint renter is responsible"; anyone +# else aged 16 or over living with the renter is a non-dependant (para 9(2)) +# with a bedroom under para 10(1)(c), "a non-dependant who is not a child", +# except a qualifying young person no one is responsible for (para 9(2)(g)). +# A child is a person under 16 (WRA 2012 s.40). +# +# Housing Benefit: HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b), "a +# person who is not a child", where a child is a person under 16 (reg 2(1)). +# +# Rents of £200 a week (£10,400 a year) sit between the published Maidstone +# one-bedroom (B) and two-bedroom (C) rates (VOA, LHA April 2024, held for +# 2025-26: B £172.60, C £208.27 a week), so the bedroom decides whether the +# rent is met in full. + +- name: A qualifying young person has their own bedroom + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_household_head: true + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + benunits: + family: + members: [parent, young_person] + households: + home: + members: [parent, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + # UC: the renter (para 10(1)(a)) and the qualifying young person (para + # 10(1)(b)): 2 bedrooms, category C. + LHA_allowed_bedrooms: 2 + LHA_category: C + # The rent is below the two-bedroom rate, so it is met in full. + uc_housing_costs_element: 10_400 + # HB: one bedroom each for the claimant and the young person, neither of + # whom is a child (reg 13D(3)(b)): 2 bedrooms, category C. + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + +- name: A qualifying young person aged 19 has their own bedroom + period: 2026 + input: + people: + parent: + age: 50 + is_household_head: true + is_parent: true + young_person: + age: 19 + current_education: UPPER_SECONDARY + # UC Regs 2013 reg 5(1)(b) and 5(2): enrolled before 19, and before + # the 1 September after their 19th birthday. + age_started_or_accepted_current_education_or_training: 18 + is_before_universal_credit_qualifying_young_person_terminal_date: true + benunits: + family: + members: [parent, young_person] + households: + home: + members: [parent, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + LHA_allowed_bedrooms: 2 + LHA_category: C + uc_housing_costs_element: 10_400 + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + +- name: A member aged 18 who is not a qualifying young person is a non-dependant with a bedroom + period: 2026 + input: + people: + parent: + age: 45 + is_household_head: true + is_parent: true + son: + age: 18 + current_education: NOT_IN_EDUCATION + benunits: + family: + members: [parent, son] + households: + home: + members: [parent, son] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, false] + is_qualifying_young_person_for_universal_credit: [false, false] + # UC: not a qualifying young person, so a non-dependant (para 9(2)) who + # is not a child (para 10(1)(c)). + LHA_allowed_bedrooms: 2 + LHA_category: C + # HB: a person who is not a child (reg 13D(3)(b)). + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + +- name: A qualifying young person no one is responsible for has no Universal Credit bedroom + period: 2026 + input: + people: + carer: + age: 40 + is_household_head: true + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + # UC Regs 2013 reg 4(6)(a): no one is responsible for a qualifying + # young person looked after by a local authority. + is_looked_after_by_local_authority: true + benunits: + family: + members: [carer, young_person] + households: + home: + members: [carer, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, false] + # Not under para 10(1)(b), and not a non-dependant (para 9(2)(g)): the + # renter's bedroom only. + LHA_allowed_bedrooms: 1 + LHA_category: B + +- name: A qualifying young person cannot share with a younger sibling of the same sex + period: 2026 + input: + people: + parent_1: + age: 42 + is_household_head: true + is_parent: true + is_male: true + parent_2: + age: 40 + is_parent: true + is_male: false + young_person: + age: 17 + is_male: true + current_education: UPPER_SECONDARY + boy: + age: 12 + is_male: true + girl: + age: 8 + is_male: false + benunits: + family: + members: [parent_1, parent_2, young_person, boy, girl] + households: + home: + members: [parent_1, parent_2, young_person, boy, girl] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true, false, false, false] + # UC: the couple (para 10(1)(a)), the qualifying young person (b), and + # one bedroom each for the boy and the girl, who are of opposite sexes + # and one of whom is 10 or over ((d)-(f)): 4 bedrooms, category E. + LHA_allowed_bedrooms: 4 + LHA_category: E + # HB: the couple (reg 13D(3)(a)), the young person ((b)), and the two + # children ((e)): 4 bedrooms, the maximum category. + housing_benefit_LHA_allowed_bedrooms: 4 + housing_benefit_LHA_category: E + +- name: A partner aged 19 adds no bedroom + period: 2026 + input: + people: + claimant: + age: 24 + is_household_head: true + partner: + age: 19 + current_education: NOT_IN_EDUCATION + benunits: + couple: + members: [claimant, partner] + households: + home: + members: [claimant, partner] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true] + # The couple share one bedroom (UC para 10(1)(a); HB reg 13D(3)(a)). + LHA_allowed_bedrooms: 1 + LHA_category: B + housing_benefit_LHA_allowed_bedrooms: 1 + housing_benefit_LHA_category: B + +- name: A pensioner couple's grandchild in full-time education has their own Housing Benefit bedroom + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandparent_1: + age: 70 + is_household_head: true + grandparent_2: + age: 68 + grandchild: + age: 17 + current_education: UPPER_SECONDARY + benunits: + family: + members: [grandparent_1, grandparent_2, grandchild] + households: + home: + members: [grandparent_1, grandparent_2, grandchild] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true, false] + is_child_or_young_person_for_legacy_benefits: [false, false, true] + # HB (SPC) Regs 2006 reg 13D(3): the couple ((a)) and the young person + # ((b)): 2 bedrooms, category C. + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C + # No income, so Housing Benefit meets the eligible rent: the rent, which + # is below the two-bedroom rate. + housing_benefit: 10_400 diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py new file mode 100644 index 0000000000..b23fa6a8cd --- /dev/null +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -0,0 +1,247 @@ +"""Property-based tests for the bedrooms of benefit-unit members aged 16 or +over who are not the claimant or partner (UC Regs 2013 Sch 4 paras 9-10; HB +Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)). + +Invariants, for any generated population of households: + +1. Monotonicity: adding a person aged 16 to 19 to any family never lowers + any family's Universal Credit or Housing Benefit bedrooms, whether the + claimant and partner are supplied or inferred. +2. Exact effect: when the added person is not the claimant or partner, their + family gains one Housing Benefit bedroom, and one Universal Credit bedroom + unless they are a qualifying young person no one is responsible for (para + 9(2)(g)). The household head's family also gains one bedroom under both + schemes if the person joins a non-dependant's family, and one Housing + Benefit bedroom if they join a boarder's or lodger's family. No other + family's bedrooms change. +3. Reference: every family's bedrooms equal an independent count of the size + criteria: one for the claimant or couple; one for each other member aged + 16 or over (for Universal Credit, except a qualifying young person no one + is responsible for); for the household head's family, one for each person + aged 16 or over in a non-dependant's family (and, for Housing Benefit, in + a boarder's or lodger's family); and the fewest rooms that hold the + family's children under 16 two to a room, where only children of the same + sex or two children under 10 may share, found by brute force. Children of + other families are not counted for the household head, matching the model + (a separate, existing gap). +""" + +from itertools import combinations + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2025 # Situation inputs without a period are set for 2025. +PROPERTY_SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +ROLES = ["sharer", "boarder", "lodger", "non_dependant"] +EDUCATION = ["NOT_IN_EDUCATION", "UPPER_SECONDARY", "TERTIARY"] + +child = st.tuples(st.integers(0, 15), st.booleans()) # (age, is_male) + + +@st.composite +def family(draw, role): + return dict( + role=role, + adults=draw(st.lists(st.integers(20, 85), min_size=1, max_size=2)), + children=draw(st.lists(child, min_size=0, max_size=4)), + ) + + +@st.composite +def household(draw): + others = draw( + st.lists(st.sampled_from(ROLES).flatmap(family), min_size=0, max_size=2) + ) + return [draw(family("head"))] + others + + +@st.composite +def young_person(draw): + return dict( + age=draw(st.integers(16, 19)), + is_male=draw(st.booleans()), + current_education=draw(st.sampled_from(EDUCATION)), + age_started_or_accepted_current_education_or_training=draw(st.integers(14, 19)), + is_before_universal_credit_qualifying_young_person_terminal_date=draw( + st.booleans() + ), + is_looked_after_by_local_authority=draw(st.booleans()), + receives_benefits_in_own_right=draw(st.booleans()), + ) + + +@st.composite +def scenario(draw): + population = draw(st.lists(household(), min_size=1, max_size=4)) + h = draw(st.integers(0, len(population) - 1)) + f = draw(st.integers(0, len(population[h]) - 1)) + return population, (h, f), draw(young_person()) + + +def build(population, added=None, supply_roles=True): + """One situation for the population, optionally with a person added to + family ``added[0]`` = (household, family) with attributes ``added[1]``. + + Returns the situation and, per benefit unit, its (household, family).""" + people, benunits, homes, keys = {}, {}, {}, [] + for h, families in enumerate(population): + members = [] + for f, fam in enumerate(families): + name = f"h{h}_f{f}" + ids = [] + for i, age in enumerate(fam["adults"]): + pid = f"{name}_adult_{i}" + people[pid] = {"age": age, "is_household_head": False} + if supply_roles: + people[pid]["is_claimant_or_partner"] = True + elif fam["children"]: + people[pid]["is_parent"] = True + ids.append(pid) + for i, (age, male) in enumerate(fam["children"]): + pid = f"{name}_child_{i}" + people[pid] = {"age": age, "is_male": male, "is_household_head": False} + if supply_roles: + people[pid]["is_claimant_or_partner"] = False + ids.append(pid) + if added is not None and added[0] == (h, f): + pid = f"{name}_added" + people[pid] = {**added[1], "is_household_head": False} + if supply_roles: + people[pid]["is_claimant_or_partner"] = False + ids.append(pid) + extra = {} + if fam["role"] == "sharer": + extra["liable_for_share_of_household_rent"] = True + if fam["role"] in ("boarder", "lodger"): + people[ids[0]][f"rent_paid_as_{fam['role']}"] = 5_000 + if fam["role"] == "head": + people[ids[0]]["is_household_head"] = True + benunits[name] = {"members": ids, **extra} + members.extend(ids) + keys.append((h, f)) + homes[f"h{h}"] = { + "members": members, + "rent": 12_000, + "tenure_type": "RENT_PRIVATELY", + "brma": "MAIDSTONE", + } + situation = {"people": people, "benunits": benunits, "households": homes} + return situation, keys + + +def bedrooms(situation): + sim = Simulation(situation=situation) + uc = np.asarray(sim.calculate("LHA_allowed_bedrooms", YEAR)) + hb = np.asarray(sim.calculate("housing_benefit_LHA_allowed_bedrooms", YEAR)) + return sim, uc, hb + + +def fewest_rooms_for_children(children): + """Brute force: the fewest rooms holding the children two to a room, + where a pair must be of the same sex or both under 10.""" + + def can_share(a, b): + return a[1] == b[1] or (a[0] < 10 and b[0] < 10) + + def best(remaining): + if not remaining: + return 0 + first, rest = remaining[0], remaining[1:] + rooms = 1 + best(rest) + for j, other in enumerate(rest): + if can_share(first, other): + rooms = min(rooms, 1 + best(rest[:j] + rest[j + 1 :])) + return rooms + + return best(list(children)) + + +@PROPERTY_SETTINGS +@given(scenario(), st.booleans()) +def test_adding_a_person_aged_16_to_19_never_lowers_bedrooms(case, supply_roles): + population, target, person = case + before, keys = build(population, supply_roles=supply_roles) + after, keys_after = build(population, (target, person), supply_roles) + assert keys == keys_after + _, uc_before, hb_before = bedrooms(before) + _, uc_after, hb_after = bedrooms(after) + # 1. Monotonicity. + assert np.all(uc_after >= uc_before) + assert np.all(hb_after >= hb_before) + + +@PROPERTY_SETTINGS +@given(scenario()) +def test_exact_effect_of_adding_a_person_aged_16_to_19(case): + population, target, person = case + before, keys = build(population) + after, _ = build(population, (target, person)) + _, uc_before, hb_before = bedrooms(before) + sim, uc_after, hb_after = bedrooms(after) + # Whether the added person is a qualifying young person no one is + # responsible for. (The qualifying young person tests themselves are + # covered elsewhere.) + pid = f"h{target[0]}_f{target[1]}_added" + index = list(after["people"]).index(pid) + qualifying = sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) + responsible = sim.calculate( + "is_child_or_qualifying_young_person_for_universal_credit", YEAR + ) + unclaimed = bool(qualifying[index]) and not bool(responsible[index]) + # 2. Exact effect. + expected_uc = np.zeros(len(keys)) + expected_hb = np.zeros(len(keys)) + t = keys.index(target) + expected_uc[t] += 0 if unclaimed else 1 + expected_hb[t] += 1 + role = population[target[0]][target[1]]["role"] + head = keys.index((target[0], 0)) + if role == "non_dependant": + expected_uc[head] += 1 + expected_hb[head] += 1 + elif role in ("boarder", "lodger"): + expected_hb[head] += 1 + assert np.array_equal(uc_after - uc_before, expected_uc) + assert np.array_equal(hb_after - hb_before, expected_hb) + + +@PROPERTY_SETTINGS +@given(scenario()) +def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): + population, target, person = case + situation, keys = build(population, (target, person)) + sim, uc, hb = bedrooms(situation) + qualifying = sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) + responsible = sim.calculate( + "is_child_or_qualifying_young_person_for_universal_credit", YEAR + ) + unclaimed = dict( + zip(situation["people"], np.asarray(qualifying) & ~np.asarray(responsible)) + ) + # 3. Reference count. + for b, (h, f) in enumerate(keys): + families = population[h] + fam = families[f] + added = (h, f) == target + own_children = fewest_rooms_for_children(fam["children"]) + own_uc = int(added and not unclaimed[f"h{h}_f{f}_added"]) + own_hb = int(added) + outside_uc = outside_hb = 0 + if fam["role"] == "head": + for g, other in enumerate(families[1:], start=1): + aged_16_or_over = len(other["adults"]) + int(target == (h, g)) + if other["role"] == "non_dependant": + outside_uc += aged_16_or_over + if other["role"] in ("non_dependant", "boarder", "lodger"): + outside_hb += aged_16_or_over + assert uc[b] == 1 + own_uc + outside_uc + own_children + assert hb[b] == 1 + own_hb + outside_hb + own_children From 73abf3f4a82501f3648f1a37e1a15540b7621b4c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 14:39:49 -0400 Subject: [PATCH 03/67] 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 04/67] 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 05/67] 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 06/67] 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 07/67] 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 08/67] 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 09/67] 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 10/67] 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 ) From c4556bdf6bb6b26ad9e42741ba89f9c4da7146e7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 08:28:52 -0400 Subject: [PATCH 11/67] Exclude a qualifying young person no one is responsible for across families; one HB bedroom for an occupier couple Review findings on #2010: - UC Sch 4 para 9(2)(g) applies to the whole extended benefit unit, so a qualifying young person no one is responsible for (reg 4(6)) in a non-dependant's family adds no bedroom for the household head. - HB reg 13D(3)(a): an ordinary occupier couple outside the claimant's family shares one bedroom (DWP circular HB A3/2017); only non-couple occupiers aged 16 or over take one each under (b). The property oracles encoded both errors as law; they now count the law, and two YAML cases pin the cross-family results. Co-Authored-By: Claude Opus 5.5 --- .../family/LHA_young_person_bedroom.yaml | 71 +++++++++++++++++++ ...est_lha_bedroom_young_people_properties.py | 31 +++++--- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 32 +++++---- .../housing_benefit_LHA_allowed_bedrooms.py | 26 ++++--- 4 files changed, 127 insertions(+), 33 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml index d1a3e83f8e..11bca1d504 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -249,3 +249,74 @@ # No income, so Housing Benefit meets the eligible rent: the rent, which # is below the two-bedroom rate. housing_benefit: 10_400 + +- name: A qualifying young person no one is responsible for in a non-dependant's family adds no Universal Credit bedroom + period: 2026 + input: + people: + renter: + age: 40 + is_household_head: true + carer: + age: 45 + is_parent: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + is_looked_after_by_local_authority: true + benunits: + renter_family: + members: [renter] + carer_family: + members: [carer, young_person] + households: + home: + members: [renter, carer, young_person] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_non_dependant_of_household_head: [false, true, true] + is_qualifying_young_person_for_universal_credit: [false, false, true] + is_child_or_qualifying_young_person_for_universal_credit: [false, false, false] + # The renter (para 10(1)(a)) and the carer, a non-dependant who is not a + # child ((c)). The young person is no one's responsibility (reg 4(6)), so + # is not a non-dependant (para 9(2)(g)): 2 bedrooms, category C. + LHA_allowed_bedrooms: [2, 1] + LHA_category: [C, B] + +- name: A non-dependant couple has two Universal Credit bedrooms but one Housing Benefit bedroom + period: 2026 + input: + people: + renter: + age: 40 + is_household_head: true + non_dependant_1: + age: 30 + non_dependant_2: + age: 28 + benunits: + renter_family: + members: [renter] + couple: + members: [non_dependant_1, non_dependant_2] + households: + home: + members: [renter, non_dependant_1, non_dependant_2] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_claimant_or_partner: [true, true, true] + # UC: the renter (para 10(1)(a)) and each non-dependant who is not a + # child ((c)): 3 bedrooms, category D. The couple's own category would be + # 1 bedroom, and a couple is not a specified renter (para 28): B. + LHA_allowed_bedrooms: [3, 1] + LHA_category: [D, B] + # HB: the claimant ((b)) and the occupier couple ((a)): 2 bedrooms, + # category C. An ordinary couple shares a bedroom; only a couple who + # cannot share has two ((za); DWP circular HB A3/2017). The couple pay no + # rent; their own category would be 1 bedroom: B. + housing_benefit_LHA_allowed_bedrooms: [2, 1] + housing_benefit_LHA_category: [C, B] diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index b23fa6a8cd..eda77f7a98 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -10,16 +10,22 @@ 2. Exact effect: when the added person is not the claimant or partner, their family gains one Housing Benefit bedroom, and one Universal Credit bedroom unless they are a qualifying young person no one is responsible for (para - 9(2)(g)). The household head's family also gains one bedroom under both - schemes if the person joins a non-dependant's family, and one Housing - Benefit bedroom if they join a boarder's or lodger's family. No other - family's bedrooms change. + 9(2)(g)). If the person joins a non-dependant's family, the household + head's family also gains one Housing Benefit bedroom, and one Universal + Credit bedroom unless they are a qualifying young person no one is + responsible for (para 9(2)(g) applies to the whole extended benefit + unit). If they join a boarder's or lodger's family, the household head's + family gains one Housing Benefit bedroom. No other family's bedrooms + change. 3. Reference: every family's bedrooms equal an independent count of the size criteria: one for the claimant or couple; one for each other member aged 16 or over (for Universal Credit, except a qualifying young person no one is responsible for); for the household head's family, one for each person - aged 16 or over in a non-dependant's family (and, for Housing Benefit, in - a boarder's or lodger's family); and the fewest rooms that hold the + aged 16 or over in a non-dependant's family under Universal Credit (except + a qualifying young person no one is responsible for), and under Housing + Benefit one for each occupier aged 16 or over in a non-dependant's, + boarder's or lodger's family, a couple sharing one (reg 13D(3)(a)); and + the fewest rooms that hold the family's children under 16 two to a room, where only children of the same sex or two children under 10 may share, found by brute force. Children of other families are not counted for the household head, matching the model @@ -206,7 +212,7 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): role = population[target[0]][target[1]]["role"] head = keys.index((target[0], 0)) if role == "non_dependant": - expected_uc[head] += 1 + expected_uc[head] += 0 if unclaimed else 1 expected_hb[head] += 1 elif role in ("boarder", "lodger"): expected_hb[head] += 1 @@ -238,10 +244,15 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): outside_uc = outside_hb = 0 if fam["role"] == "head": for g, other in enumerate(families[1:], start=1): - aged_16_or_over = len(other["adults"]) + int(target == (h, g)) + joins = target == (h, g) + adults = len(other["adults"]) # supplied as claimant/partner if other["role"] == "non_dependant": - outside_uc += aged_16_or_over + # UC: each non-dependant who is not a child. + outside_uc += adults + outside_uc += int(joins and not unclaimed[f"h{h}_f{g}_added"]) if other["role"] in ("non_dependant", "boarder", "lodger"): - outside_hb += aged_16_or_over + # HB: a couple ((a)) or a single adult ((b)), plus the + # added person ((b)). + outside_hb += 1 + int(joins) assert uc[b] == 1 + own_uc + outside_uc + own_children assert hb[b] == 1 + own_hb + outside_hb + own_children diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 4f91b49750..6550d988e0 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -46,9 +46,10 @@ class LHA_allowed_bedrooms(Variable): "is not the claimant or partner, one for each non-dependant aged 16 or " "over, and the children's bedrooms. A qualifying young person the " "renter is responsible for has their own bedroom; any other member " - "aged 16 or over is a non-dependant and has one too, except a " - "qualifying young person no one is responsible for, such as one " - "looked after by a local authority. Joint tenants and other sharers " + "aged 16 or over is a non-dependant and has one too. A qualifying " + "young person no one is responsible for, such as one looked after by " + "a local authority, is not a non-dependant and has no bedroom, in " + "this family or another. Joint tenants and other sharers " "of the rent, boarders and lodgers are not non-dependants, so they " "add no bedroom to anyone's entitlement; only the household head's " "family has non-dependants from other families (see " @@ -67,22 +68,27 @@ def formula(benunit, period, parameters): # A child is a person under 16 (WRA 2012 s.40). aged_16_or_over = person("age", period) >= 16 family_member = aged_16_or_over & ~person("is_claimant_or_partner", period) - # UC Regs 2013 Sch 4 para 10(1)(b): a qualifying young person for - # whom the renter is responsible (regs 4 and 5). - qualifying_young_person = family_member & person( + qualifying = person("is_qualifying_young_person_for_universal_credit", period) + responsible = person( "is_child_or_qualifying_young_person_for_universal_credit", period ) + # UC Regs 2013 Sch 4 para 9(2)(g): a qualifying young person for whom + # no one is responsible (reg 4(6), such as one looked after by a local + # authority) is not a non-dependant, in this family or another. + no_one_responsible = qualifying & ~responsible + # Para 10(1)(b): a qualifying young person for whom the renter is + # responsible (regs 4 and 5). + qualifying_young_person = family_member & responsible # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a - # non-dependant, unless they are a qualifying young person for whom - # no one is responsible (para 9(2)(g); reg 4(6)). - family_non_dependant = family_member & ~person( - "is_qualifying_young_person_for_universal_credit", period - ) + # non-dependant. + family_non_dependant = family_member & ~qualifying family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) # Para 10(1)(c): a non-dependant from another family who is not a # child. - non_dependant = aged_16_or_over & person( - "is_non_dependant_of_household_head", period + non_dependant = ( + aged_16_or_over + & ~no_one_responsible + & person("is_non_dependant_of_household_head", period) ) head_family = benunit.any(person("is_household_head", period)) non_dependants = head_family * benunit.max(person.household.sum(non_dependant)) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py index 12ff521ea5..d82f494e09 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit_LHA_allowed_bedrooms.py @@ -10,10 +10,10 @@ class housing_benefit_LHA_allowed_bedrooms(Variable): label = "Bedrooms in the Housing Benefit size criteria" documentation = ( "Housing Benefit size criteria: one bedroom for the claimant or " - "couple, one for each other occupier aged 16 or over, and the " - "children's bedrooms. Occupiers are everyone who lives in the " - "dwelling as their home except a joint tenant outside the claimant's " - "household. So every member of the benefit unit aged 16 or over who " + "couple, one for each other occupier aged 16 or over (an occupier " + "couple sharing one), and the children's bedrooms. Occupiers are " + "everyone who lives in the dwelling as their home except a joint " + "tenant outside the claimant's household. So every member of the benefit unit aged 16 or over who " "is not the claimant or partner adds a bedroom, such as a young " "person in full-time education; so do a householder's boarder or " "lodger and a non-dependant, but a sharer of the rent does not. A " @@ -34,14 +34,20 @@ def formula(benunit, period, parameters): aged_16_or_over = person("age", period) >= 16 # HB Regs 2006 reg 13D(3)(b): a person who is not a child, here a # member of the benefit unit other than the claimant or partner. - family_rooms = benunit.sum( - aged_16_or_over & ~person("is_claimant_or_partner", period) - ) + claimant_or_partner = person("is_claimant_or_partner", period) + family_rooms = benunit.sum(aged_16_or_over & ~claimant_or_partner) head_family = person.benunit.any(person("is_household_head", period)) sharer = person.benunit("liable_for_share_of_household_rent", period) - # Reg 13D(3)(b) for occupiers outside the family, as defined in - # 13D(12). - other_occupier = aged_16_or_over & ~head_family & ~sharer + # Reg 13D(3) for occupiers outside the family, as defined in 13D(12): + # a couple has one bedroom ((a)) and anyone else aged 16 or over one + # each ((b)), so each member of a couple counts as half. The extra + # bedroom for a couple who cannot share ((za)) is not modelled. + in_couple = claimant_or_partner & ( + person.benunit.sum(claimant_or_partner & aged_16_or_over) == 2 + ) + other_occupier = (aged_16_or_over & ~head_family & ~sharer) * where( + in_couple, 0.5, 1 + ) is_head_family = benunit.any(person("is_household_head", period)) other_occupiers = is_head_family * benunit.max( person.household.sum(other_occupier) From a4c2a06082caaa206147857a6a46c536e56b4cad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 13:09:03 -0400 Subject: [PATCH 12/67] Apply HB and national CTR non-dependant deductions as legislated Housing Benefit (SI 2006/213 reg 74; SI 2006/214 reg 55): - bands include their lower edge, and only a non-dependant in remunerative work (16+ hours, reg 6(1)) is banded, on the couple's joint income; anyone else pays the reg 74(1)(b) amount; - one deduction per couple, the higher (reg 74(3)); - no deduction for full-time students, State Pension Credit recipients, or under-25s on IS, income-based JSA or UC without earned income (reg 74(7), (8), (10)), nor for any non-dependant when the claimant or partner is blind or on AA, DLA care, PIP daily living or AFIP (reg 74(6)). Council Tax Reduction (SI 2012/2885 Sch 1 para 8; WSI 2013/3029 Sch 1 para 3 and Sch 6 para 5; SSI 2012/319 reg 48; SSI 2012/303 reg 67 and SSI 2021/249 reg 90): - each national scheme's own weekly scale for 2013-14 to 2026-27, instead of the Housing Benefit amounts; - the same banding and couple rules, with Wales's working-age exception for couples on UC; - the schemes' any-age exemptions, the UC limb from the year each scheme added it, and the existing applicant exemption. Fixes #2004; the non-dependant half of #1934. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 1 + .../programs/gov/dwp/housing-benefit.ipynb | 2 +- .../non_dep_deduction/age_threshold.yaml | 12 +- .../income_related_benefit_age_limit.yaml | 12 + .../remunerative_work_hours.yaml | 12 + .../pensioners/non_dep_deduction/amount.yaml | 154 ++ ...niversal_credit_without_earned_income.yaml | 11 + .../remunerative_work_hours.yaml | 10 + .../non_dep_deduction/amount.yaml | 160 ++ ...niversal_credit_without_earned_income.yaml | 13 + .../remunerative_work_hours.yaml | 14 + .../non_dep_deduction/amount.yaml | 162 ++ ...niversal_credit_without_earned_income.yaml | 11 + .../remunerative_work_hours.yaml | 12 + policyengine_uk/programs.yaml | 2 +- ...ax_reduction_non_dependant_deductions.yaml | 185 +++ ...benefits_individual_non_dep_deduction.yaml | 190 ++- ...individual_non_dep_deduction_eligible.yaml | 15 + ...using_benefit_non_dep_deduction_chain.yaml | 90 ++ ...sing_benefit_non_dep_deduction_exempt.yaml | 119 ++ .../housing_benefit_non_dep_deductions.yaml | 45 + ...it_non_dep_deductions_claimant_exempt.yaml | 77 + ...ncil_tax_reduction_non_dep_deductions.yaml | 1312 +++++++++++++++++ .../council_tax_reduction_pension_credit.yaml | 14 +- ...test_non_dependant_deduction_properties.py | 545 +++++++ ...d_benefits_individual_non_dep_deduction.py | 24 +- ...t_individual_non_dep_deduction_eligible.py | 7 +- ...ousing_benefit_non_dep_deduction_exempt.py | 57 + .../housing_benefit_non_dep_deductions.py | 22 +- ...efit_non_dep_deductions_claimant_exempt.py | 33 + ..._reduction_individual_non_dep_deduction.py | 77 +- ..._tax_reduction_non_dep_deduction_exempt.py | 80 + ...ouncil_tax_reduction_non_dep_deductions.py | 44 +- uv.lock | 2 +- 34 files changed, 3480 insertions(+), 46 deletions(-) create mode 100644 changelog.d/2004.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml create mode 100644 policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml create mode 100644 policyengine_uk/tests/test_non_dependant_deduction_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md new file mode 100644 index 0000000000..f01a97f236 --- /dev/null +++ b/changelog.d/2004.fixed.md @@ -0,0 +1 @@ +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge, only a non-dependant in remunerative work is banded (on a couple's joint income, one deduction per couple), the claimant-side and non-dependant exemptions apply, and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 01ae7d9a50..4a963be413 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -40,7 +40,7 @@ "\n", "1. **Eligible rent**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", - "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", + "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). Under reg 74 (SI 2006/214 reg 55), a non-dependant aged 18 or over in remunerative work (16 hours a week or more) pays the deduction for the band containing their normal gross weekly income, counting a couple's joint income and including each band's lower edge; one not in remunerative work pays the lowest amount. A couple pays one deduction, the higher. No deduction is made for full-time students, State Pension Credit recipients, or under-25s on Income Support, income-based JSA or Universal Credit without earned income, nor for any non-dependant if the claimant or partner is blind or receives Attendance Allowance, the DLA care component, the PIP daily living component or Armed Forces Independence Payment. The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `benefit_cap_reduction` if the household's total benefit income exceeds the cap and no cap exemption applies.\n", "\n", "The take-up gate (`would_claim_housing_benefit`) is applied inside `housing_benefit_pre_benefit_cap`." diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml index c08cba87a4..d22b71993c 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/age_threshold.yaml @@ -1,11 +1,13 @@ -description: A non dependent deduction is provided under Housing Benefit for filers at or above this age threshold. +description: Housing Benefit makes a non-dependant deduction only for non-dependants at or above this age. metadata: label: Housing benefit non dependent deduction age threshold period: year unit: year - reference: - - title: The Housing Benefit Regulations 2006 sec. 74 (1) - href: https://www.legislation.gov.uk/uksi/2006/213/regulation/74/2024-04-01 + reference: + - title: The Housing Benefit Regulations 2006 reg. 74(1) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/74 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 55(1) + href: https://www.legislation.gov.uk/uksi/2006/214/regulation/55 values: - 2019-04-01: 18 + 2006-03-06: 18 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml new file mode 100644 index 0000000000..643e0c4133 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/income_related_benefit_age_limit.yaml @@ -0,0 +1,12 @@ +description: Housing Benefit makes no deduction for a non-dependant under this age who is on Income Support or income-based Jobseeker's Allowance, or entitled to Universal Credit calculated on the basis that they have no earned income. +values: + 2006-03-06: 25 +metadata: + label: Housing Benefit non-dependant income-related benefit exemption age limit + unit: year + period: year + reference: + - title: The Housing Benefit Regulations 2006 reg. 74(8) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/74 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 55(8) + href: https://www.legislation.gov.uk/uksi/2006/214/regulation/55 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..0fecad832e --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,12 @@ +description: Housing Benefit treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2006-03-06: 16 +metadata: + label: Housing Benefit remunerative work hours + unit: hour + period: week + reference: + - title: The Housing Benefit Regulations 2006 reg. 6(1) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/6 + - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 reg. 6(1) + href: https://www.legislation.gov.uk/uksi/2006/214/regulation/6 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml new file mode 100644 index 0000000000..f7d2106bc1 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/amount.yaml @@ -0,0 +1,154 @@ +description: England pensioner Council Tax Reduction deducts this weekly amount for each non-dependant aged 18 or over, by the non-dependant's normal gross weekly income if they are in remunerative work; a non-dependant not in remunerative work pays the lowest amount. + +# Sums from paragraph 8 of Schedule 1 to the Council Tax Reduction Schemes +# (Prescribed Requirements) (England) Regulations 2012 (SI 2012/2885), as +# substituted for each financial year by the amending instruments in the +# references. Each year's sums apply from 1 April. +brackets: +- threshold: + 2013-04-01: 0 + amount: + 2013-04-01: 3.65 + 2014-04-01: 3.7 + 2015-04-01: 3.74 + 2016-04-01: 3.77 + 2017-04-01: 3.8 + 2018-04-01: 3.9 + 2019-04-01: 4 + 2020-04-01: 4.05 + 2021-04-01: 4.05 + 2022-04-01: 4.2 + 2023-04-01: 4.6 + 2024-04-01: 4.9 + 2025-04-01: 5 + 2026-04-01: 5.2 +- threshold: + 2013-04-01: 186 + 2014-04-01: 188 + 2015-04-01: 189 + 2016-04-01: 195 + 2017-04-01: 196.95 + 2018-04-01: 202.85 + 2019-04-01: 207.7 + 2020-04-01: 217 + 2021-04-01: 217 + 2022-04-01: 224 + 2023-04-01: 236 + 2024-04-01: 256 + 2025-04-01: 266 + 2026-04-01: 279 + amount: + 2013-04-01: 7.25 + 2014-04-01: 7.45 + 2015-04-01: 7.52 + 2016-04-01: 7.58 + 2017-04-01: 7.65 + 2018-04-01: 7.9 + 2019-04-01: 8.1 + 2020-04-01: 8.25 + 2021-04-01: 8.3 + 2022-04-01: 8.55 + 2023-04-01: 9.4 + 2024-04-01: 10.05 + 2025-04-01: 10.2 + 2026-04-01: 10.6 +- threshold: + 2013-04-01: 322 + 2014-04-01: 326 + 2015-04-01: 328 + 2016-04-01: 338 + 2017-04-01: 341.4 + 2018-04-01: 351.65 + 2019-04-01: 360.1 + 2020-04-01: 377 + 2021-04-01: 377 + 2022-04-01: 389 + 2023-04-01: 410 + 2024-04-01: 445 + 2025-04-01: 463 + 2026-04-01: 485 + amount: + 2013-04-01: 9.15 + 2014-04-01: 9.4 + 2015-04-01: 9.49 + 2016-04-01: 9.56 + 2017-04-01: 9.65 + 2018-04-01: 9.95 + 2019-04-01: 10.2 + 2020-04-01: 10.35 + 2021-04-01: 10.4 + 2022-04-01: 10.7 + 2023-04-01: 11.8 + 2024-04-01: 12.6 + 2025-04-01: 12.8 + 2026-04-01: 13.3 +- threshold: + 2013-04-01: 401 + 2014-04-01: 406 + 2015-04-01: 408 + 2016-04-01: 420 + 2017-04-01: 424.2 + 2018-04-01: 436.9 + 2019-04-01: 447.4 + 2020-04-01: 469 + 2021-04-01: 469 + 2022-04-01: 484 + 2023-04-01: 511 + 2024-04-01: 554 + 2025-04-01: 577 + 2026-04-01: 605 + amount: + 2013-04-01: 10.95 + 2014-04-01: 11.25 + 2015-04-01: 11.36 + 2016-04-01: 11.45 + 2017-04-01: 11.55 + 2018-04-01: 11.9 + 2019-04-01: 12.2 + 2020-04-01: 12.4 + 2021-04-01: 12.45 + 2022-04-01: 12.85 + 2023-04-01: 14.15 + 2024-04-01: 15.1 + 2025-04-01: 15.35 + 2026-04-01: 15.95 +metadata: + label: England pensioner CTR non-dependant deduction + amount_unit: currency-GBP + threshold_unit: currency-GBP + period: week + type: single_amount + economy: false + propagate_metadata_to_children: true + uprating: gov.benefit_uprating_cpi + reference: + - title: Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012, Sch. 1 para. 8 + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8 + - title: SI 2012/3085, sums for 2013-14 + href: https://www.legislation.gov.uk/uksi/2012/3085/made + - title: SI 2013/3181, sums for 2014-15 + href: https://www.legislation.gov.uk/uksi/2013/3181/made + - title: SI 2014/3312, sums for 2015-16 + href: https://www.legislation.gov.uk/uksi/2014/3312/made + - title: SI 2015/2041, sums for 2016-17 + href: https://www.legislation.gov.uk/uksi/2015/2041/made + - title: SI 2016/1262, sums for 2017-18 + href: https://www.legislation.gov.uk/uksi/2016/1262/made + - title: SI 2017/1305, sums for 2018-19 + href: https://www.legislation.gov.uk/uksi/2017/1305/made + - title: SI 2018/1346, sums for 2019-20 + href: https://www.legislation.gov.uk/uksi/2018/1346/made + - title: SI 2020/23, sums for 2020-21 + href: https://www.legislation.gov.uk/uksi/2020/23/made + - title: SI 2021/29, sums for 2021-22 + href: https://www.legislation.gov.uk/uksi/2021/29/made + - title: SI 2022/25, sums for 2022-23 + href: https://www.legislation.gov.uk/uksi/2022/25/made + - title: SI 2023/16, sums for 2023-24 + href: https://www.legislation.gov.uk/uksi/2023/16/made + - title: SI 2024/29, sums for 2024-25 + href: https://www.legislation.gov.uk/uksi/2024/29/made + - title: SI 2025/39, sums for 2025-26 + href: https://www.legislation.gov.uk/uksi/2025/39/made + - title: SI 2026/27, sums for 2026-27 + href: https://www.legislation.gov.uk/uksi/2026/27/made diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml new file mode 100644 index 0000000000..75410b1ece --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml @@ -0,0 +1,11 @@ +description: England pensioner Council Tax Reduction makes no deduction for a non-dependant entitled to Universal Credit calculated on the basis that they have no earned income, from the year this applies (Sch. 1 para. 8(8)(c)). +values: + 2013-04-01: false + 2015-04-01: true +metadata: + label: England pensioner CTR exempts non-dependants on Universal Credit without earned income + unit: bool + period: year + reference: + - title: SI 2012/2885 Sch. 1 para. 8(8)(c), inserted for 2015-16 by SI 2014/3312 reg. 2(5)(a)(vi) + href: https://www.legislation.gov.uk/uksi/2014/3312/regulation/2/made diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..565e049a0b --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,10 @@ +description: England pensioner Council Tax Reduction treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2013-04-01: 16 +metadata: + label: England pensioner CTR remunerative work hours + unit: hour + period: week + reference: + - title: Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012, reg. 10(1) + href: https://www.legislation.gov.uk/uksi/2012/2885/regulation/10 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml new file mode 100644 index 0000000000..3b5c0874b7 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/amount.yaml @@ -0,0 +1,160 @@ +description: Scotland Council Tax Reduction deducts this weekly amount for each non-dependant aged 18 or over, by the non-dependant's normal gross weekly income if they are in remunerative work; a non-dependant not in remunerative work pays the lowest amount. + +# Sums from regulation 48 of the Council Tax Reduction (State Pension Credit) +# (Scotland) Regulations 2012 (SSI 2012/319) and, for people under pension +# age, regulation 67 of SSI 2012/303 to 2021-22 and regulation 90 of the +# Council Tax Reduction (Scotland) Regulations 2021 (SSI 2021/249) from +# 2022-23, as substituted for each financial year by the amending instruments +# in the references. Each year's sums apply from 1 April. +brackets: +- threshold: + 2013-04-01: 0 + amount: + 2013-04-01: 3.65 + 2014-04-01: 3.75 + 2015-04-01: 3.85 + 2016-04-01: 3.9 + 2017-04-01: 3.95 + 2018-04-01: 4.05 + 2019-04-01: 4.15 + 2020-04-01: 4.25 + 2021-04-01: 4.3 + 2022-04-01: 4.45 + 2023-04-01: 4.9 + 2024-04-01: 5.25 + 2025-04-01: 5.35 + 2026-04-01: 5.55 +- threshold: + 2013-04-01: 186 + 2014-04-01: 188 + 2015-04-01: 189 + 2016-04-01: 195 + 2017-04-01: 200 + 2018-04-01: 204 + 2019-04-01: 209 + 2020-04-01: 213 + 2021-04-01: 213 + 2022-04-01: 228 + 2023-04-01: 241 + 2024-04-01: 260 + 2025-04-01: 273 + 2026-04-01: 286 + amount: + 2013-04-01: 7.25 + 2014-04-01: 7.5 + 2015-04-01: 7.7 + 2016-04-01: 7.75 + 2017-04-01: 7.85 + 2018-04-01: 8.1 + 2019-04-01: 8.3 + 2020-04-01: 8.45 + 2021-04-01: 8.5 + 2022-04-01: 8.8 + 2023-04-01: 9.7 + 2024-04-01: 10.35 + 2025-04-01: 10.55 + 2026-04-01: 10.95 +- threshold: + 2013-04-01: 322 + 2014-04-01: 326 + 2015-04-01: 328 + 2016-04-01: 338 + 2017-04-01: 346 + 2018-04-01: 354 + 2019-04-01: 363 + 2020-04-01: 370 + 2021-04-01: 370 + 2022-04-01: 396 + 2023-04-01: 418 + 2024-04-01: 451 + 2025-04-01: 474 + 2026-04-01: 497 + amount: + 2013-04-01: 9.15 + 2014-04-01: 9.5 + 2015-04-01: 9.8 + 2016-04-01: 9.85 + 2017-04-01: 9.95 + 2018-04-01: 10.25 + 2019-04-01: 10.5 + 2020-04-01: 10.7 + 2021-04-01: 10.8 + 2022-04-01: 11.15 + 2023-04-01: 12.3 + 2024-04-01: 13.15 + 2025-04-01: 13.4 + 2026-04-01: 13.9 +- threshold: + 2013-04-01: 401 + 2014-04-01: 406 + 2015-04-01: 408 + 2016-04-01: 420 + 2017-04-01: 430 + 2018-04-01: 439 + 2019-04-01: 451 + 2020-04-01: 458 + 2021-04-01: 458 + 2022-04-01: 490 + 2023-04-01: 517 + 2024-04-01: 558 + 2025-04-01: 586 + 2026-04-01: 614 + amount: + 2013-04-01: 10.95 + 2014-04-01: 11.35 + 2015-04-01: 11.7 + 2016-04-01: 11.8 + 2017-04-01: 11.9 + 2018-04-01: 12.25 + 2019-04-01: 12.55 + 2020-04-01: 12.8 + 2021-04-01: 12.9 + 2022-04-01: 13.3 + 2023-04-01: 14.65 + 2024-04-01: 15.65 + 2025-04-01: 15.95 + 2026-04-01: 16.55 +metadata: + label: Scotland CTR non-dependant deduction + amount_unit: currency-GBP + threshold_unit: currency-GBP + period: week + type: single_amount + economy: false + propagate_metadata_to_children: true + uprating: gov.benefit_uprating_cpi + reference: + - title: Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012, reg. 48 + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/48 + - title: Council Tax Reduction (Scotland) Regulations 2012, reg. 67 (to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/67 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 90 + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/90 + - title: SSI 2013/49, sums for 2013-14 + href: https://www.legislation.gov.uk/ssi/2013/49/made + - title: SSI 2014/35, sums for 2014-15 + href: https://www.legislation.gov.uk/ssi/2014/35/made + - title: SSI 2015/46, sums for 2015-16 + href: https://www.legislation.gov.uk/ssi/2015/46/made + - title: SSI 2016/81, sums for 2016-17 + href: https://www.legislation.gov.uk/ssi/2016/81/made + - title: SSI 2017/41, sums for 2017-18 + href: https://www.legislation.gov.uk/ssi/2017/41/made + - title: SSI 2018/69, sums for 2018-19 + href: https://www.legislation.gov.uk/ssi/2018/69/made + - title: SSI 2019/29, sums for 2019-20 + href: https://www.legislation.gov.uk/ssi/2019/29/made + - title: SSI 2020/25, sums for 2020-21 + href: https://www.legislation.gov.uk/ssi/2020/25/made + - title: SSI 2021/51, sums for 2021-22 + href: https://www.legislation.gov.uk/ssi/2021/51/made + - title: SSI 2022/52, sums for 2022-23 + href: https://www.legislation.gov.uk/ssi/2022/52/made + - title: SSI 2023/38, sums for 2023-24 + href: https://www.legislation.gov.uk/ssi/2023/38/made + - title: SSI 2024/35, sums for 2024-25 + href: https://www.legislation.gov.uk/ssi/2024/35/made + - title: SSI 2025/24, sums for 2025-26 + href: https://www.legislation.gov.uk/ssi/2025/24/made + - title: SSI 2026/56, sums for 2026-27 + href: https://www.legislation.gov.uk/ssi/2026/56/made diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml new file mode 100644 index 0000000000..7ee2d4484a --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml @@ -0,0 +1,13 @@ +description: Scotland Council Tax Reduction makes no deduction for a non-dependant entitled to Universal Credit calculated on the basis that they have no earned income, from the year this applies (SSI 2012/319 reg. 48(8)(c), SSI 2012/303 reg. 67(8)(c) and SSI 2021/249 reg. 90(8)(c)). +values: + 2013-04-01: false + 2016-04-01: true +metadata: + label: Scotland CTR exempts non-dependants on Universal Credit without earned income + unit: bool + period: year + reference: + - title: SSI 2012/319 reg. 48(8)(c) and SSI 2012/303 reg. 67(8)(c), inserted from 1 April 2016 by SSI 2016/81 + href: https://www.legislation.gov.uk/ssi/2016/81/made + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 90(8)(c) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/90 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..9bf0f1cdf8 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,14 @@ +description: Scotland Council Tax Reduction treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2013-04-01: 16 +metadata: + label: Scotland CTR remunerative work hours + unit: hour + period: week + reference: + - title: Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012, reg. 6(1) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/6 + - title: Council Tax Reduction (Scotland) Regulations 2012, reg. 6(1) (to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/6 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 10(1) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/10 diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml new file mode 100644 index 0000000000..72afa4a599 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/amount.yaml @@ -0,0 +1,162 @@ +description: Wales Council Tax Reduction deducts this weekly amount for each non-dependant aged 18 or over, by the non-dependant's normal gross weekly income if they are in remunerative work; a non-dependant not in remunerative work pays the lowest amount. + +# Sums from paragraph 3 of Schedule 1 (pensioners) and paragraph 5 of Schedule +# 6 (persons who are not pensioners) to the Council Tax Reduction Schemes and +# Prescribed Requirements (Wales) Regulations 2013 (WSI 2013/3029); for +# 2013-14, the same paragraphs of WSI 2012/3144, as substituted for each +# financial year by the amending instruments in the references. Each year's +# sums apply from 1 April. +# 2016-17: the English text sets the first band at £194; the Welsh text has +# £195 (WSI 2017/46 reg. 3(a)(iii)-(v) replaced both). +brackets: +- threshold: + 2013-04-01: 0 + amount: + 2013-04-01: 3.65 + 2014-04-01: 3.75 + 2015-04-01: 3.9 + 2016-04-01: 4.05 + 2017-04-01: 4.2 + 2018-04-01: 4.35 + 2019-04-01: 4.55 + 2020-04-01: 4.85 + 2021-04-01: 5.1 + 2022-04-01: 5.3 + 2023-04-01: 5.45 + 2024-04-01: 5.8 + 2025-04-01: 6.25 + 2026-04-01: 6.7 +- threshold: + 2013-04-01: 186 + 2014-04-01: 188 + 2015-04-01: 189 + 2016-04-01: 194 + 2017-04-01: 200 + 2018-04-01: 205 + 2019-04-01: 210 + 2020-04-01: 217 + 2021-04-01: 217 + 2022-04-01: 224 + 2023-04-01: 236 + 2024-04-01: 256 + 2025-04-01: 266 + 2026-04-01: 279 + amount: + 2013-04-01: 7.25 + 2014-04-01: 7.5 + 2015-04-01: 7.8 + 2016-04-01: 8.1 + 2017-04-01: 8.4 + 2018-04-01: 8.7 + 2019-04-01: 9.15 + 2020-04-01: 9.75 + 2021-04-01: 10.2 + 2022-04-01: 10.6 + 2023-04-01: 10.9 + 2024-04-01: 11.55 + 2025-04-01: 12.45 + 2026-04-01: 13.35 +- threshold: + 2013-04-01: 322 + 2014-04-01: 326 + 2015-04-01: 328 + 2016-04-01: 338 + 2017-04-01: 346 + 2018-04-01: 355 + 2019-04-01: 365 + 2020-04-01: 377 + 2021-04-01: 377 + 2022-04-01: 389 + 2023-04-01: 410 + 2024-04-01: 445 + 2025-04-01: 463 + 2026-04-01: 485 + amount: + 2013-04-01: 9.15 + 2014-04-01: 9.45 + 2015-04-01: 9.85 + 2016-04-01: 10.25 + 2017-04-01: 10.6 + 2018-04-01: 10.95 + 2019-04-01: 11.5 + 2020-04-01: 12.25 + 2021-04-01: 12.85 + 2022-04-01: 13.35 + 2023-04-01: 13.7 + 2024-04-01: 14.5 + 2025-04-01: 15.65 + 2026-04-01: 16.8 +- threshold: + 2013-04-01: 401 + 2014-04-01: 406 + 2015-04-01: 408 + 2016-04-01: 420 + 2017-04-01: 430 + 2018-04-01: 440 + 2019-04-01: 450 + 2020-04-01: 469 + 2021-04-01: 469 + 2022-04-01: 484 + 2023-04-01: 511 + 2024-04-01: 554 + 2025-04-01: 577 + 2026-04-01: 605 + amount: + 2013-04-01: 10.95 + 2014-04-01: 11.3 + 2015-04-01: 11.75 + 2016-04-01: 12.25 + 2017-04-01: 12.7 + 2018-04-01: 13.1 + 2019-04-01: 13.75 + 2020-04-01: 14.65 + 2021-04-01: 15.35 + 2022-04-01: 15.95 + 2023-04-01: 16.4 + 2024-04-01: 17.35 + 2025-04-01: 18.7 + 2026-04-01: 20.05 +metadata: + label: Wales CTR non-dependant deduction + amount_unit: currency-GBP + threshold_unit: currency-GBP + period: week + type: single_amount + economy: false + propagate_metadata_to_children: true + uprating: gov.benefit_uprating_cpi + reference: + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013, Sch. 1 para. 3 (pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3 + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013, Sch. 6 para. 5 (persons who are not pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5 + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (2013-14), Sch. 1 para. 3 and Sch. 6 para. 5 + href: https://www.legislation.gov.uk/wsi/2012/3144/schedule/1/paragraph/3 + - title: WSI 2013/112, sums for 2013-14 + href: https://www.legislation.gov.uk/wsi/2013/112/made + - title: WSI 2014/66, sums for 2014-15 + href: https://www.legislation.gov.uk/wsi/2014/66/made + - title: WSI 2015/44, sums for 2015-16 + href: https://www.legislation.gov.uk/wsi/2015/44/made + - title: WSI 2016/50, sums for 2016-17 + href: https://www.legislation.gov.uk/wsi/2016/50/made + - title: WSI 2017/46, sums for 2017-18 + href: https://www.legislation.gov.uk/wsi/2017/46/made + - title: WSI 2018/14, sums for 2018-19 + href: https://www.legislation.gov.uk/wsi/2018/14/made + - title: WSI 2019/11, sums for 2019-20 + href: https://www.legislation.gov.uk/wsi/2019/11/made + - title: WSI 2020/16, sums for 2020-21 + href: https://www.legislation.gov.uk/wsi/2020/16/made + - title: WSI 2021/34, sums for 2021-22 + href: https://www.legislation.gov.uk/wsi/2021/34/made + - title: WSI 2022/51, sums for 2022-23 + href: https://www.legislation.gov.uk/wsi/2022/51/made + - title: WSI 2023/47, sums for 2023-24 + href: https://www.legislation.gov.uk/wsi/2023/47/made + - title: WSI 2024/56, sums for 2024-25 + href: https://www.legislation.gov.uk/wsi/2024/56/made + - title: WSI 2025/59, sums for 2025-26 + href: https://www.legislation.gov.uk/wsi/2025/59/made + - title: WSI 2026/13, sums for 2026-27 + href: https://www.legislation.gov.uk/wsi/2026/13/made diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml new file mode 100644 index 0000000000..d7ec14e179 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/exempt_universal_credit_without_earned_income.yaml @@ -0,0 +1,11 @@ +description: Wales Council Tax Reduction makes no deduction for a non-dependant entitled to Universal Credit calculated on the basis that they have no earned income, from the year this applies (Sch. 1 para. 3(8)(c) and Sch. 6 para. 5(8)(c)). +values: + 2013-04-01: false + 2015-04-01: true +metadata: + label: Wales CTR exempts non-dependants on Universal Credit without earned income + unit: bool + period: year + reference: + - title: WSI 2013/3029 Sch. 1 para. 3(8)(c) and Sch. 6 para. 5(8)(c), added for 2015-16 by WSI 2015/44 + href: https://www.legislation.gov.uk/wsi/2015/44/made diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml new file mode 100644 index 0000000000..46775c49e1 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/non_dep_deduction/remunerative_work_hours.yaml @@ -0,0 +1,12 @@ +description: Wales Council Tax Reduction treats a person working at least this many hours a week as in remunerative work, which puts a non-dependant on the income-banded deduction. +values: + 2013-04-01: 16 +metadata: + label: Wales CTR remunerative work hours + unit: hour + period: week + reference: + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013, reg. 10(1) + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/10 + - title: Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (2013-14), reg. 10(1) + href: https://www.legislation.gov.uk/wsi/2012/3144/regulation/10 diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index 46d253f103..61a51cecef 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -521,7 +521,7 @@ programs: variable: council_tax_reduction parameter_prefix: gov.local_authorities verified_start_year: 2013 - notes: First-pass national support for Scotland, Wales, and England pensioner schemes (including the Pension Credit guarantee credit disregard and savings-credit-only rules), with reported CTB/CTR retained for unsupported English working-age local schemes. + notes: First-pass national support for Scotland, Wales, and England pensioner schemes (including the Pension Credit guarantee credit disregard and savings-credit-only rules, and each scheme's own non-dependant deductions and exemptions), with reported CTB/CTR retained for unsupported English working-age local schemes. # --- Treasury programs --- - id: cost_of_living_support diff --git a/policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml b/policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml new file mode 100644 index 0000000000..2aa4334b76 --- /dev/null +++ b/policyengine_uk/tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml @@ -0,0 +1,185 @@ +# Council Tax Reduction non-dependant deductions as enacted, weekly, for the +# financial year starting 1 April of each year, from each year's amending +# instrument (source) and the point-in-time text of the scheme provision on +# legislation.gov.uk. Bands include their lower edge; the first amount is +# para (1)(b), the last para (1)(a). Wales: WSI 2012/3144 (2013-14), then WSI +# 2013/3029 Sch 1 para 3 and Sch 6 para 5 (identical scales). Scotland: SSI +# 2012/319 reg 48, SSI 2012/303 reg 67 and SSI 2021/249 reg 90 (identical). + +england_pensioners: + parameter: gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.amount + years: + 2013: + thresholds: [0, 186.00, 322.00, 401.00] + amounts: [3.65, 7.25, 9.15, 10.95] + source: https://www.legislation.gov.uk/uksi/2012/3085/made + 2014: + thresholds: [0, 188.00, 326.00, 406.00] + amounts: [3.70, 7.45, 9.40, 11.25] + source: https://www.legislation.gov.uk/uksi/2013/3181/made + 2015: + thresholds: [0, 189.00, 328.00, 408.00] + amounts: [3.74, 7.52, 9.49, 11.36] + source: https://www.legislation.gov.uk/uksi/2014/3312/made + 2016: + thresholds: [0, 195.00, 338.00, 420.00] + amounts: [3.77, 7.58, 9.56, 11.45] + source: https://www.legislation.gov.uk/uksi/2015/2041/made + 2017: + thresholds: [0, 196.95, 341.40, 424.20] + amounts: [3.80, 7.65, 9.65, 11.55] + source: https://www.legislation.gov.uk/uksi/2016/1262/made + 2018: + thresholds: [0, 202.85, 351.65, 436.90] + amounts: [3.90, 7.90, 9.95, 11.90] + source: https://www.legislation.gov.uk/uksi/2017/1305/made + 2019: + thresholds: [0, 207.70, 360.10, 447.40] + amounts: [4.00, 8.10, 10.20, 12.20] + source: https://www.legislation.gov.uk/uksi/2018/1346/made + 2020: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [4.05, 8.25, 10.35, 12.40] + source: https://www.legislation.gov.uk/uksi/2020/23/made + 2021: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [4.05, 8.30, 10.40, 12.45] + source: https://www.legislation.gov.uk/uksi/2021/29/made + 2022: + thresholds: [0, 224.00, 389.00, 484.00] + amounts: [4.20, 8.55, 10.70, 12.85] + source: https://www.legislation.gov.uk/uksi/2022/25/made + 2023: + thresholds: [0, 236.00, 410.00, 511.00] + amounts: [4.60, 9.40, 11.80, 14.15] + source: https://www.legislation.gov.uk/uksi/2023/16/made + 2024: + thresholds: [0, 256.00, 445.00, 554.00] + amounts: [4.90, 10.05, 12.60, 15.10] + source: https://www.legislation.gov.uk/uksi/2024/29/made + 2025: + thresholds: [0, 266.00, 463.00, 577.00] + amounts: [5.00, 10.20, 12.80, 15.35] + source: https://www.legislation.gov.uk/uksi/2025/39/made + 2026: + thresholds: [0, 279.00, 485.00, 605.00] + amounts: [5.20, 10.60, 13.30, 15.95] + source: https://www.legislation.gov.uk/uksi/2026/27/made +wales: + parameter: gov.local_authorities.wales.council_tax_reduction.non_dep_deduction.amount + years: + 2013: + thresholds: [0, 186.00, 322.00, 401.00] + amounts: [3.65, 7.25, 9.15, 10.95] + source: https://www.legislation.gov.uk/wsi/2013/112/made + 2014: + thresholds: [0, 188.00, 326.00, 406.00] + amounts: [3.75, 7.50, 9.45, 11.30] + source: https://www.legislation.gov.uk/wsi/2014/66/made + 2015: + thresholds: [0, 189.00, 328.00, 408.00] + amounts: [3.90, 7.80, 9.85, 11.75] + source: https://www.legislation.gov.uk/wsi/2015/44/made + 2016: + thresholds: [0, 194.00, 338.00, 420.00] + amounts: [4.05, 8.10, 10.25, 12.25] + source: https://www.legislation.gov.uk/wsi/2016/50/made + 2017: + thresholds: [0, 200.00, 346.00, 430.00] + amounts: [4.20, 8.40, 10.60, 12.70] + source: https://www.legislation.gov.uk/wsi/2017/46/made + 2018: + thresholds: [0, 205.00, 355.00, 440.00] + amounts: [4.35, 8.70, 10.95, 13.10] + source: https://www.legislation.gov.uk/wsi/2018/14/made + 2019: + thresholds: [0, 210.00, 365.00, 450.00] + amounts: [4.55, 9.15, 11.50, 13.75] + source: https://www.legislation.gov.uk/wsi/2019/11/made + 2020: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [4.85, 9.75, 12.25, 14.65] + source: https://www.legislation.gov.uk/wsi/2020/16/made + 2021: + thresholds: [0, 217.00, 377.00, 469.00] + amounts: [5.10, 10.20, 12.85, 15.35] + source: https://www.legislation.gov.uk/wsi/2021/34/made + 2022: + thresholds: [0, 224.00, 389.00, 484.00] + amounts: [5.30, 10.60, 13.35, 15.95] + source: https://www.legislation.gov.uk/wsi/2022/51/made + 2023: + thresholds: [0, 236.00, 410.00, 511.00] + amounts: [5.45, 10.90, 13.70, 16.40] + source: https://www.legislation.gov.uk/wsi/2023/47/made + 2024: + thresholds: [0, 256.00, 445.00, 554.00] + amounts: [5.80, 11.55, 14.50, 17.35] + source: https://www.legislation.gov.uk/wsi/2024/56/made + 2025: + thresholds: [0, 266.00, 463.00, 577.00] + amounts: [6.25, 12.45, 15.65, 18.70] + source: https://www.legislation.gov.uk/wsi/2025/59/made + 2026: + thresholds: [0, 279.00, 485.00, 605.00] + amounts: [6.70, 13.35, 16.80, 20.05] + source: https://www.legislation.gov.uk/wsi/2026/13/made +scotland: + parameter: gov.local_authorities.scotland.council_tax_reduction.non_dep_deduction.amount + years: + 2013: + thresholds: [0, 186.00, 322.00, 401.00] + amounts: [3.65, 7.25, 9.15, 10.95] + source: https://www.legislation.gov.uk/ssi/2013/49/made + 2014: + thresholds: [0, 188.00, 326.00, 406.00] + amounts: [3.75, 7.50, 9.50, 11.35] + source: https://www.legislation.gov.uk/ssi/2014/35/made + 2015: + thresholds: [0, 189.00, 328.00, 408.00] + amounts: [3.85, 7.70, 9.80, 11.70] + source: https://www.legislation.gov.uk/ssi/2015/46/made + 2016: + thresholds: [0, 195.00, 338.00, 420.00] + amounts: [3.90, 7.75, 9.85, 11.80] + source: https://www.legislation.gov.uk/ssi/2016/81/made + 2017: + thresholds: [0, 200.00, 346.00, 430.00] + amounts: [3.95, 7.85, 9.95, 11.90] + source: https://www.legislation.gov.uk/ssi/2017/41/made + 2018: + thresholds: [0, 204.00, 354.00, 439.00] + amounts: [4.05, 8.10, 10.25, 12.25] + source: https://www.legislation.gov.uk/ssi/2018/69/made + 2019: + thresholds: [0, 209.00, 363.00, 451.00] + amounts: [4.15, 8.30, 10.50, 12.55] + source: https://www.legislation.gov.uk/ssi/2019/29/made + 2020: + thresholds: [0, 213.00, 370.00, 458.00] + amounts: [4.25, 8.45, 10.70, 12.80] + source: https://www.legislation.gov.uk/ssi/2020/25/made + 2021: + thresholds: [0, 213.00, 370.00, 458.00] + amounts: [4.30, 8.50, 10.80, 12.90] + source: https://www.legislation.gov.uk/ssi/2021/51/made + 2022: + thresholds: [0, 228.00, 396.00, 490.00] + amounts: [4.45, 8.80, 11.15, 13.30] + source: https://www.legislation.gov.uk/ssi/2022/52/made + 2023: + thresholds: [0, 241.00, 418.00, 517.00] + amounts: [4.90, 9.70, 12.30, 14.65] + source: https://www.legislation.gov.uk/ssi/2023/38/made + 2024: + thresholds: [0, 260.00, 451.00, 558.00] + amounts: [5.25, 10.35, 13.15, 15.65] + source: https://www.legislation.gov.uk/ssi/2024/35/made + 2025: + thresholds: [0, 273.00, 474.00, 586.00] + amounts: [5.35, 10.55, 13.40, 15.95] + source: https://www.legislation.gov.uk/ssi/2025/24/made + 2026: + thresholds: [0, 286.00, 497.00, 614.00] + amounts: [5.55, 10.95, 13.90, 16.55] + source: https://www.legislation.gov.uk/ssi/2026/56/made diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 49c22bda96..41919b3d7c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -1,31 +1,189 @@ -- name: Reduced rent without LHA - period: 2025 +# 2026-27 deductions (HB Regs 2006 reg 74(1)-(2); SI 2006/214 reg 55(1)-(2); +# up-rated by SI 2026/148 art 23(3)). In remunerative work (16 hours or more, +# reg 6(1)): under £192 a week 20.40; not less than £192 46.85; £279 64.35; +# £365 105.20; £485 119.85; £605 and above 131.45. Not in remunerative work: +# 20.40 (reg 74(1)(b)). Bands include their lower edge ("not less than"). + +- name: In remunerative work with no income pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 0 + weekly_hours: 35 + output: + # reg 74(2)(a) and (1)(b) + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: Just below the second band + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 9983.48 + weekly_hours: 35 + output: + # 191.99 a week + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: Exactly 192 a week is in the band starting at 192 + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 9984 + weekly_hours: 35 + output: + household_benefits_individual_non_dep_deduction: 46.85 * 52 + +- name: Exactly 279 a week is in the band starting at 279 + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 14508 + weekly_hours: 35 + output: + household_benefits_individual_non_dep_deduction: 64.35 * 52 + +- name: Exactly 365 a week is in the band starting at 365 + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 5_200 # 100 weekly + age: 30 + total_income: 18980 + weekly_hours: 35 output: - household_benefits_individual_non_dep_deduction: 19.3 * 52 + household_benefits_individual_non_dep_deduction: 105.2 * 52 -- name: Higher amount - period: 2025 +- name: Exactly 485 a week is in the band starting at 485 + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 28_912 # 556 weekly + age: 30 + total_income: 25220 + weekly_hours: 35 output: - household_benefits_individual_non_dep_deduction: 124.55 * 52 + household_benefits_individual_non_dep_deduction: 119.85 * 52 -- name: Middle amount - period: 2025 +- name: Exactly 605 a week is in the band starting at 605 + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 17_420 # 335 weekly + age: 30 + total_income: 31460 + weekly_hours: 35 output: - household_benefits_individual_non_dep_deduction: 99.65 * 52 + household_benefits_individual_non_dep_deduction: 131.45 * 52 -- name: Exclusive brackets - period: 2025 +- name: Just below the top band + period: 2026 + absolute_error_margin: 0.01 input: housing_benefit_individual_non_dep_deduction_eligible: true - total_income: 9_153 # 176 weekly + age: 30 + total_income: 31459.48 + weekly_hours: 35 + output: + # 604.99 a week + household_benefits_individual_non_dep_deduction: 119.85 * 52 + +- name: Not in remunerative work pays the lowest amount whatever the income + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 40000 + weekly_hours: 0 + output: + # reg 74(1)(b): the bands apply only in remunerative work + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: Just under 16 hours is not remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 40000 + weekly_hours: 15.9 + output: + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: 16 hours is remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 40000 + weekly_hours: 16 + output: + household_benefits_individual_non_dep_deduction: 131.45 * 52 + +- name: A loss counts as no income + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: -5000 + weekly_hours: 35 + output: + household_benefits_individual_non_dep_deduction: 20.4 * 52 + +- name: A couple's joint income sets the band for the member in remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + people: + worker: + age: 30 + total_income: 10_400 # 200 a week + weekly_hours: 35 + housing_benefit_individual_non_dep_deduction_eligible: true + partner: + age: 30 + total_income: 10_400 + weekly_hours: 0 + housing_benefit_individual_non_dep_deduction_eligible: true + benunits: + couple: + members: [worker, partner] + households: + household: + members: [worker, partner] + output: + # reg 74(4): joint income 400 a week is in the 365 band; the partner, not + # in remunerative work, would pay 20.40 (reg 74(1)(b)). + household_benefits_individual_non_dep_deduction: [5_470.40, 1_060.80] + +- name: A dependent child's income does not count towards the couple's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + total_income: 9_360 # 180 a week + weekly_hours: 35 + housing_benefit_individual_non_dep_deduction_eligible: true + child: + age: 17 + total_income: 5_200 + benunits: + family: + members: [parent, child] + households: + household: + members: [parent, child] output: - household_benefits_individual_non_dep_deduction: 44.4 * 52 + household_benefits_individual_non_dep_deduction: [1_060.80, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml index 0307c37986..e27118fd0b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.yaml @@ -3,6 +3,12 @@ input: benunit_is_rent_liable: false age: 18 + # Without this the age-based default puts an 18-year-old in tertiary + # education, and full-time students are exempt (reg 74(7)(c)). + current_education: NOT_IN_EDUCATION + # An 18-year-old with no income is otherwise entitled to UC without + # earned income, which exempts them (reg 74(8)(b)). + would_claim_uc: false output: housing_benefit_individual_non_dep_deduction_eligible: true @@ -21,3 +27,12 @@ age: 18 output: housing_benefit_individual_non_dep_deduction_eligible: false + +- name: Exempt non-dependant is ineligible + period: 2025 + input: + benunit_is_rent_liable: false + age: 20 + current_education: TERTIARY + output: + housing_benefit_individual_non_dep_deduction_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml new file mode 100644 index 0000000000..99aa29abf3 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml @@ -0,0 +1,90 @@ +# Housing Benefit with a non-dependant, end to end through eligibility, +# exemption, banding and aggregation. Means-test inputs are fixed so the +# award is the eligible rent less the non-dependant deductions. + +- name: A zero-income full-time student does not reduce the claimant's HB + # The counterexample from the PolicyEngine/policyengine-uk#1925 review: + # HB Regs 2006 reg 74(7)(c)-(d). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + tenant: + age: 40 + total_income: 0 + student: + age: 20 + total_income: 0 + employment_status: STUDENT + current_education: TERTIARY + benunits: + tenant_unit: + members: [tenant] + benunit_is_rent_liable: true + housing_benefit_eligible: true + would_claim_housing_benefit: true + benunit_rent: 6_000 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + student_unit: + members: [student] + benunit_is_rent_liable: false + housing_benefit_eligible: false + would_claim_housing_benefit: false + benunit_rent: 0 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + households: + home: + members: [tenant, student] + output: + housing_benefit_non_dep_deduction_exempt: [false, true] + household_benefits_individual_non_dep_deduction: [0, 0] + housing_benefit_non_dep_deductions: [0, 0] + housing_benefit: [6_000, 0] + +- name: A zero-income non-student non-dependant pays the lowest deduction + # 2026 rather than 2025 so the amount is the same before and after + # PolicyEngine/policyengine-uk#1925 (which corrects the 2025-26 rates). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + tenant: + age: 40 + total_income: 0 + son: + age: 30 + total_income: 0 + current_education: NOT_IN_EDUCATION + benunits: + tenant_unit: + members: [tenant] + benunit_is_rent_liable: true + housing_benefit_eligible: true + would_claim_housing_benefit: true + benunit_rent: 6_000 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + son_unit: + members: [son] + benunit_is_rent_liable: false + housing_benefit_eligible: false + would_claim_housing_benefit: false + benunit_rent: 0 + housing_benefit_applicable_amount: 0 + housing_benefit_applicable_income: 0 + LHA_eligible: false + benefit_cap_reduction: 0 + households: + home: + members: [tenant, son] + output: + # reg 74(1)(b): 20.40 a week in 2026-27. + housing_benefit_non_dep_deductions: [1_060.80, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml new file mode 100644 index 0000000000..5699336677 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml @@ -0,0 +1,119 @@ +# HB Regs 2006 (SI 2006/213) reg 74(7)-(8) and (10); SI 2006/214 reg 55(7)-(9). + +- name: Full-time tertiary student is exempt + period: 2026 + input: + age: 20 + would_claim_uc: false + current_education: TERTIARY + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Full-time student in non-advanced education is exempt + period: 2026 + input: + age: 18 + would_claim_uc: false + current_education: UPPER_SECONDARY + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Higher education flag alone makes a student exempt + period: 2026 + input: + age: 30 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + in_HE: true + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: A worker not in education is not exempt + period: 2026 + input: + age: 30 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + employment_income: 20_000 + output: + housing_benefit_non_dep_deduction_exempt: false + +- name: State Pension Credit recipient is exempt at any age + period: 2026 + input: + age: 70 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + pension_credit: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Income Support under 25 is exempt + period: 2026 + input: + age: 24 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + income_support: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Income Support at 25 is not exempt + period: 2026 + input: + age: 25 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + income_support: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: false + +- name: Income-based JSA under 25 is exempt + period: 2026 + input: + age: 24 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + jsa_income: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Income-related ESA under 25 is not exempt without an assessment-phase input + period: 2026 + input: + age: 24 + would_claim_uc: false + current_education: NOT_IN_EDUCATION + esa_income: 1_000 + output: + # reg 74(8)(a) exempts only ESA(IR) without the support component and + # outside the work-related activity group; the model has no ESA group input. + housing_benefit_non_dep_deduction_exempt: false + +- name: Universal Credit without earned income under 25 is exempt + period: 2026 + input: + age: 24 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: true + +- name: Universal Credit with earned income under 25 is not exempt + period: 2026 + input: + age: 24 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 1_000 + employment_income: 2_000 + output: + housing_benefit_non_dep_deduction_exempt: false + +- name: Universal Credit without earned income at 25 is not exempt + period: 2026 + input: + age: 25 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 1_000 + output: + housing_benefit_non_dep_deduction_exempt: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 46c2b6ac44..071c6610c8 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -59,3 +59,48 @@ members: [person_1, person_2, person_3] output: housing_benefit_non_dep_deductions: [5, 4, 3] + +- name: One deduction for a non-dependant couple, the higher amount + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + household_benefits_individual_non_dep_deduction: 0 + non_dep_1: + household_benefits_individual_non_dep_deduction: 1_060.80 + non_dep_2: + household_benefits_individual_non_dep_deduction: 5_470.40 + benunits: + claimant_unit: + members: [claimant] + couple: + members: [non_dep_1, non_dep_2] + households: + household: + members: [claimant, non_dep_1, non_dep_2] + output: + # reg 74(3); the couple's own unit has no non-dependants. + housing_benefit_non_dep_deductions: [5_470.40, 0] + +- name: No deductions where the claimant is exempt + period: 2026 + absolute_error_margin: 0 + input: + people: + claimant: + household_benefits_individual_non_dep_deduction: 0 + non_dep: + household_benefits_individual_non_dep_deduction: 1_060.80 + benunits: + claimant_unit: + members: [claimant] + housing_benefit_non_dep_deductions_claimant_exempt: true + non_dep_unit: + members: [non_dep] + housing_benefit_non_dep_deductions_claimant_exempt: false + households: + household: + members: [claimant, non_dep] + output: + housing_benefit_non_dep_deductions: [0, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml new file mode 100644 index 0000000000..f4de59feff --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml @@ -0,0 +1,77 @@ +# HB Regs 2006 (SI 2006/213) reg 74(6); SI 2006/214 reg 55(6). + +- name: Blind claimant + period: 2026 + input: + age: 40 + is_blind: true + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Claimant on Attendance Allowance + period: 2026 + input: + age: 70 + aa_category: LOWER + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Partner on the DLA care component + period: 2026 + input: + people: + claimant: + age: 40 + partner: + age: 38 + dla_sc_category: LOWER + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Claimant on the PIP daily living component + period: 2026 + input: + age: 40 + pip_dl_category: STANDARD + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: Claimant on Armed Forces Independence Payment + period: 2026 + input: + age: 40 + armed_forces_independence_payment: 1_000 + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + +- name: A child's DLA care component does not exempt the claimant + period: 2026 + input: + people: + claimant: + age: 40 + child: + age: 10 + dla_sc_category: HIGHER + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + output: + housing_benefit_non_dep_deductions_claimant_exempt: false + +- name: DLA mobility only does not exempt the claimant + period: 2026 + input: + age: 40 + dla_m_category: HIGHER + output: + housing_benefit_non_dep_deductions_claimant_exempt: false diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml new file mode 100644 index 0000000000..31dd42ed94 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml @@ -0,0 +1,1312 @@ +# National Council Tax Reduction schemes' own non-dependant deductions: +# England pensioners SI 2012/2885 Sch 1 para 8; Wales WSI 2013/3029 Sch 1 +# para 3 and Sch 6 para 5; Scotland SSI 2012/319 reg 48 and SSI 2021/249 +# reg 90. 2026-27 weekly sums, with bands including their lower edge: +# England: under 279 5.20; 279 10.60; 485 13.30; 605 and above 15.95. +# Wales: under 279 6.70; 279 13.35; 485 16.80; 605 and above 20.05. +# Scotland: under 286 5.55; 286 10.95; 497 13.90; 614 and above 16.55. +# A non-dependant not in remunerative work (under 16 hours) pays the lowest. +# The applicant is the benefit unit with the household's oldest member, so +# working-age applicants here are 60. + +- name: England in remunerative work with no income pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 0 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: England just below 279 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 14507.48 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # 278.99 a week + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: England exactly 279 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 14508 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: England exactly 485 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 25220 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 691.60] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: England just below 605 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 31459.48 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # 604.99 a week + council_tax_reduction_individual_non_dep_deduction: [0, 691.60] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: England exactly 605 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 31460 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 829.40] + council_tax_reduction_non_dep_deductions: [829.40, 0] + +- name: England not in remunerative work pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 40000 + weekly_hours: 15.9 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: England 2025-26 top amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 30004 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 798.20] + council_tax_reduction_non_dep_deductions: [798.20, 0] + +- name: Scotland pensioner claimant, below 286 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 14871.48 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + # 285.99 a week + council_tax_reduction_individual_non_dep_deduction: [0, 288.60] + council_tax_reduction_non_dep_deductions: [288.60, 0] + +- name: Scotland pensioner claimant, exactly 286 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 14872 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 569.40] + council_tax_reduction_non_dep_deductions: [569.40, 0] + +- name: Scotland working-age claimant, exactly 497 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 25844 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 722.80] + council_tax_reduction_non_dep_deductions: [722.80, 0] + +- name: Scotland working-age claimant, exactly 614 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 31928 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 860.60] + council_tax_reduction_non_dep_deductions: [860.60, 0] + +- name: Scotland 2025-26 lowest amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 40000 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + +- name: Wales pensioner claimant, not in remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 40000 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 348.40] + council_tax_reduction_non_dep_deductions: [348.40, 0] + +- name: Wales pensioner claimant, exactly 279 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 14508 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 694.20] + council_tax_reduction_non_dep_deductions: [694.20, 0] + +- name: Wales working-age claimant, exactly 485 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 25220 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 873.60] + council_tax_reduction_non_dep_deductions: [873.60, 0] + +- name: Wales working-age claimant, exactly 605 a week + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 31460 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 1_042.60] + council_tax_reduction_non_dep_deductions: [1_042.60, 0] + +- name: Wales 2025-26 lowest amount + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 325.00] + council_tax_reduction_non_dep_deductions: [325.00, 0] + +- name: English working-age households have no national deduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 36400 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # Unsupported English working-age local schemes keep reported CTB. + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A full-time student is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 20 + current_education: TERTIARY + total_income: 20000 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # SI 2012/2885 Sch 1 para 8(7)(c) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A 40-year-old on Income Support is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 1000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(8)(a) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A 40-year-old on income-based JSA is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 1000 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A 40-year-old on income-related ESA is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 1000 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: A non-dependant on State Pension Credit is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 70 + total_income: 9000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: Universal Credit without earned income is exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(8)(c) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: Universal Credit with earned income is not exempt + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + employment_income: 2000 + weekly_hours: 10 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Scotland Income Support exemption applies at working age too + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 1000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + # SSI 2021/249 reg 90(8)(a) + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: "England: no Universal Credit exemption in 2014-15" + period: 2014 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # Parameters before 2015 are read at 1 January (PE-UK converts to fiscal + # years from 2015), so only the exemption is asserted here. + council_tax_reduction_non_dep_deduction_exempt: [false, false] + +- name: "England: Universal Credit exemption from 2015-16" + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # inserted by SI 2014/3312 + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: "Wales: no Universal Credit exemption in 2014-15" + period: 2014 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # Parameters before 2015 are read at 1 January (PE-UK converts to fiscal + # years from 2015), so only the exemption is asserted here. + council_tax_reduction_non_dep_deduction_exempt: [false, false] + +- name: "Wales: Universal Credit exemption from 2015-16" + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # added by WSI 2015/44 + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: "Scotland: no Universal Credit exemption in 2015-16" + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_individual_non_dep_deduction: [0, 200.20] + council_tax_reduction_non_dep_deductions: [200.20, 0] + +- name: "Scotland: Universal Credit exemption from 2016-17" + period: 2016 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + total_income: 0 + weekly_hours: 0 + age: 40 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son] + country: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + # inserted by SSI 2016/81 + council_tax_reduction_individual_non_dep_deduction: [0, 0.00] + council_tax_reduction_non_dep_deductions: [0.00, 0] + +- name: No deduction where the applicant is blind + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_blind: True + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 36400 + weekly_hours: 35 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(6)(a); the son's own amount is 15.95 a week, but none is deducted. + council_tax_reduction_individual_non_dep_deduction: [0, 829.40] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: "England: one deduction for a non-dependant couple, on their joint income" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + age: 40 + current_education: NOT_IN_EDUCATION + total_income: 15600 + weekly_hours: 35 + partner: + age: 38 + current_education: NOT_IN_EDUCATION + total_income: 0 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son, partner] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son, partner] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # para 8(3)-(4): joint 300 a week is in the 279 band (10.60); the partner + # not in remunerative work would pay 5.20; only the higher is deducted. + council_tax_reduction_individual_non_dep_deduction: [0, 551.20, 270.40] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: "Wales working age: both members of a couple on Universal Credit are deducted" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 60 + son: + employment_income: 15600 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + partner: + age: 38 + current_education: NOT_IN_EDUCATION + employment_income: 1000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son, partner] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son, partner] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # WSI 2013/3029 Sch 6 para 5(3): one deduction per couple "other than where + # there is an award of universal credit". Joint 319.23 a week: 13.35 and 6.70. + council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 348.40] + council_tax_reduction_non_dep_deductions: [1_042.60, 0] + +- name: "Wales pensioner: one deduction for a couple on Universal Credit" + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + son: + employment_income: 15600 + age: 40 + current_education: NOT_IN_EDUCATION + weekly_hours: 35 + partner: + age: 38 + current_education: NOT_IN_EDUCATION + employment_income: 1000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son, partner] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, son, partner] + country: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # Sch 1 para 3(3) has no Universal Credit exception. + council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 348.40] + council_tax_reduction_non_dep_deductions: [694.20, 0] + +- name: A Pension Credit pensioner living with a working son (issue 1934) + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + son: + age: 40 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + son_benunit: + members: [son] + would_claim_uc: false + households: + household: + members: [pensioner, son] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + in_receipt_of_guarantee_credit: [true, false] + # 30,000 / 52 = 576.92 a week, in the 485 band: 13.30 a week, 691.60 a + # year. On the Housing Benefit scale it was 119.85 a week, which took the + # whole 2,000. + council_tax_reduction_non_dep_deductions: [691.60, 0] + council_tax_reduction: 1_308.40 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml index 6e52f66d3d..b1e4be9ded 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml @@ -245,14 +245,12 @@ guarantee_credit: [3_376, 0] in_receipt_of_guarantee_credit: [true, false] council_tax_reduction_applicable_income: [0, 5_000] - # The national schemes' non-dependant deduction currently uses Housing - # Benefit amounts (20.40 a week here), not the CTR schedules' own amounts - # and exemptions (SI 2012/2885 Sch 1 para 8); this pins that the deduction - # still applies on guarantee credit, not its size. - council_tax_reduction_non_dep_deductions: [1_060.80, 0] - # 2,000 - 1,060.80 - simulated_council_tax_reduction_benunit: [939.20, 0] - council_tax_reduction: 939.20 + # SI 2012/2885 Sch 1 para 8(1)(b): the son has no hours, so he is not in + # remunerative work and pays the 2026-27 lowest amount, 5.20 a week. + council_tax_reduction_non_dep_deductions: [270.40, 0] + # 2,000 - 270.40 + simulated_council_tax_reduction_benunit: [1_729.60, 0] + council_tax_reduction: 1_729.60 - name: Mixed-age couple is not in receipt of guarantee credit, so the capital limit applies period: 2026 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py new file mode 100644 index 0000000000..66fd8dc9d4 --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -0,0 +1,545 @@ +"""Property-based and exhaustive tests for the Housing Benefit and national +Council Tax Reduction non-dependant deductions. + +Law: +- Housing Benefit: SI 2006/213 reg 74 (working age) and SI 2006/214 reg 55 + (pension age). +- Council Tax Reduction: SI 2012/2885 Sch 1 para 8 (England, pensioners); + WSI 2013/3029 Sch 1 para 3 and Sch 6 para 5 (Wales); SSI 2012/319 reg 48 and + SSI 2021/249 reg 90 (Scotland). + +Invariants, for any generated population of households, each a claimant family +and a non-dependant family of one or two adults: + +1. An exempt non-dependant's individual deduction is 0, for Housing Benefit and + for Council Tax Reduction. +2. Bounds: an eligible non-dependant who is not exempt pays between the + scale's lowest and highest weekly amounts. In English working-age + households (local schemes) the national CTR deduction is 0. +3. A non-dependant not in remunerative work (under 16 hours) pays the lowest + amount, whatever their income. +4. If the claimant or partner is exempt, the claimant family's deductions + are 0. +5. Aggregation: a family's deductions are the sum, over the other families in + the household, of their higher member's amount (both members' amounts for a + Welsh working-age applicant's non-dependant couple with a Universal Credit + award), recomputed here from the individual amounts. +6. Monotonicity (metamorphic): raising any non-dependant's income never lowers + an individual deduction or a family's deductions. +7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, + including uprated years, an income at a band's lower edge gets that band's + amount and the next lower representable income gets the band below's. +8. Differential: each CTR scale's parameter equals the enacted sums in + tests/fixtures/council_tax_reduction_non_dependant_deductions.yaml. +9. Differential: each exemption flag equals a reading of the provisions + straight from the generated inputs. Housing Benefit: a full-time student, + a State Pension Credit recipient, or under 25 and on Income Support, + income-based JSA or Universal Credit without earned income. CTR (any age): + a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national + scheme, on Universal Credit without earned income. +""" + +from pathlib import Path + +import numpy as np +import pytest +import yaml +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import CountryTaxBenefitSystem, Simulation + +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Local authorities without a modelled local scheme. +COUNTRIES = { + "ENGLAND": "MAIDSTONE", + "WALES": "CARDIFF", + "SCOTLAND": "GLASGOW_CITY", +} +CTR_SCALES = { + "ENGLAND": "england.council_tax_reduction.pensioners.non_dep_deduction", + "WALES": "wales.council_tax_reduction.non_dep_deduction", + "SCOTLAND": "scotland.council_tax_reduction.non_dep_deduction", +} +FIXTURE = ( + Path(__file__).parent + / "fixtures" + / "council_tax_reduction_non_dependant_deductions.yaml" +) +WEEKS = 52 +CLAIMANT_DISABILITY = [ + None, + None, + None, + "is_blind", + "attendance_allowance", + "dla_sc", + "pip_dl", + "armed_forces_independence_payment", +] +BENEFITS = [ + "income_support", + "jsa_income", + "esa_income", + "pension_credit", + "universal_credit_pre_benefit_cap", +] +SYSTEM = CountryTaxBenefitSystem() + + +def for_years(situation, years): + """Key every input by year: unkeyed inputs apply only to the default + period.""" + for group in situation.values(): + for entity in group.values(): + for key, value in entity.items(): + if key != "members" and not isinstance(value, dict): + entity[key] = {str(year): value for year in years} + return situation + + +@st.composite +def households(draw): + # The CTR applicant is the family with the household's oldest member, so + # claimants are older than their non-dependants. State Pension age is at + # most 67 in 2025-30. + claimant_age = draw(st.one_of(st.integers(50, 64), st.integers(68, 95))) + non_dependants = [ + dict( + age=draw(st.integers(18, claimant_age - 1)), + education=draw( + st.sampled_from( + ["NOT_IN_EDUCATION"] * 4 + ["TERTIARY", "UPPER_SECONDARY"] + ) + ), + earnings=draw(st.one_of(st.just(0.0), st.floats(0, 60_000))), + other_income=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + hours=draw(st.sampled_from([0.0, 10.0, 15.9, 16.0, 35.0, 45.0])), + income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + ) + for _ in range(draw(st.integers(1, 2))) + ] + return dict( + country=draw(st.sampled_from(sorted(COUNTRIES))), + claimant_age=claimant_age, + claimant_disability=draw(st.sampled_from(CLAIMANT_DISABILITY)), + benefits={ + benefit: draw(st.sampled_from([0.0, 0.0, 0.0, 1_000.0])) + for benefit in BENEFITS + }, + non_dependants=non_dependants, + ) + + +def build(population, year, raise_income=False): + people, benunits, households_, persons = {}, {}, {}, [] + for h, household in enumerate(population): + claimant = f"claimant_{h}" + flag = household["claimant_disability"] + people[claimant] = dict( + age=household["claimant_age"], + current_education="NOT_IN_EDUCATION", + total_income=0.0, + employment_income=0.0, + weekly_hours=0.0, + is_blind=flag == "is_blind", + **{ + name: 1_000.0 if flag == name else 0.0 + for name in CLAIMANT_DISABILITY[3:] + if name != "is_blind" + }, + ) + persons.append(dict(household=h, benunit=2 * h, non_dependant=None)) + members = [] + for n, nd in enumerate(household["non_dependants"]): + name = f"non_dependant_{h}_{n}" + other = nd["other_income"] + (nd["income_rise"] if raise_income else 0) + people[name] = dict( + age=nd["age"], + current_education=nd["education"], + total_income=nd["earnings"] + other, + employment_income=nd["earnings"], + weekly_hours=nd["hours"], + is_blind=False, + **{ + benefit: 0.0 + for benefit in CLAIMANT_DISABILITY[3:] + if benefit != "is_blind" + }, + ) + members.append(name) + persons.append(dict(household=h, benunit=2 * h + 1, non_dependant=nd)) + benunits[f"claimant_unit_{h}"] = dict( + members=[claimant], + benunit_is_rent_liable=True, + **{benefit: 0.0 for benefit in BENEFITS}, + ) + benunits[f"non_dependant_unit_{h}"] = dict( + members=members, + benunit_is_rent_liable=False, + **household["benefits"], + ) + households_[f"household_{h}"] = dict( + members=[claimant, *members], + country=household["country"], + local_authority=COUNTRIES[household["country"]], + ) + situation = dict(people=people, benunits=benunits, households=households_) + return Simulation(situation=for_years(situation, [year])), persons + + +def calculate(sim, year): + names = dict( + person=[ + "housing_benefit_non_dep_deduction_exempt", + "housing_benefit_individual_non_dep_deduction_eligible", + "household_benefits_individual_non_dep_deduction", + "council_tax_reduction_non_dep_deduction_exempt", + "council_tax_reduction_individual_non_dep_deduction_eligible", + "council_tax_reduction_individual_non_dep_deduction", + ], + benunit=[ + "housing_benefit_non_dep_deductions", + "housing_benefit_non_dep_deductions_claimant_exempt", + "council_tax_reduction_non_dep_deductions", + "universal_credit_pre_benefit_cap", + ], + household=[ + "council_tax_reduction_household_has_non_dep_exemption", + "council_tax_reduction_household_has_pensioner", + ], + ) + return { + name: np.asarray(sim.calculate(name, year)) + for entity in names.values() + for name in entity + } + + +def aggregate(individual, persons, benunit_count, each_member): + """Recompute family deductions from individual amounts (invariant 5).""" + by_benunit = {} + for i, person in enumerate(persons): + by_benunit.setdefault(person["benunit"], []).append(individual[i]) + own = np.array( + [ + (sum if each_member[b] else max)(by_benunit.get(b, [0.0])) + for b in range(benunit_count) + ] + ) + household_of = {p["benunit"]: p["household"] for p in persons} + totals = {} + for b in range(benunit_count): + totals[household_of[b]] = totals.get(household_of[b], 0.0) + own[b] + return np.array([totals[household_of[b]] - own[b] for b in range(benunit_count)]) + + +@PROPERTY_SETTINGS +@given( + population=st.lists(households(), min_size=1, max_size=12), + year=st.integers(2025, 2030), +) +@example( + # Rare under random draws: a Welsh working-age applicant's non-dependant + # couple on Universal Credit, both with earnings (Sch 6 para 5(3)); and + # the Housing Benefit under-25 boundary on Income Support (reg 74(8)). + population=[ + dict( + country="WALES", + claimant_age=60, + claimant_disability=None, + benefits={ + **dict.fromkeys(BENEFITS, 0.0), + "universal_credit_pre_benefit_cap": 1_000.0, + }, + non_dependants=[ + dict( + age=30, + education="NOT_IN_EDUCATION", + earnings=15_600.0, + other_income=0.0, + hours=35.0, + income_rise=0.0, + ), + dict( + age=28, + education="NOT_IN_EDUCATION", + earnings=1_000.0, + other_income=0.0, + hours=0.0, + income_rise=5_000.0, + ), + ], + ), + *[ + dict( + country="ENGLAND", + claimant_age=70, + claimant_disability=None, + benefits={**dict.fromkeys(BENEFITS, 0.0), "income_support": 1_000.0}, + non_dependants=[ + dict( + age=age, + education="NOT_IN_EDUCATION", + earnings=0.0, + other_income=0.0, + hours=0.0, + income_rise=0.0, + ) + ], + ) + for age in [24, 25] + ], + ], + year=2026, +) +def test_non_dependant_deduction_invariants(population, year): + sim, persons = build(population, year) + r = calculate(sim, year) + p = SYSTEM.parameters(str(year)) + hb = p.gov.dwp.housing_benefit.non_dep_deduction + hb_lowest, hb_top = hb.amount.amounts[0] * WEEKS, hb.amount.amounts[-1] * WEEKS + pension_age = np.repeat( + r["council_tax_reduction_household_has_pensioner"], + [1 + len(h["non_dependants"]) for h in population], + ) + + for i, person in enumerate(persons): + household = population[person["household"]] + hb_deduction = r["household_benefits_individual_non_dep_deduction"][i] + ctr_deduction = r["council_tax_reduction_individual_non_dep_deduction"][i] + # 1. Exempt => 0. + if r["housing_benefit_non_dep_deduction_exempt"][i]: + assert hb_deduction == 0 + if r["council_tax_reduction_non_dep_deduction_exempt"][i]: + assert ctr_deduction == 0 + nd = person["non_dependant"] + if nd is None: + continue + # 9. The exemptions, read straight from the inputs. + benefits = household["benefits"] + student = nd["education"] != "NOT_IN_EDUCATION" + uc_without_earnings = ( + benefits["universal_credit_pre_benefit_cap"] > 0 and nd["earnings"] <= 0 + ) + hb_exempt = ( + student + or benefits["pension_credit"] > 0 + or ( + nd["age"] < 25 + and ( + benefits["income_support"] > 0 + or benefits["jsa_income"] > 0 + or uc_without_earnings + ) + ) + ) + assert bool(r["housing_benefit_non_dep_deduction_exempt"][i]) == hb_exempt + national = household["country"] != "ENGLAND" or pension_age[i] + ctr_exempt = ( + student + or any( + benefits[b] > 0 + for b in [ + "income_support", + "jsa_income", + "esa_income", + "pension_credit", + ] + ) + or (national and uc_without_earnings) + ) + assert ( + bool(r["council_tax_reduction_non_dep_deduction_exempt"][i]) == ctr_exempt + ) + # 2-3. Housing Benefit bounds and the not-in-work amount. + if ( + r["housing_benefit_individual_non_dep_deduction_eligible"][i] + and not r["housing_benefit_non_dep_deduction_exempt"][i] + ): + assert hb_lowest - 0.01 <= hb_deduction <= hb_top + 0.01 + if nd["hours"] < hb.remunerative_work_hours: + assert hb_deduction == pytest.approx(hb_lowest, abs=0.01) + # 2-3. Council Tax Reduction bounds and the not-in-work amount. + country = household["country"] + if not national: + assert ctr_deduction == 0 + elif ( + r["council_tax_reduction_individual_non_dep_deduction_eligible"][i] + and not r["council_tax_reduction_non_dep_deduction_exempt"][i] + ): + scale = p.gov.local_authorities + for part in CTR_SCALES[country].split("."): + scale = getattr(scale, part) + lowest = scale.amount.amounts[0] * WEEKS + top = scale.amount.amounts[-1] * WEEKS + assert lowest - 0.01 <= ctr_deduction <= top + 0.01 + if nd["hours"] < scale.remunerative_work_hours: + assert ctr_deduction == pytest.approx(lowest, abs=0.01) + + benunit_count = 2 * len(population) + # 4-5. Housing Benefit aggregation and the claimant exemption. + expected_hb = aggregate( + r["household_benefits_individual_non_dep_deduction"], + persons, + benunit_count, + [False] * benunit_count, + ) + expected_hb = np.where( + r["housing_benefit_non_dep_deductions_claimant_exempt"], 0, expected_hb + ) + np.testing.assert_allclose( + r["housing_benefit_non_dep_deductions"], expected_hb, atol=0.01 + ) + # 4-5. Council Tax Reduction aggregation and the applicant exemption. + welsh_working_age = [ + population[b // 2]["country"] == "WALES" + and not r["council_tax_reduction_household_has_pensioner"][b // 2] + and r["universal_credit_pre_benefit_cap"][b] > 0 + for b in range(benunit_count) + ] + expected_ctr = aggregate( + r["council_tax_reduction_individual_non_dep_deduction"], + persons, + benunit_count, + welsh_working_age, + ) + household_exempt = np.repeat( + r["council_tax_reduction_household_has_non_dep_exemption"], 2 + ) + expected_ctr = np.where(household_exempt, 0, expected_ctr) + np.testing.assert_allclose( + r["council_tax_reduction_non_dep_deductions"], expected_ctr, atol=0.01 + ) + + # 6. Raising non-dependants' incomes never lowers a deduction. + raised, _ = build(population, year, raise_income=True) + s = calculate(raised, year) + for name in [ + "household_benefits_individual_non_dep_deduction", + "council_tax_reduction_individual_non_dep_deduction", + "housing_benefit_non_dep_deductions", + "council_tax_reduction_non_dep_deductions", + ]: + assert np.all(s[name] >= r[name] - 1e-3), name + + +def first_income_at_or_above(threshold): + """The smallest float32 annual income (the input's storage type) whose + weekly value, computed as the model does (the benefit-unit sum returns + float64, then divided by 52), is at least the threshold.""" + + def weekly(annual): + return float(np.float32(annual)) / WEEKS + + income = np.float32(threshold * WEEKS) + while weekly(income) < threshold: + income = np.nextafter(income, np.float32(np.inf)) + while weekly(np.nextafter(income, np.float32(-np.inf))) >= threshold: + income = np.nextafter(income, np.float32(-np.inf)) + return income + + +SCALES = [ + ("HOUSING_BENEFIT", range(2019, 2031)), + ("ENGLAND", range(2015, 2031)), + ("WALES", range(2015, 2031)), + ("SCOTLAND", range(2015, 2031)), +] + + +@pytest.mark.parametrize("scale_name, years", SCALES, ids=[s for s, _ in SCALES]) +def test_band_edges_are_inclusive(scale_name, years): + """Invariant 7: every lower edge, every year, both schemes.""" + country = "ENGLAND" if scale_name == "HOUSING_BENEFIT" else scale_name + edges_by_year = {} + for year in years: + p = SYSTEM.parameters(str(year)) + if scale_name == "HOUSING_BENEFIT": + scale = p.gov.dwp.housing_benefit.non_dep_deduction.amount + else: + scale = p.gov.local_authorities + for part in CTR_SCALES[scale_name].split("."): + scale = getattr(scale, part) + scale = scale.amount + edges_by_year[year] = list( + zip(scale.thresholds[1:], scale.amounts[1:], scale.amounts[:-1]) + ) + count = max(len(edges) for edges in edges_by_year.values()) + people, benunits, households_ = {}, {}, {} + for k in range(2 * count): + claimant, non_dependant = f"claimant_{k}", f"non_dependant_{k}" + incomes = {} + for year, edges in edges_by_year.items(): + threshold, _, _ = edges[k // 2] + at_edge = first_income_at_or_above(threshold) + incomes[str(year)] = float( + at_edge if k % 2 == 0 else np.nextafter(at_edge, np.float32(-np.inf)) + ) + people[claimant] = dict(age=80, current_education="NOT_IN_EDUCATION") + people[non_dependant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + weekly_hours={str(y): 35.0 for y in years}, + total_income=incomes, + employment_income={str(y): 0.0 for y in years}, + ) + benunits[f"claimant_unit_{k}"] = dict( + members=[claimant], + benunit_is_rent_liable={str(y): True for y in years}, + **{b: {str(y): 0.0 for y in years} for b in BENEFITS}, + ) + benunits[f"non_dependant_unit_{k}"] = dict( + members=[non_dependant], + benunit_is_rent_liable={str(y): False for y in years}, + **{b: {str(y): 0.0 for y in years} for b in BENEFITS}, + ) + households_[f"household_{k}"] = dict( + members=[claimant, non_dependant], + country=country, + local_authority=COUNTRIES[country], + ) + sim = Simulation( + situation=for_years( + dict(people=people, benunits=benunits, households=households_), years + ) + ) + variable = ( + "household_benefits_individual_non_dep_deduction" + if scale_name == "HOUSING_BENEFIT" + else "council_tax_reduction_individual_non_dep_deduction" + ) + for year, edges in edges_by_year.items(): + deductions = np.asarray(sim.calculate(variable, year))[1::2] / WEEKS + for k, (threshold, amount, below) in enumerate(edges): + assert deductions[2 * k] == pytest.approx(amount, abs=1e-4), ( + year, + threshold, + ) + assert deductions[2 * k + 1] == pytest.approx(below, abs=1e-4), ( + year, + threshold, + ) + + +def test_ctr_scales_match_enacted_sums(): + """Invariant 8: the parameters reproduce the enacted table.""" + fixture = yaml.safe_load(FIXTURE.read_text()) + for scheme, entry in fixture.items(): + for year, row in entry["years"].items(): + # PolicyEngine UK reads parameters at 30 April from 2015; earlier + # years are read at that date directly. + instant = str(year) if year >= 2015 else f"{year}-04-30" + node = SYSTEM.parameters(instant) + for part in entry["parameter"].split("."): + node = getattr(node, part) + assert list(node.thresholds) == pytest.approx(row["thresholds"]), ( + scheme, + year, + ) + assert list(node.amounts) == pytest.approx(row["amounts"]), (scheme, year) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index 95b6bcb695..32dc5d9662 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -5,12 +5,30 @@ class household_benefits_individual_non_dep_deduction(Variable): value_type = float entity = Person label = "Housing Benefit individual non-dependent deduction" + documentation = ( + "A non-dependant in remunerative work pays the deduction for the band " + "containing their normal gross weekly income, counting a couple's " + "joint income; bands include their lower edge. A non-dependant not in " + "remunerative work pays the lowest amount. Gross income is taxable " + "total income, so the disregarded disability benefits are excluded." + ) definition_period = YEAR unit = GBP defined_for = "housing_benefit_individual_non_dep_deduction_eligible" + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction - weekly_income = person("total_income", period) / WEEKS_IN_YEAR - deduction = p.amount.calc(weekly_income, right=True) - return deduction * WEEKS_IN_YEAR + income = max_(0, person("total_income", period)) + counted = person("age", period) >= p.age_threshold + weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + in_remunerative_work = ( + person("weekly_hours", period) >= p.remunerative_work_hours + ) + banded = p.amount.calc(weekly_income) + not_in_remunerative_work = p.amount.calc(np.zeros_like(weekly_income)) + weekly = where(in_remunerative_work, banded, not_in_remunerative_work) + return weekly * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py index 37ea75a311..a02f8f8da8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_individual_non_dep_deduction_eligible.py @@ -6,9 +6,14 @@ class housing_benefit_individual_non_dep_deduction_eligible(Variable): entity = Person label = "eligible person for the Housing Benefit non-dependent deduction" definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) def formula(person, period, parameters): rent_liable = person.benunit("benunit_is_rent_liable", period) p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction age_eligible = person("age", period) >= p.age_threshold - return age_eligible & ~rent_liable + exempt = person("housing_benefit_non_dep_deduction_exempt", period) + return age_eligible & ~rent_liable & ~exempt diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py new file mode 100644 index 0000000000..1c998c6694 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -0,0 +1,57 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + is_full_time_student_non_dep, +) + + +class housing_benefit_non_dep_deduction_exempt(Variable): + value_type = bool + entity = Person + label = "Housing Benefit makes no non-dependant deduction for this person" + documentation = ( + "Modelled exemptions: a full-time student (SI 2006/213 reg 74(7)(c)-(e); " + "SI 2006/214 reg 55(7)(e)); a non-dependant on State Pension Credit " + "(reg 74(10); reg 55(9)); and a non-dependant under 25 on Income " + "Support or income-based JSA, or entitled to Universal Credit " + "calculated on no earned income (reg 74(8); reg 55(8)). Not modelled: " + "a working student's summer vacation (reg 74(7)(d)), under-25s on " + "assessment-phase income-related ESA (no ESA group input), youth " + "training allowances, a normal home elsewhere, and absence in hospital, " + "prison or on armed forces operations." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction + full_time_student = is_full_time_student_non_dep(person, period) + on_pension_credit = person.benunit("pension_credit", period) > 0 + on_legacy_income_related_benefit = ( + person.benunit("income_support", period) > 0 + ) | (person.benunit("jsa_income", period) > 0) + # Entitlement before the benefit cap: universal_credit itself depends on + # Housing Benefit through the cap. + entitled_to_universal_credit = ( + person.benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + earned_income = add( + person, period, ["employment_income", "self_employment_income"] + ) + universal_credit_without_earned_income = entitled_to_universal_credit & ( + earned_income <= 0 + ) + under_age_limit = person("age", period) < p.income_related_benefit_age_limit + return ( + full_time_student + | on_pension_credit + | ( + under_age_limit + & ( + on_legacy_income_related_benefit + | universal_credit_without_earned_income + ) + ) + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 3c35be82d2..86eb32437a 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -5,15 +5,29 @@ class housing_benefit_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "non-dependent deductions" + documentation = ( + "Deductions for the non-dependants in other benefit units of the " + "household: one per couple, the higher of the two members' amounts, " + "and none if the claimant or partner is exempt." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) def formula(benunit, period, parameters): deductions = benunit.members( "household_benefits_individual_non_dep_deduction", period ) - non_dep_deductions_in_hh = benunit.max( - benunit.members.household.sum(deductions) + deduction_for_benunit = benunit.max(deductions) + is_benunit_head = benunit.members("is_benunit_head", period) + counted = is_benunit_head * benunit.project(deduction_for_benunit) + deductions_in_household = benunit.max(benunit.members.household.sum(counted)) + claimant_exempt = benunit( + "housing_benefit_non_dep_deductions_claimant_exempt", period + ) + return where( + claimant_exempt, 0, deductions_in_household - deduction_for_benunit ) - non_dep_deductions_in_bu = benunit.sum(deductions) - return non_dep_deductions_in_hh - non_dep_deductions_in_bu diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py new file mode 100644 index 0000000000..4568e9f714 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_non_dep_deductions_claimant_exempt(Variable): + value_type = bool + entity = BenUnit + label = "Housing Benefit claimant exempt from all non-dependant deductions" + documentation = ( + "No deduction is made for any non-dependant if the claimant or partner " + "is blind or receives Attendance Allowance, the DLA care component, the " + "PIP daily living component or Armed Forces Independence Payment (SI " + "2006/213 reg 74(6); SI 2006/214 reg 55(6)). Scottish disability " + "assistance (pension age and adult disability payments, child " + "disability payment, Scottish adult DLA) has no separate input." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + ) + + def formula(benunit, period, parameters): + person = benunit.members + # Claimant or partner as in council_tax_reduction_household_has_non_dep_exemption. + claimant_or_partner = person("is_adult", period) + qualifying = ( + person("is_blind", period) + | (person("attendance_allowance", period) > 0) + | (person("dla_sc", period) > 0) + | (person("pip_dl", period) > 0) + | (person("armed_forces_independence_payment", period) > 0) + ) + return benunit.any(claimant_or_partner & qualifying) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py index 102c093f63..fea66dd381 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py @@ -1,15 +1,86 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_england_pensioner_scheme, + is_scotland_scheme, + is_wales_scheme, +) class council_tax_reduction_individual_non_dep_deduction(Variable): value_type = float entity = Person label = "CTR individual non-dependent deduction" + documentation = ( + "The England pensioner, Scottish and Welsh schemes' own weekly scales. " + "A non-dependant in remunerative work pays the deduction for the band " + "containing their normal gross weekly income, counting a couple's " + "joint income; bands include their lower edge. A non-dependant not in " + "remunerative work pays the lowest amount; an exempt non-dependant " + "pays nothing. Gross income is taxable total income, so the " + "disregarded disability benefits are excluded. English working-age " + "local schemes have their own variables." + ) definition_period = YEAR unit = GBP defined_for = "council_tax_reduction_individual_non_dep_deduction_eligible" + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + ) def formula(person, period, parameters): - p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction - weekly_total_income = person("total_income", period) / WEEKS_IN_YEAR - return p.amount.calc(weekly_total_income, right=True) * WEEKS_IN_YEAR + local_authorities = parameters(period).gov.local_authorities + england = ( + local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction + ) + scotland = local_authorities.scotland.council_tax_reduction.non_dep_deduction + wales = local_authorities.wales.council_tax_reduction.non_dep_deduction + country = person.household("country", period) + has_pensioner = person.household( + "council_tax_reduction_household_has_pensioner", period + ) + schemes = [ + is_england_pensioner_scheme(country, has_pensioner), + is_scotland_scheme(country), + is_wales_scheme(country), + ] + income = max_(0, person("total_income", period)) + counted = person("age", period) >= 18 + weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + no_income = np.zeros_like(weekly_income) + remunerative_work_hours = select( + schemes, + [ + england.remunerative_work_hours, + scotland.remunerative_work_hours, + wales.remunerative_work_hours, + ], + default=np.inf, + ) + in_remunerative_work = person("weekly_hours", period) >= remunerative_work_hours + banded = select( + schemes, + [ + england.amount.calc(weekly_income), + scotland.amount.calc(weekly_income), + wales.amount.calc(weekly_income), + ], + default=0, + ) + not_in_remunerative_work = select( + schemes, + [ + england.amount.calc(no_income), + scotland.amount.calc(no_income), + wales.amount.calc(no_income), + ], + default=0, + ) + weekly = where(in_remunerative_work, banded, not_in_remunerative_work) + # The local schemes share the eligibility variable and apply their own + # exemptions, so the national ones apply here. + exempt = person("council_tax_reduction_non_dep_deduction_exempt", period) + return where(exempt, 0, weekly * WEEKS_IN_YEAR) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py new file mode 100644 index 0000000000..004237bc6a --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -0,0 +1,80 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( + is_full_time_student_non_dep, +) +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_england_pensioner_scheme, + is_scotland_scheme, + is_wales_scheme, +) + + +class council_tax_reduction_non_dep_deduction_exempt(Variable): + value_type = bool + entity = Person + label = "national Council Tax Reduction makes no non-dependant deduction for this person" + documentation = ( + "Modelled exemptions in the England pensioner, Scottish and Welsh " + "schemes: a full-time student; a non-dependant on Income Support, " + "income-based JSA, income-related ESA or State Pension Credit (any " + "age); and, from the year each scheme added it, one entitled to " + "Universal Credit calculated on no earned income. Wales excludes " + "members of the ESA work-related activity group; with no ESA group " + "input, income-related ESA is treated as exempt. Not modelled: persons " + "disregarded for council tax discounts (LGFA 1992 Sch 1) other than " + "students, youth training allowances, a normal home elsewhere, and " + "absence in hospital or on armed forces operations." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + ) + + def formula(person, period, parameters): + local_authorities = parameters(period).gov.local_authorities + england = local_authorities.england.council_tax_reduction.pensioners + scotland = local_authorities.scotland.council_tax_reduction + wales = local_authorities.wales.council_tax_reduction + country = person.household("country", period) + has_pensioner = person.household( + "council_tax_reduction_household_has_pensioner", period + ) + schemes = [ + is_england_pensioner_scheme(country, has_pensioner), + is_scotland_scheme(country), + is_wales_scheme(country), + ] + full_time_student = is_full_time_student_non_dep(person, period) + on_income_related_benefit = ( + (person.benunit("income_support", period) > 0) + | (person.benunit("jsa_income", period) > 0) + | (person.benunit("esa_income", period) > 0) + | (person.benunit("pension_credit", period) > 0) + ) + universal_credit_limb = select( + schemes, + [ + england.non_dep_deduction.exempt_universal_credit_without_earned_income, + scotland.non_dep_deduction.exempt_universal_credit_without_earned_income, + wales.non_dep_deduction.exempt_universal_credit_without_earned_income, + ], + default=False, + ) + entitled_to_universal_credit = ( + person.benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + earned_income = add( + person, period, ["employment_income", "self_employment_income"] + ) + universal_credit_without_earned_income = ( + universal_credit_limb & entitled_to_universal_credit & (earned_income <= 0) + ) + return ( + full_time_student + | on_income_related_benefit + | universal_credit_without_earned_income + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index c6b5fc1d8d..4384831a88 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -1,17 +1,55 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_wales_scheme, +) class council_tax_reduction_non_dep_deductions(Variable): value_type = float entity = BenUnit label = "CTR non-dependent deductions" + documentation = ( + "Deductions for the non-dependants in other benefit units of the " + "household: one per couple, the higher of the two members' amounts " + "(both, in the Welsh scheme for people who are not pensioners, when " + "the couple has a Universal Credit award), and none if the applicant " + "or partner is exempt." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/3", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + ) def formula(benunit, period, parameters): deductions = benunit.members( "council_tax_reduction_individual_non_dep_deduction", period ) - deductions_in_household = benunit.max(benunit.members.household.sum(deductions)) - deductions_in_benunit = benunit.sum(deductions) - return deductions_in_household - deductions_in_benunit + country = benunit.household("country", period) + has_pensioner = benunit.household( + "council_tax_reduction_household_has_pensioner", period + ) + # WSI 2013/3029 Sch 6 para 5(3). + each_member_deducted = ( + is_wales_scheme(country) + & ~has_pensioner + & (benunit("universal_credit_pre_benefit_cap", period) > 0) + ) + deduction_for_benunit = where( + each_member_deducted, + benunit.sum(deductions), + benunit.max(deductions), + ) + is_benunit_head = benunit.members("is_benunit_head", period) + counted = is_benunit_head * benunit.project(deduction_for_benunit) + deductions_in_household = benunit.max(benunit.members.household.sum(counted)) + applicant_exempt = benunit.household( + "council_tax_reduction_household_has_non_dep_exemption", period + ) + return where( + applicant_exempt, 0, deductions_in_household - deduction_for_benunit + ) diff --git a/uv.lock b/uv.lock index 47b04e0136..1b1776e219 100644 --- a/uv.lock +++ b/uv.lock @@ -1471,7 +1471,7 @@ wheels = [ [[package]] name = "policyengine-uk" -version = "2.102.3" +version = "2.104.2" source = { editable = "." } dependencies = [ { name = "microdf-python" }, From 86d7ae6e3b86da552610215894c5b31ce1fd4d94 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 02:25:54 -0400 Subject: [PATCH 13/67] Address review: claimant or partner, couple income, reg 6(6), earnings, gross income - Identify the claimant or partner as the benefit unit's head or any member who is not an SSCBA s.142 child or qualifying young person (HB Regs 2006 regs 2(1) and 19; CTR regs 2(1)), replacing is_adult in the HB and CTR claimant exemptions. - Band a couple on the couple's joint income only, give the couple one deduction and any other adult in the family their own, and count benefit receipt (IS, JSA(IB), ESA(IR), SPC, UC) only for the award's claimant or partner. - Treat a non-dependant on IS, JSA(IB) or ESA(IR) as not in remunerative work for HB (reg 6(6)). - Take self-employed losses as nil in the UC no-earned-income test (UC reg 57(2)). - Add the family's UC (before the cap), tax credits and child benefit to gross income. - Tests: the review's counterexamples, a non-vacuous SPC exemption case, and property-test generators and oracles for dependent young people and losses. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- ...benefits_individual_non_dep_deduction.yaml | 126 +++++++++++++ .../non_dependant_deduction_review_cases.yaml | 167 ++++++++++++++++++ ...ncil_tax_reduction_non_dep_deductions.yaml | 40 ++++- ...test_non_dependant_deduction_properties.py | 107 ++++++++--- .../non_dep_deduction/_non_dependants.py | 74 ++++++++ ...d_benefits_individual_non_dep_deduction.py | 25 ++- ...ousing_benefit_non_dep_deduction_exempt.py | 26 +-- .../housing_benefit_non_dep_deductions.py | 18 +- ...efit_non_dep_deductions_claimant_exempt.py | 6 +- ..._reduction_individual_non_dep_deduction.py | 17 +- ..._tax_reduction_non_dep_deduction_exempt.py | 17 +- ...ouncil_tax_reduction_non_dep_deductions.py | 21 +-- 13 files changed, 570 insertions(+), 76 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index f01a97f236..dbfa0aa46e 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge, only a non-dependant in remunerative work is banded (on a couple's joint income, one deduction per couple), the claimant-side and non-dependant exemptions apply, and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner identified by the SSCBA s.142 child and young person definitions; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 41919b3d7c..f21ab85587 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -3,6 +3,8 @@ # reg 6(1)): under £192 a week 20.40; not less than £192 46.85; £279 64.35; # £365 105.20; £485 119.85; £605 and above 131.45. Not in remunerative work: # 20.40 (reg 74(1)(b)). Bands include their lower edge ("not less than"). +# Family benefits (UC, tax credits, child benefit), which count as gross +# income, are held at zero except where a case tests them. - name: In remunerative work with no income pays the lowest amount period: 2026 @@ -12,6 +14,10 @@ age: 30 total_income: 0 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # reg 74(2)(a) and (1)(b) household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -24,6 +30,10 @@ age: 30 total_income: 9983.48 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # 191.99 a week household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -36,6 +46,10 @@ age: 30 total_income: 9984 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 46.85 * 52 @@ -47,6 +61,10 @@ age: 30 total_income: 14508 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 64.35 * 52 @@ -58,6 +76,10 @@ age: 30 total_income: 18980 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 105.2 * 52 @@ -69,6 +91,10 @@ age: 30 total_income: 25220 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 119.85 * 52 @@ -80,6 +106,10 @@ age: 30 total_income: 31460 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 131.45 * 52 @@ -91,6 +121,10 @@ age: 30 total_income: 31459.48 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # 604.99 a week household_benefits_individual_non_dep_deduction: 119.85 * 52 @@ -103,6 +137,10 @@ age: 30 total_income: 40000 weekly_hours: 0 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # reg 74(1)(b): the bands apply only in remunerative work household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -115,6 +153,10 @@ age: 30 total_income: 40000 weekly_hours: 15.9 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -126,6 +168,10 @@ age: 30 total_income: 40000 weekly_hours: 16 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 131.45 * 52 @@ -137,6 +183,10 @@ age: 30 total_income: -5000 weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 20.4 * 52 @@ -158,6 +208,10 @@ benunits: couple: members: [worker, partner] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 households: household: members: [worker, partner] @@ -182,8 +236,80 @@ benunits: family: members: [parent, child] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 households: household: members: [parent, child] output: household_benefits_individual_non_dep_deduction: [1_060.80, 0] + +- name: Universal Credit counts towards gross income + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 40 + total_income: 14_040 # 270 a week + weekly_hours: 16 + universal_credit_pre_benefit_cap: 1_040 # 20 a week + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + # reg 74(2): 290 a week gross is in the 279 band (64.35). UC is not among + # the reg 74(9) disregards. + household_benefits_individual_non_dep_deduction: 64.35 * 52 + +- name: On income-related ESA, 16 hours is not remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 40 + total_income: 10_574.72 # 203.36 a week (supported permitted work) + weekly_hours: 16 + esa_income: 5_200 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + # reg 6(6): a person on income-related ESA is treated as not in + # remunerative work, so pays the reg 74(1)(b) amount. + household_benefits_individual_non_dep_deduction: 20.40 * 52 + +- name: A dependent young person in the family is banded on their own income, not the parent's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + total_income: 9_360 # 180 a week + weekly_hours: 16 + housing_benefit_individual_non_dep_deduction_eligible: true + young_person: + age: 19 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 18 + total_income: 5_200 # 100 a week + weekly_hours: 8 + housing_benefit_individual_non_dep_deduction_eligible: true + benunits: + family: + members: [parent, young_person] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + households: + household: + members: [parent, young_person] + output: + # reg 74(4) joins only a couple's income. The parent's 180 a week is + # under the 192 band (20.40); the young person, under 16 hours, pays 20.40 + # if not exempt (eligibility is an input here). + household_benefits_individual_non_dep_deduction: [1_060.80, 1_060.80] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml new file mode 100644 index 0000000000..e91df019b0 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -0,0 +1,167 @@ +# Counterexamples from the independent review of PolicyEngine/policyengine-uk#2007, +# end to end for Housing Benefit (HB Regs 2006 reg 74) and the England pensioner +# Council Tax Reduction scheme (SI 2012/2885 Sch 1 para 8), 2026-27 amounts. +# The claimant family is the rent-liable family with the household's oldest +# member. Family benefits are held at zero except where a case tests them. + +- name: A dependent 18-year-old's PIP does not exempt the claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + dependant: + age: 18 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + pip_dl_category: STANDARD + lodger_son: + age: 40 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant, dependant] + benunit_is_rent_liable: true + son_unit: + members: [lodger_son] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, dependant, lodger_son] + country: ENGLAND + local_authority: MAIDSTONE + output: + # reg 74(6) and para 8(6) protect the claimant or partner, not a young + # person in the family (reg 19; SSCBA s.142). The son's 576.92 a week is + # in the HB 485 band (119.85) and the CTR 485 band (13.30). + housing_benefit_non_dep_deductions_claimant_exempt: [false, false] + council_tax_reduction_household_has_non_dep_exemption: false + housing_benefit_non_dep_deductions: [6_232.20, 0] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: A 17-year-old claimant's PIP exempts their family + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 17 + current_education: NOT_IN_EDUCATION + pip_dl_category: STANDARD + sibling: + age: 16 + employment_income: 20_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + sibling_unit: + members: [sibling] + benunit_is_rent_liable: false + households: + household: + members: [claimant, sibling] + output: + # The benefit unit's head is the claimant whatever their age. + housing_benefit_non_dep_deductions_claimant_exempt: [true, false] + +- name: A parent non-dependant is banded on their own income, not their young person's + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + parent: + age: 45 + employment_income: 9_360 # 180 a week + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + young_person: + age: 19 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 18 + employment_income: 5_200 # 100 a week + weekly_hours: 8 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + parent_unit: + members: [parent, young_person] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # reg 74(4) and para 8(4) join a couple's income only. The parent's 180 a + # week is under both lowest bands; the young person is a full-time student + # (reg 74(7)(c); para 8(7)(c)). + housing_benefit_non_dep_deduction_exempt: [false, false, true] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: A self-employed loss does not cancel wages for the Universal Credit exemption + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 24 + employment_income: 5_200 + self_employment_income: -5_200 + weekly_hours: 8 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 1_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 57(2) step 3: a self-employed loss is nil, so the wages + # are earned income and neither exemption applies. Under 16 hours: the + # lowest amounts. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml index 31dd42ed94..a8df69a38e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml @@ -780,7 +780,7 @@ input: people: claimant: - age: 70 + age: 90 son: age: 70 total_income: 9000 @@ -804,9 +804,47 @@ council_tax: 2_000 savings: 0 output: + # The claimant is strictly older, so the son is an eligible non-dependant + # and the zero comes from the SPC exemption (para 8(8)(a)), not from the + # son also counting as the applicant. + council_tax_reduction_individual_non_dep_deduction_eligible: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] council_tax_reduction_individual_non_dep_deduction: [0, 0.00] council_tax_reduction_non_dep_deductions: [0.00, 0] +- name: The same non-dependant without State Pension Credit pays the lowest amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 90 + son: + age: 70 + total_income: 9000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + son_unit: + members: [son] + would_claim_uc: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, son] + country: ENGLAND + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deductions: [270.40, 0] + - name: Universal Credit without earned income is exempt period: 2026 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 66fd8dc9d4..4d8112ad06 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -16,14 +16,16 @@ 2. Bounds: an eligible non-dependant who is not exempt pays between the scale's lowest and highest weekly amounts. In English working-age households (local schemes) the national CTR deduction is 0. -3. A non-dependant not in remunerative work (under 16 hours) pays the lowest - amount, whatever their income. +3. A non-dependant not in remunerative work (under 16 hours or, for Housing + Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)) pays the lowest amount, + whatever their income. 4. If the claimant or partner is exempt, the claimant family's deductions are 0. 5. Aggregation: a family's deductions are the sum, over the other families in - the household, of their higher member's amount (both members' amounts for a - Welsh working-age applicant's non-dependant couple with a Universal Credit - award), recomputed here from the individual amounts. + the household, of their claimant and partner's higher amount (both amounts + for a Welsh working-age applicant's non-dependant couple with a Universal + Credit award) plus each other member's, recomputed here from the + individual amounts. 6. Monotonicity (metamorphic): raising any non-dependant's income never lowers an individual deduction or a family's deductions. 7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, @@ -36,7 +38,9 @@ a State Pension Credit recipient, or under 25 and on Income Support, income-based JSA or Universal Credit without earned income. CTR (any age): a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national - scheme, on Universal Credit without earned income. + scheme, on Universal Credit without earned income. Benefit receipt counts + only for the claimant or partner of the award; a self-employed loss counts + as nil earnings (UC reg 57(2)). """ from pathlib import Path @@ -89,6 +93,9 @@ "pension_credit", "universal_credit_pre_benefit_cap", ] +# Family benefits counted in gross income, held at zero so the oracle knows +# each family's gross income. +OTHER_FAMILY_BENEFITS = ["child_tax_credit", "working_tax_credit", "child_benefit"] SYSTEM = CountryTaxBenefitSystem() @@ -119,11 +126,30 @@ def households(draw): ), earnings=draw(st.one_of(st.just(0.0), st.floats(0, 60_000))), other_income=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + self_employment=draw(st.one_of(st.just(0.0), st.floats(-20_000, 20_000))), hours=draw(st.sampled_from([0.0, 10.0, 15.9, 16.0, 35.0, 45.0])), income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + approved_training=False, ) for _ in range(draw(st.integers(1, 2))) ] + # A dependent young person (SSCBA s.142) in the non-dependant family: in + # full-time education (a student) or on approved training (not a student, + # so a separate non-dependant with their own deduction). + if draw(st.booleans()): + training = draw(st.booleans()) + non_dependants.append( + dict( + age=draw(st.integers(16, 19)), + education="NOT_IN_EDUCATION" if training else "UPPER_SECONDARY", + earnings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + other_income=0.0, + self_employment=0.0, + hours=draw(st.sampled_from([0.0, 8.0, 16.0, 35.0])), + income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 10_000))), + approved_training=training, + ) + ) return dict( country=draw(st.sampled_from(sorted(COUNTRIES))), claimant_age=claimant_age, @@ -146,6 +172,9 @@ def build(population, year, raise_income=False): current_education="NOT_IN_EDUCATION", total_income=0.0, employment_income=0.0, + self_employment_income=0.0, + is_in_approved_training=False, + age_started_or_accepted_current_education_or_training=1_000, weekly_hours=0.0, is_blind=flag == "is_blind", **{ @@ -164,6 +193,10 @@ def build(population, year, raise_income=False): current_education=nd["education"], total_income=nd["earnings"] + other, employment_income=nd["earnings"], + self_employment_income=nd["self_employment"], + is_in_approved_training=nd["approved_training"], + # Young people started at 16; adults' start ages don't matter. + age_started_or_accepted_current_education_or_training=16, weekly_hours=nd["hours"], is_blind=False, **{ @@ -177,12 +210,13 @@ def build(population, year, raise_income=False): benunits[f"claimant_unit_{h}"] = dict( members=[claimant], benunit_is_rent_liable=True, - **{benefit: 0.0 for benefit in BENEFITS}, + **{benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS}, ) benunits[f"non_dependant_unit_{h}"] = dict( members=members, benunit_is_rent_liable=False, **household["benefits"], + **{benefit: 0.0 for benefit in OTHER_FAMILY_BENEFITS}, ) households_[f"household_{h}"] = dict( members=[claimant, *members], @@ -202,6 +236,8 @@ def calculate(sim, year): "council_tax_reduction_non_dep_deduction_exempt", "council_tax_reduction_individual_non_dep_deduction_eligible", "council_tax_reduction_individual_non_dep_deduction", + "is_benunit_head", + "is_child_or_qualifying_young_person_for_child_benefit", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -221,14 +257,18 @@ def calculate(sim, year): } -def aggregate(individual, persons, benunit_count, each_member): - """Recompute family deductions from individual amounts (invariant 5).""" - by_benunit = {} +def aggregate(individual, persons, couple, benunit_count, each_member): + """Recompute family deductions from individual amounts (invariant 5): the + claimant and partner's higher amount (both where each_member), plus each + other member's.""" + couple_amounts, others = {}, {} for i, person in enumerate(persons): - by_benunit.setdefault(person["benunit"], []).append(individual[i]) + target = couple_amounts if couple[i] else others + target.setdefault(person["benunit"], []).append(individual[i]) own = np.array( [ - (sum if each_member[b] else max)(by_benunit.get(b, [0.0])) + (sum if each_member[b] else max)(couple_amounts.get(b, [0.0])) + + sum(others.get(b, [])) for b in range(benunit_count) ] ) @@ -265,6 +305,8 @@ def aggregate(individual, persons, benunit_count, each_member): other_income=0.0, hours=35.0, income_rise=0.0, + self_employment=0.0, + approved_training=False, ), dict( age=28, @@ -273,6 +315,8 @@ def aggregate(individual, persons, benunit_count, each_member): other_income=0.0, hours=0.0, income_rise=5_000.0, + self_employment=0.0, + approved_training=False, ), ], ), @@ -290,6 +334,8 @@ def aggregate(individual, persons, benunit_count, each_member): other_income=0.0, hours=0.0, income_rise=0.0, + self_employment=0.0, + approved_training=False, ) ], ) @@ -308,6 +354,11 @@ def test_non_dependant_deduction_invariants(population, year): r["council_tax_reduction_household_has_pensioner"], [1 + len(h["non_dependants"]) for h in population], ) + # Claimant or partner of each person's own family: its head, or a member + # who is not an SSCBA s.142 child or qualifying young person. + couple = r["is_benunit_head"].astype(bool) | ~r[ + "is_child_or_qualifying_young_person_for_child_benefit" + ].astype(bool) for i, person in enumerate(persons): household = population[person["household"]] @@ -322,21 +373,23 @@ def test_non_dependant_deduction_invariants(population, year): if nd is None: continue # 9. The exemptions, read straight from the inputs. - benefits = household["benefits"] + # A family award belongs to its claimant and partner; a self-employed + # loss counts as nil earnings (UC reg 57(2)). + on = {b: bool(couple[i]) and household["benefits"][b] > 0 for b in BENEFITS} student = nd["education"] != "NOT_IN_EDUCATION" - uc_without_earnings = ( - benefits["universal_credit_pre_benefit_cap"] > 0 and nd["earnings"] <= 0 + # Inputs are stored as float32, so a tiny draw can underflow to nil. + earned = ( + max(0.0, float(np.float32(nd["earnings"]))) + + max(0.0, float(np.float32(nd["self_employment"]))) + > 0 ) + uc_without_earnings = on["universal_credit_pre_benefit_cap"] and not earned hb_exempt = ( student - or benefits["pension_credit"] > 0 + or on["pension_credit"] or ( nd["age"] < 25 - and ( - benefits["income_support"] > 0 - or benefits["jsa_income"] > 0 - or uc_without_earnings - ) + and (on["income_support"] or on["jsa_income"] or uc_without_earnings) ) ) assert bool(r["housing_benefit_non_dep_deduction_exempt"][i]) == hb_exempt @@ -344,7 +397,7 @@ def test_non_dependant_deduction_invariants(population, year): ctr_exempt = ( student or any( - benefits[b] > 0 + on[b] for b in [ "income_support", "jsa_income", @@ -363,7 +416,13 @@ def test_non_dependant_deduction_invariants(population, year): and not r["housing_benefit_non_dep_deduction_exempt"][i] ): assert hb_lowest - 0.01 <= hb_deduction <= hb_top + 0.01 - if nd["hours"] < hb.remunerative_work_hours: + # Reg 6(6): on IS, JSA(IB) or ESA(IR) is not remunerative work. + if ( + nd["hours"] < hb.remunerative_work_hours + or on["income_support"] + or on["jsa_income"] + or on["esa_income"] + ): assert hb_deduction == pytest.approx(hb_lowest, abs=0.01) # 2-3. Council Tax Reduction bounds and the not-in-work amount. country = household["country"] @@ -387,6 +446,7 @@ def test_non_dependant_deduction_invariants(population, year): expected_hb = aggregate( r["household_benefits_individual_non_dep_deduction"], persons, + couple, benunit_count, [False] * benunit_count, ) @@ -406,6 +466,7 @@ def test_non_dependant_deduction_invariants(population, year): expected_ctr = aggregate( r["council_tax_reduction_individual_non_dep_deduction"], persons, + couple, benunit_count, welsh_working_age, ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py new file mode 100644 index 0000000000..5a0aebbc14 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -0,0 +1,74 @@ +"""Shared definitions for Housing Benefit and Council Tax Reduction +non-dependant deductions.""" + +from policyengine_uk.model_api import * + +# Family benefits counted in a couple's gross income. Universal Credit is taken +# before the benefit cap: the capped award depends on Housing Benefit. +FAMILY_GROSS_INCOME_BENEFITS = [ + "universal_credit_pre_benefit_cap", + "child_tax_credit", + "working_tax_credit", + "child_benefit", +] + + +def is_claimant_or_partner(person, period): + """The claimant or partner of a benefit unit: its head, or any member who + is not a child or qualifying young person (SSCBA 1992 s.142), which is how + the Housing Benefit Regulations 2006 (regs 2(1) and 19) and the English and + Welsh Council Tax Reduction regulations (reg 2(1)) define the members of a + family other than the claimant and partner.""" + return person("is_benunit_head", period) | ~person( + "is_child_or_qualifying_young_person_for_child_benefit", period + ) + + +def non_dependant_weekly_gross_income(person, period): + """Normal weekly gross income for the deduction bands. A claimant or + partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 + para 8(4)): each member's taxable income plus the family's Universal + Credit, tax credits and child benefit. Anyone else in the benefit unit is a + separate non-dependant, banded on their own taxable income. Taxable income + excludes the disregarded disability benefits (HB reg 74(9)).""" + claimant_or_partner = is_claimant_or_partner(person, period) + own_income = max_(0, person("total_income", period)) + family_benefits = sum( + person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS + ) + couple_income = ( + person.benunit.sum(own_income * claimant_or_partner) + family_benefits + ) + annual = where(claimant_or_partner, couple_income, own_income) + return annual / WEEKS_IN_YEAR + + +def has_earned_income(person, period): + """Whether the person has earned income (UC Regs 2013 reg 52), with + self-employed losses taken as nil (reg 57(2), step 3).""" + return ( + max_(0, person("employment_income", period)) + + max_(0, person("self_employment_income", period)) + ) > 0 + + +def deduction_per_family(benunit, period, deductions, both_members_of_couple): + """One deduction for the claimant and partner, the higher of theirs (HB reg + 74(3); CTR Sch 1 para 8(3)), or both where both_members_of_couple; plus a + separate deduction for each other member.""" + claimant_or_partner = is_claimant_or_partner(benunit.members, period) + couple = where( + both_members_of_couple, + benunit.sum(deductions * claimant_or_partner), + benunit.max(deductions * claimant_or_partner), + ) + return couple + benunit.sum(deductions * ~claimant_or_partner) + + +def charged_to_other_families(benunit, period, deduction_for_benunit): + """Deductions for the non-dependant families in the household other than + this one: each family's amount counted once (via its head).""" + is_benunit_head = benunit.members("is_benunit_head", period) + counted = is_benunit_head * benunit.project(deduction_for_benunit) + in_household = benunit.max(benunit.members.household.sum(counted)) + return in_household - deduction_for_benunit diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index 32dc5d9662..87c0c8d6c4 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + is_claimant_or_partner, + non_dependant_weekly_gross_income, +) class household_benefits_individual_non_dep_deduction(Variable): @@ -7,10 +11,11 @@ class household_benefits_individual_non_dep_deduction(Variable): label = "Housing Benefit individual non-dependent deduction" documentation = ( "A non-dependant in remunerative work pays the deduction for the band " - "containing their normal gross weekly income, counting a couple's " - "joint income; bands include their lower edge. A non-dependant not in " - "remunerative work pays the lowest amount. Gross income is taxable " - "total income, so the disregarded disability benefits are excluded." + "containing their normal gross weekly income (a couple's joint income " + "for a claimant or partner of the non-dependant's family); bands " + "include their lower edge. A non-dependant not in remunerative work, " + "including one on Income Support, income-based JSA or income-related " + "ESA (reg 6(6)), pays the lowest amount." ) definition_period = YEAR unit = GBP @@ -18,16 +23,20 @@ class household_benefits_individual_non_dep_deduction(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/74", "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/6", ) def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction - income = max_(0, person("total_income", period)) - counted = person("age", period) >= p.age_threshold - weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + weekly_income = non_dependant_weekly_gross_income(person, period) + on_income_related_benefit = is_claimant_or_partner(person, period) & ( + (person.benunit("income_support", period) > 0) + | (person.benunit("jsa_income", period) > 0) + | (person.benunit("esa_income", period) > 0) + ) in_remunerative_work = ( person("weekly_hours", period) >= p.remunerative_work_hours - ) + ) & ~on_income_related_benefit banded = p.amount.calc(weekly_income) not_in_remunerative_work = p.amount.calc(np.zeros_like(weekly_income)) weekly = where(in_remunerative_work, banded, not_in_remunerative_work) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 1c998c6694..1002656b47 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + has_earned_income, + is_claimant_or_partner, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, ) @@ -28,20 +32,22 @@ class housing_benefit_non_dep_deduction_exempt(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction full_time_student = is_full_time_student_non_dep(person, period) - on_pension_credit = person.benunit("pension_credit", period) > 0 - on_legacy_income_related_benefit = ( - person.benunit("income_support", period) > 0 - ) | (person.benunit("jsa_income", period) > 0) + # Receipt of a family award belongs to its claimant and partner. + claimant_or_partner = is_claimant_or_partner(person, period) + on_pension_credit = claimant_or_partner & ( + person.benunit("pension_credit", period) > 0 + ) + on_legacy_income_related_benefit = claimant_or_partner & ( + (person.benunit("income_support", period) > 0) + | (person.benunit("jsa_income", period) > 0) + ) # Entitlement before the benefit cap: universal_credit itself depends on # Housing Benefit through the cap. - entitled_to_universal_credit = ( + entitled_to_universal_credit = claimant_or_partner & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) - earned_income = add( - person, period, ["employment_income", "self_employment_income"] - ) - universal_credit_without_earned_income = entitled_to_universal_credit & ( - earned_income <= 0 + universal_credit_without_earned_income = ( + entitled_to_universal_credit & ~has_earned_income(person, period) ) under_age_limit = person("age", period) < p.income_related_benefit_age_limit return ( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 86eb32437a..30b29f0786 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + charged_to_other_families, + deduction_per_family, +) class housing_benefit_non_dep_deductions(Variable): @@ -7,8 +11,9 @@ class housing_benefit_non_dep_deductions(Variable): label = "non-dependent deductions" documentation = ( "Deductions for the non-dependants in other benefit units of the " - "household: one per couple, the higher of the two members' amounts, " - "and none if the claimant or partner is exempt." + "household: one for each couple, the higher of the two members' " + "amounts, and one for each other member; none if the claimant or " + "partner is exempt." ) definition_period = YEAR unit = GBP @@ -21,13 +26,12 @@ def formula(benunit, period, parameters): deductions = benunit.members( "household_benefits_individual_non_dep_deduction", period ) - deduction_for_benunit = benunit.max(deductions) - is_benunit_head = benunit.members("is_benunit_head", period) - counted = is_benunit_head * benunit.project(deduction_for_benunit) - deductions_in_household = benunit.max(benunit.members.household.sum(counted)) + deduction_for_benunit = deduction_per_family(benunit, period, deductions, False) claimant_exempt = benunit( "housing_benefit_non_dep_deductions_claimant_exempt", period ) return where( - claimant_exempt, 0, deductions_in_household - deduction_for_benunit + claimant_exempt, + 0, + charged_to_other_families(benunit, period, deduction_for_benunit), ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 4568e9f714..0ddaef9cc1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + is_claimant_or_partner, +) class housing_benefit_non_dep_deductions_claimant_exempt(Variable): @@ -21,8 +24,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): def formula(benunit, period, parameters): person = benunit.members - # Claimant or partner as in council_tax_reduction_household_has_non_dep_exemption. - claimant_or_partner = person("is_adult", period) + claimant_or_partner = is_claimant_or_partner(person, period) qualifying = ( person("is_blind", period) | (person("attendance_allowance", period) > 0) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py index fea66dd381..fb8f466080 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + non_dependant_weekly_gross_income, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_england_pensioner_scheme, is_scotland_scheme, @@ -13,12 +16,12 @@ class council_tax_reduction_individual_non_dep_deduction(Variable): documentation = ( "The England pensioner, Scottish and Welsh schemes' own weekly scales. " "A non-dependant in remunerative work pays the deduction for the band " - "containing their normal gross weekly income, counting a couple's " - "joint income; bands include their lower edge. A non-dependant not in " + "containing their normal gross weekly income (a couple's joint income " + "for a claimant or partner of the non-dependant's family); bands " + "include their lower edge. A non-dependant not in " "remunerative work pays the lowest amount; an exempt non-dependant " - "pays nothing. Gross income is taxable total income, so the " - "disregarded disability benefits are excluded. English working-age " - "local schemes have their own variables." + "pays nothing. English working-age local schemes have their own " + "variables." ) definition_period = YEAR unit = GBP @@ -47,9 +50,7 @@ def formula(person, period, parameters): is_scotland_scheme(country), is_wales_scheme(country), ] - income = max_(0, person("total_income", period)) - counted = person("age", period) >= 18 - weekly_income = person.benunit.sum(income * counted) / WEEKS_IN_YEAR + weekly_income = non_dependant_weekly_gross_income(person, period) no_income = np.zeros_like(weekly_income) remunerative_work_hours = select( schemes, diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 004237bc6a..066c8c7d26 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + has_earned_income, + is_claimant_or_partner, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, ) @@ -49,7 +53,9 @@ def formula(person, period, parameters): is_wales_scheme(country), ] full_time_student = is_full_time_student_non_dep(person, period) - on_income_related_benefit = ( + # Receipt of a family award belongs to its claimant and partner. + claimant_or_partner = is_claimant_or_partner(person, period) + on_income_related_benefit = claimant_or_partner & ( (person.benunit("income_support", period) > 0) | (person.benunit("jsa_income", period) > 0) | (person.benunit("esa_income", period) > 0) @@ -64,14 +70,13 @@ def formula(person, period, parameters): ], default=False, ) - entitled_to_universal_credit = ( + entitled_to_universal_credit = claimant_or_partner & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) - earned_income = add( - person, period, ["employment_income", "self_employment_income"] - ) universal_credit_without_earned_income = ( - universal_credit_limb & entitled_to_universal_credit & (earned_income <= 0) + universal_credit_limb + & entitled_to_universal_credit + & ~has_earned_income(person, period) ) return ( full_time_student diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index 4384831a88..7d0c3f9484 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -1,4 +1,8 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + charged_to_other_families, + deduction_per_family, +) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( is_wales_scheme, ) @@ -12,8 +16,8 @@ class council_tax_reduction_non_dep_deductions(Variable): "Deductions for the non-dependants in other benefit units of the " "household: one per couple, the higher of the two members' amounts " "(both, in the Welsh scheme for people who are not pensioners, when " - "the couple has a Universal Credit award), and none if the applicant " - "or partner is exempt." + "the couple has a Universal Credit award), one for each other member, " + "and none if the applicant or partner is exempt." ) definition_period = YEAR unit = GBP @@ -39,17 +43,14 @@ def formula(benunit, period, parameters): & ~has_pensioner & (benunit("universal_credit_pre_benefit_cap", period) > 0) ) - deduction_for_benunit = where( - each_member_deducted, - benunit.sum(deductions), - benunit.max(deductions), + deduction_for_benunit = deduction_per_family( + benunit, period, deductions, each_member_deducted ) - is_benunit_head = benunit.members("is_benunit_head", period) - counted = is_benunit_head * benunit.project(deduction_for_benunit) - deductions_in_household = benunit.max(benunit.members.household.sum(counted)) applicant_exempt = benunit.household( "council_tax_reduction_household_has_non_dep_exemption", period ) return where( - applicant_exempt, 0, deductions_in_household - deduction_for_benunit + applicant_exempt, + 0, + charged_to_other_families(benunit, period, deduction_for_benunit), ) From 12ac5f605d062053a95d7db8d0551c70f90d7704 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 05:36:02 -0400 Subject: [PATCH 14/67] Address review round 2: UC earned income, award payee, capped UC, LGFA disregard - "No earned income" follows UC Regs 2013 regs 52-62: employment income plus SSP and SMP less relievable pension contributions (reg 55(4)-(5)), self-employed earnings with losses nil and the minimum income floor where the UC model applies it (regs 57(2), 62), and other paid work. - IS, JSA(IB), ESA(IR) and SPC count only for the person they are payable to (HB Regs 2006 reg 2(3)-(3A)): the member reporting the award, or the head; used for the exemptions and HB reg 6(6). UC still counts for both joint claimants. - Gross income uses UC after the benefit cap (UC reg 81), applying the cap to the capped benefits other than HB, which a non-dependant family does not receive. - CTR exempts an adult for whom another person is entitled to child benefit (LGFA 1992 Sch 1 para 3, via para 8(8)(b) and equivalents). - Tests: the review's counterexamples, and property oracles for the payee and the child benefit disregard. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 260 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 29 +- .../non_dep_deduction/_non_dependants.py | 75 ++++- ...d_benefits_individual_non_dep_deduction.py | 10 +- ...ousing_benefit_non_dep_deduction_exempt.py | 15 +- ..._tax_reduction_non_dep_deduction_exempt.py | 36 ++- 6 files changed, 386 insertions(+), 39 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index e91df019b0..de9ebc33ad 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -165,3 +165,263 @@ council_tax_reduction_non_dep_deduction_exempt: [false, false] housing_benefit_non_dep_deductions: [1_060.80, 0] council_tax_reduction_non_dep_deductions: [270.40, 0] + +# Round 2 of the independent review. + +- name: Statutory maternity pay is earned income for the Universal Credit exemption + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 22 + statutory_maternity_pay: 8000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 2000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 55(4)(b): SMP is employed earnings, so the award is not + # calculated on no earned income. Not in remunerative work: the lowest amounts. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Wages fully paid into a pension leave no earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 24 + employment_income: 6240 + employee_pension_contributions: 6240 + weekly_hours: 8 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 55(5)(a): relievable pension contributions are deducted + # from employed earnings, leaving none (HB reg 74(8)(b); CTR para 8(8)(c)). + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: The minimum income floor counts as earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 24 + self_employment_income: 0 + uc_mif_applies: true + uc_minimum_income_floor: 20000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 1000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 62(2): a person to whom the floor applies is treated as + # having earned income equal to it. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Income-related ESA is payable to the recipient, not the working partner + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + recipient: + age: 45 + esa_income_reported: 2600 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + partner: + age: 40 + employment_income: 10920 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 2600 + pension_credit: 0 + would_claim_uc: false + households: + household: + members: [claimant, recipient, partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB Regs 2006 reg 2(3A): only the recipient is on ESA(IR), so only they are + # out of remunerative work (reg 6(6)) and exempt from CTR (para 8(8)(a)). The + # partner's 210 a week is in the HB 192 band (46.85) and under the CTR 279 band + # (5.20); one deduction for the couple, the higher. + housing_benefit_non_dep_deductions: [2_436.20, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Gross income counts Universal Credit after the benefit cap + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + non_dependant: + age: 40 + employment_income: 52 # 1 a week, so not exempt as UC without earnings + total_income: 52 + weekly_hours: 35 + current_education: NOT_IN_EDUCATION + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 30000 + benefit_cap: 20000 + households: + household: + members: [claimant, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 81: the cap reduces the award to 20,000; with 1 a week of + # wages, 385.62 a week, + # in the HB 365 band (105.20) and the CTR 279 band (10.60). Before the cap it + # would be 576.92 a week. + housing_benefit_non_dep_deductions: [5_470.40, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A young person for whom child benefit is paid is disregarded for CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + parent: + age: 45 + employment_income: 10400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + young_person: + age: 19 + current_education: NOT_IN_EDUCATION + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 16 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [parent, young_person] + benunit_is_rent_liable: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 1406.6 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + would_claim_uc: false + households: + household: + members: [claimant, parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # CTR para 8(8)(b) and LGFA 1992 Sch 1 para 3: the 19-year-old on approved + # training is disregarded, so only the parent is deducted: 227.05 a week with + # the child benefit, under 279 (5.20). HB has no such exemption: the parent + # (192 band, 46.85) plus the young person, not in remunerative work (20.40). + council_tax_reduction_non_dep_deduction_exempt: [false, false, true] + council_tax_reduction_non_dep_deductions: [270.40, 0] + housing_benefit_non_dep_deduction_exempt: [false, false, false] + housing_benefit_non_dep_deductions: [3_497.00, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 4d8112ad06..8ed4a8baa9 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -38,9 +38,11 @@ a State Pension Credit recipient, or under 25 and on Income Support, income-based JSA or Universal Credit without earned income. CTR (any age): a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national - scheme, on Universal Credit without earned income. Benefit receipt counts - only for the claimant or partner of the award; a self-employed loss counts - as nil earnings (UC reg 57(2)). + scheme, on Universal Credit without earned income, or an adult for whom + someone else is entitled to child benefit (LGFA 1992 Sch 1 para 3). IS, + JSA(IB), ESA(IR) and SPC count only for the person they are payable to; + Universal Credit for both joint claimants; a self-employed loss counts as + nil earnings (UC reg 57(2)). """ from pathlib import Path @@ -238,6 +240,7 @@ def calculate(sim, year): "council_tax_reduction_individual_non_dep_deduction", "is_benunit_head", "is_child_or_qualifying_young_person_for_child_benefit", + "is_qualifying_young_person_for_child_benefit", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -373,9 +376,16 @@ def test_non_dependant_deduction_invariants(population, year): if nd is None: continue # 9. The exemptions, read straight from the inputs. - # A family award belongs to its claimant and partner; a self-employed - # loss counts as nil earnings (UC reg 57(2)). - on = {b: bool(couple[i]) and household["benefits"][b] > 0 for b in BENEFITS} + # IS, JSA(IB), ESA(IR) and SPC count for their payee: here the benefit + # unit's head, as no member reports them (HB reg 2(3)-(3A)). Universal + # Credit counts for both joint claimants. A self-employed loss counts as + # nil earnings (UC reg 57(2)). + payee = bool(r["is_benunit_head"][i]) + on = { + b: (bool(couple[i]) if b == "universal_credit_pre_benefit_cap" else payee) + and household["benefits"][b] > 0 + for b in BENEFITS + } student = nd["education"] != "NOT_IN_EDUCATION" # Inputs are stored as float32, so a tiny draw can underflow to nil. earned = ( @@ -406,6 +416,13 @@ def test_non_dependant_deduction_invariants(population, year): ] ) or (national and uc_without_earnings) + # LGFA 1992 Sch 1 para 3: an adult qualifying young person in + # someone else's family. + or ( + nd["age"] >= 18 + and bool(r["is_qualifying_young_person_for_child_benefit"][i]) + and not couple[i] + ) ) assert ( bool(r["council_tax_reduction_non_dep_deduction_exempt"][i]) == ctr_exempt diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 5a0aebbc14..47d1b762b9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -3,15 +3,27 @@ from policyengine_uk.model_api import * -# Family benefits counted in a couple's gross income. Universal Credit is taken -# before the benefit cap: the capped award depends on Housing Benefit. +# Family benefits counted in a couple's gross income, besides Universal Credit. FAMILY_GROSS_INCOME_BENEFITS = [ - "universal_credit_pre_benefit_cap", "child_tax_credit", "working_tax_credit", "child_benefit", ] +# Benefits the benefit cap counts, other than Housing Benefit (benefit_cap_reduction). +CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ + "child_benefit", + "child_tax_credit", + "jsa_income", + "income_support", + "esa_income", + "universal_credit_pre_benefit_cap", + "jsa_contrib", + "incapacity_benefit", + "esa_contrib", + "sda", +] + def is_claimant_or_partner(person, period): """The claimant or partner of a benefit unit: its head, or any member who @@ -33,7 +45,7 @@ def non_dependant_weekly_gross_income(person, period): excludes the disregarded disability benefits (HB reg 74(9)).""" claimant_or_partner = is_claimant_or_partner(person, period) own_income = max_(0, person("total_income", period)) - family_benefits = sum( + family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS ) couple_income = ( @@ -43,13 +55,56 @@ def non_dependant_weekly_gross_income(person, period): return annual / WEEKS_IN_YEAR +def universal_credit_after_benefit_cap(benunit, period): + """A family's Universal Credit award after the benefit cap (UC Regs 2013 + reg 81), for a family that is not liable for rent. universal_credit itself + depends on Housing Benefit through the cap, so the cap is applied here to + the capped benefits other than Housing Benefit, which such a family does + not receive.""" + capped = sum( + benunit(benefit, period) for benefit in CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT + ) + reduction = max_(0, capped - benunit("benefit_cap", period)) + return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) + + def has_earned_income(person, period): - """Whether the person has earned income (UC Regs 2013 reg 52), with - self-employed losses taken as nil (reg 57(2), step 3).""" - return ( - max_(0, person("employment_income", period)) - + max_(0, person("self_employment_income", period)) - ) > 0 + """Whether the person has earned income (UC Regs 2013 reg 52): employed + earnings, including statutory sick and maternity pay and less relievable + pension contributions (reg 55(4)-(5)); self-employed earnings, a loss + counting as nil (reg 57(2)) and the minimum income floor applying where + the Universal Credit model applies it (reg 62); and other paid work.""" + employed = max_( + 0, + add( + person, + period, + ["employment_income", "statutory_sick_pay", "statutory_maternity_pay"], + ) + - person("pension_contributions", period), + ) + self_employed = max_(0, person("self_employment_income", period)) + self_employed = where( + person("uc_mif_applies", period), + max_(self_employed, person("uc_minimum_income_floor", period)), + self_employed, + ) + other = max_(0, person("miscellaneous_income", period)) + return (employed + self_employed + other) > 0 + + +def is_award_payee(person, period, award, reported): + """Whether a family award is payable to this person: Housing Benefit's + test for being "on" income-based JSA or income-related ESA (HB Regs 2006 + reg 2(3) and (3A)), applied also to Income Support and State Pension + Credit, which are paid to the claimant. The payee is the member who + reports the award, or the benefit unit's head where none does.""" + has_award = person.benunit(award, period) > 0 + reports = person(reported, period) > 0 + payee = where( + person.benunit.any(reports), reports, person("is_benunit_head", period) + ) + return has_award & payee def deduction_per_family(benunit, period, deductions, both_members_of_couple): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index 87c0c8d6c4..bd7bc84f3f 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -1,6 +1,6 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - is_claimant_or_partner, + is_award_payee, non_dependant_weekly_gross_income, ) @@ -29,10 +29,10 @@ class household_benefits_individual_non_dep_deduction(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction weekly_income = non_dependant_weekly_gross_income(person, period) - on_income_related_benefit = is_claimant_or_partner(person, period) & ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) - | (person.benunit("esa_income", period) > 0) + on_income_related_benefit = ( + is_award_payee(person, period, "income_support", "income_support_reported") + | is_award_payee(person, period, "jsa_income", "jsa_income_reported") + | is_award_payee(person, period, "esa_income", "esa_income_reported") ) in_remunerative_work = ( person("weekly_hours", period) >= p.remunerative_work_hours diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 1002656b47..d26c291744 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, + is_award_payee, is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( @@ -32,15 +33,15 @@ class housing_benefit_non_dep_deduction_exempt(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction full_time_student = is_full_time_student_non_dep(person, period) - # Receipt of a family award belongs to its claimant and partner. + # IS, income-based JSA and SPC count for the person they are payable + # to; Universal Credit for both joint claimants. claimant_or_partner = is_claimant_or_partner(person, period) - on_pension_credit = claimant_or_partner & ( - person.benunit("pension_credit", period) > 0 - ) - on_legacy_income_related_benefit = claimant_or_partner & ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) + on_pension_credit = is_award_payee( + person, period, "pension_credit", "pension_credit_reported" ) + on_legacy_income_related_benefit = is_award_payee( + person, period, "income_support", "income_support_reported" + ) | is_award_payee(person, period, "jsa_income", "jsa_income_reported") # Entitlement before the benefit cap: universal_credit itself depends on # Housing Benefit through the cap. entitled_to_universal_credit = claimant_or_partner & ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 066c8c7d26..4bf4ceebce 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -1,6 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, + is_award_payee, is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( @@ -21,12 +22,13 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "Modelled exemptions in the England pensioner, Scottish and Welsh " "schemes: a full-time student; a non-dependant on Income Support, " "income-based JSA, income-related ESA or State Pension Credit (any " - "age); and, from the year each scheme added it, one entitled to " - "Universal Credit calculated on no earned income. Wales excludes " + "age); an adult for whom someone else is entitled to child benefit " + "(LGFA 1992 Sch 1 para 3); and, from the year each scheme added it, " + "one entitled to Universal Credit calculated on no earned income. Wales excludes " "members of the ESA work-related activity group; with no ESA group " - "input, income-related ESA is treated as exempt. Not modelled: persons " - "disregarded for council tax discounts (LGFA 1992 Sch 1) other than " - "students, youth training allowances, a normal home elsewhere, and " + "input, income-related ESA is treated as exempt. Not modelled: other " + "persons disregarded for council tax discounts (LGFA 1992 Sch 1), " + "youth training allowances, a normal home elsewhere, and " "absence in hospital or on armed forces operations." ) definition_period = YEAR @@ -53,13 +55,24 @@ def formula(person, period, parameters): is_wales_scheme(country), ] full_time_student = is_full_time_student_non_dep(person, period) - # Receipt of a family award belongs to its claimant and partner. + # IS, income-based JSA, income-related ESA and SPC count for the person + # they are payable to; Universal Credit for both joint claimants. claimant_or_partner = is_claimant_or_partner(person, period) - on_income_related_benefit = claimant_or_partner & ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) - | (person.benunit("esa_income", period) > 0) - | (person.benunit("pension_credit", period) > 0) + on_income_related_benefit = ( + is_award_payee(person, period, "income_support", "income_support_reported") + | is_award_payee(person, period, "jsa_income", "jsa_income_reported") + | is_award_payee(person, period, "esa_income", "esa_income_reported") + | is_award_payee( + person, period, "pension_credit", "pension_credit_reported" + ) + ) + # LGFA 1992 Sch 1 para 3 (via para 8(8)(b) and equivalents): an adult + # for whom another person is entitled to child benefit, that is a + # qualifying young person in someone else's family. + disregarded_for_child_benefit = ( + (person("age", period) >= 18) + & person("is_qualifying_young_person_for_child_benefit", period) + & ~claimant_or_partner ) universal_credit_limb = select( schemes, @@ -81,5 +94,6 @@ def formula(person, period, parameters): return ( full_time_student | on_income_related_benefit + | disregarded_for_child_benefit | universal_credit_without_earned_income ) From 9ebd8f691803f761f9159cb5a324edc65bd09c53 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 05:48:44 -0400 Subject: [PATCH 15/67] Use is_claimant_or_partner for the couple in non-dependant deductions Replaces the local claimant-or-partner helper (benefit unit head, or anyone not a child benefit child or qualifying young person) with the is_claimant_or_partner variable: the SSCBA s.137 couple, at most two people. A partner at school is now a partner, and a third adult in a benefit unit is deducted separately. Adds the round-2 review cases: a blind 16-year-old partner (Wales), a non-dependant couple with a partner at school, and a 17-year-old partner's PIP in Merton. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 143 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 9 +- .../non_dep_deduction/_non_dependants.py | 15 +- ...ousing_benefit_non_dep_deduction_exempt.py | 3 +- ...efit_non_dep_deductions_claimant_exempt.py | 5 +- ..._tax_reduction_non_dep_deduction_exempt.py | 3 +- 6 files changed, 151 insertions(+), 27 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index de9ebc33ad..0ee6a4fa4d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -425,3 +425,146 @@ council_tax_reduction_non_dep_deductions: [270.40, 0] housing_benefit_non_dep_deduction_exempt: [false, false, false] housing_benefit_non_dep_deductions: [3_497.00, 0] + +# Round 3: partners are the SSCBA s.137 couple (is_claimant_or_partner), not +# whoever is not a child benefit child or qualifying young person. + +- name: A blind 16-year-old partner exempts a Welsh 19-year-old applicant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 19 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 16 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + is_blind: true + is_claimant_or_partner: true + lodger: + age: 18 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant, partner] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, partner, lodger] + country: WALES + local_authority: CARDIFF + output: + # HB reg 74(6)(a) and WSI 2013/3029 Sch 6 para 5(6)(a): the claimant's + # partner is blind, so no deduction for the lodger (576.92 a week, which + # would otherwise be 119.85 HB and 16.80 CTR a week). + housing_benefit_non_dep_deductions_claimant_exempt: [true, false] + council_tax_reduction_household_has_non_dep_exemption: true + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: A non-dependant couple with a partner at school is banded on joint income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + older_partner: + age: 20 + employment_income: 9_360 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + younger_partner: + age: 18 + employment_income: 8_320 + weekly_hours: 10 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + couple_unit: + members: [older_partner, younger_partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, older_partner, younger_partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB reg 74(3)-(4) and CTR Sch 1 para 8(3)-(4): one deduction for the + # couple, banded on their joint 340 a week because the older partner is in + # remunerative work: 64.35 HB (269 band) and 10.60 CTR (279 band). + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A 17-year-old partner's PIP exempts a Merton applicant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 17 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + pip_dl_category: STANDARD + is_claimant_or_partner: true + lodger: + age: 40 + employment_income: 30_000 + weekly_hours: 37.5 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant, partner] + would_claim_uc: false + claims_all_entitled_benefits: true + lodger_unit: + members: [lodger] + would_claim_uc: false + households: + household: + members: [applicant, partner, lodger] + country: ENGLAND + local_authority: MERTON + council_tax: 1_800 + savings: 0 + output: + # Merton scheme para 30(6): no deduction where the applicant or partner + # gets the daily living component of PIP. + council_tax_reduction_household_has_non_dep_exemption: true + merton_council_tax_reduction_non_dep_deductions: [0, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 8ed4a8baa9..99af0cb70a 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -239,7 +239,7 @@ def calculate(sim, year): "council_tax_reduction_individual_non_dep_deduction_eligible", "council_tax_reduction_individual_non_dep_deduction", "is_benunit_head", - "is_child_or_qualifying_young_person_for_child_benefit", + "is_claimant_or_partner", "is_qualifying_young_person_for_child_benefit", ], benunit=[ @@ -357,11 +357,8 @@ def test_non_dependant_deduction_invariants(population, year): r["council_tax_reduction_household_has_pensioner"], [1 + len(h["non_dependants"]) for h in population], ) - # Claimant or partner of each person's own family: its head, or a member - # who is not an SSCBA s.142 child or qualifying young person. - couple = r["is_benunit_head"].astype(bool) | ~r[ - "is_child_or_qualifying_young_person_for_child_benefit" - ].astype(bool) + # Claimant or partner of each person's own family (SSCBA s.137 couple). + couple = r["is_claimant_or_partner"].astype(bool) for i, person in enumerate(persons): household = population[person["household"]] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 47d1b762b9..4167ce5d46 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -25,17 +25,6 @@ ] -def is_claimant_or_partner(person, period): - """The claimant or partner of a benefit unit: its head, or any member who - is not a child or qualifying young person (SSCBA 1992 s.142), which is how - the Housing Benefit Regulations 2006 (regs 2(1) and 19) and the English and - Welsh Council Tax Reduction regulations (reg 2(1)) define the members of a - family other than the claimant and partner.""" - return person("is_benunit_head", period) | ~person( - "is_child_or_qualifying_young_person_for_child_benefit", period - ) - - def non_dependant_weekly_gross_income(person, period): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 @@ -43,7 +32,7 @@ def non_dependant_weekly_gross_income(person, period): Credit, tax credits and child benefit. Anyone else in the benefit unit is a separate non-dependant, banded on their own taxable income. Taxable income excludes the disregarded disability benefits (HB reg 74(9)).""" - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) own_income = max_(0, person("total_income", period)) family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS @@ -111,7 +100,7 @@ def deduction_per_family(benunit, period, deductions, both_members_of_couple): """One deduction for the claimant and partner, the higher of theirs (HB reg 74(3); CTR Sch 1 para 8(3)), or both where both_members_of_couple; plus a separate deduction for each other member.""" - claimant_or_partner = is_claimant_or_partner(benunit.members, period) + claimant_or_partner = benunit.members("is_claimant_or_partner", period) couple = where( both_members_of_couple, benunit.sum(deductions * claimant_or_partner), diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index d26c291744..38215bd6c9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -2,7 +2,6 @@ from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, is_award_payee, - is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, @@ -35,7 +34,7 @@ def formula(person, period, parameters): full_time_student = is_full_time_student_non_dep(person, period) # IS, income-based JSA and SPC count for the person they are payable # to; Universal Credit for both joint claimants. - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) on_pension_credit = is_award_payee( person, period, "pension_credit", "pension_credit_reported" ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 0ddaef9cc1..53535fe798 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -1,7 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - is_claimant_or_partner, -) class housing_benefit_non_dep_deductions_claimant_exempt(Variable): @@ -24,7 +21,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): def formula(benunit, period, parameters): person = benunit.members - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) qualifying = ( person("is_blind", period) | (person("attendance_allowance", period) > 0) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 4bf4ceebce..0b1721e365 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -2,7 +2,6 @@ from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( has_earned_income, is_award_payee, - is_claimant_or_partner, ) from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._legacy import ( is_full_time_student_non_dep, @@ -57,7 +56,7 @@ def formula(person, period, parameters): full_time_student = is_full_time_student_non_dep(person, period) # IS, income-based JSA, income-related ESA and SPC count for the person # they are payable to; Universal Credit for both joint claimants. - claimant_or_partner = is_claimant_or_partner(person, period) + claimant_or_partner = person("is_claimant_or_partner", period) on_income_related_benefit = ( is_award_payee(person, period, "income_support", "income_support_reported") | is_award_payee(person, period, "jsa_income", "jsa_income_reported") From 75604f5f5555255186a522b9ddb52f615de83b09 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:37:40 -0400 Subject: [PATCH 16/67] Describe the claimant-or-partner source and the joint-claim JSA gap The changelog and the property generator's comment now name is_claimant_or_partner instead of the SSCBA s.142 definitions, and is_award_payee says that both members of a joint-claim jobseeker's allowance couple are on it (HB reg 2(3)(c)), which the model cannot identify. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- .../tests/test_non_dependant_deduction_properties.py | 7 ++++--- .../housing_benefit/non_dep_deduction/_non_dependants.py | 4 +++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index dbfa0aa46e..24d66cd535 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner identified by the SSCBA s.142 child and young person definitions; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 99af0cb70a..8741b3e64c 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -135,9 +135,10 @@ def households(draw): ) for _ in range(draw(st.integers(1, 2))) ] - # A dependent young person (SSCBA s.142) in the non-dependant family: in - # full-time education (a student) or on approved training (not a student, - # so a separate non-dependant with their own deduction). + # A young person aged 16-19 in the non-dependant family: in full-time + # education (a student) or on approved training (not a student). Whether + # they are the family's claimant or partner is read from the model's + # is_claimant_or_partner. if draw(st.booleans()): training = draw(st.booleans()) non_dependants.append( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 4167ce5d46..58d69dd498 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -87,7 +87,9 @@ def is_award_payee(person, period, award, reported): test for being "on" income-based JSA or income-related ESA (HB Regs 2006 reg 2(3) and (3A)), applied also to Income Support and State Pension Credit, which are paid to the claimant. The payee is the member who - reports the award, or the benefit unit's head where none does.""" + reports the award, or the benefit unit's head where none does. Both + members of a joint-claim jobseeker's allowance couple are on it (reg + 2(3)(c)), but the model has no joint-claim input, so only the payee is.""" has_award = person.benunit(award, period) > 0 reports = person(reported, period) > 0 payee = where( From 084e1f9d68f1e3c8af904d7bb910db5a9cbff66e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 08:15:38 -0400 Subject: [PATCH 17/67] Address review round 3: gross and earned income, Scottish partners, cap childcare, child benefit claims - Gross income adds statutory sick, maternity and paternity pay (earnings, HB reg 35(1)(i)) and Maternity Allowance to each person's taxable income, and the family's IS, JSA(IB), ESA(IR) and Pension Credit to a couple's. - Earned income for the UC limb adds statutory paternity pay (UC reg 55(4)(c)), and relievable pension contributions come off self-employed earnings when there are no employed earnings (reg 57(2), step 4). - Scotland's working-age scheme exempts a non-dependant whose partner is on IS, JSA(IB) or ESA(IR) and who is not on Universal Credit, from April 2022 (SSI 2021/249 reg 90(8)(a), reg 4(1)). - The after-cap UC in gross income keeps the cap's childcare protection (UC reg 81(1)-(2)). - The LGFA 1992 Sch 1 para 3 disregard needs a child benefit claim. - The property oracle derives the claimant and applicant exemptions from the generated inputs, and the generator varies statutory pay, pension contributions, the start-up period, the award's reporter and child benefit claims. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- ...er_of_income_related_benefit_claimant.yaml | 17 + .../non_dependant_deduction_review_cases.yaml | 448 +++++++++++++++++- ...test_non_dependant_deduction_properties.py | 164 +++++-- .../non_dep_deduction/_non_dependants.py | 75 ++- ..._tax_reduction_non_dep_deduction_exempt.py | 55 ++- 6 files changed, 687 insertions(+), 74 deletions(-) create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 24d66cd535..3ad3195c5f 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including UC, tax credits and child benefit); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap and its childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml new file mode 100644 index 0000000000..d9f934cc99 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/non_dep_deduction/working_age_exempt_partner_of_income_related_benefit_claimant.yaml @@ -0,0 +1,17 @@ +description: Scotland's working-age Council Tax Reduction scheme makes no deduction for a non-dependant who is, or whose partner is, on Income Support, income-based JSA or income-related ESA and who is not on Universal Credit (a "qualifying income-related benefit claimant"), from the year this applies (SSI 2021/249 reg. 90(8)(a) with reg. 4(1)). Before April 2022 (SSI 2012/303 reg. 67(8)(a)), and in the pension-age scheme, only a non-dependant who is on one of these benefits is exempt. +values: + 2013-04-01: false + 2022-04-01: true +metadata: + label: Scotland working-age CTR exempts a non-dependant whose partner is on an income-related benefit + unit: bool + period: year + reference: + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 90(8)(a) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/90 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 4(1) ("qualifying income-related benefit claimant") + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/4 + - title: Council Tax Reduction (Scotland) Regulations 2021, reg. 1 (in force 1 April 2022) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/1 + - title: Council Tax Reduction (Scotland) Regulations 2012, reg. 67(8)(a) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/67 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 0ee6a4fa4d..10f51ccfee 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -331,8 +331,9 @@ output: # HB Regs 2006 reg 2(3A): only the recipient is on ESA(IR), so only they are # out of remunerative work (reg 6(6)) and exempt from CTR (para 8(8)(a)). The - # partner's 210 a week is in the HB 192 band (46.85) and under the CTR 279 band - # (5.20); one deduction for the couple, the higher. + # partner is banded on the couple's 260 a week (210 wages plus 50 ESA), in + # the HB 192 band (46.85) and under the CTR 279 band (5.20); one deduction + # for the couple, the higher. housing_benefit_non_dep_deductions: [2_436.20, 0] council_tax_reduction_non_dep_deductions: [270.40, 0] @@ -374,7 +375,7 @@ # UC Regs 2013 reg 81: the cap reduces the award to 20,000; with 1 a week of # wages, 385.62 a week, # in the HB 365 band (105.20) and the CTR 279 band (10.60). Before the cap it - # would be 576.92 a week. + # would be 577.92 a week. housing_benefit_non_dep_deductions: [5_470.40, 0] council_tax_reduction_non_dep_deductions: [551.20, 0] @@ -523,7 +524,7 @@ output: # HB reg 74(3)-(4) and CTR Sch 1 para 8(3)-(4): one deduction for the # couple, banded on their joint 340 a week because the older partner is in - # remunerative work: 64.35 HB (269 band) and 10.60 CTR (279 band). + # remunerative work: 64.35 HB (279 band) and 10.60 CTR (279 band). housing_benefit_non_dep_deductions: [3_346.20, 0] council_tax_reduction_non_dep_deductions: [551.20, 0] @@ -568,3 +569,442 @@ # gets the daily living component of PIP. council_tax_reduction_household_has_non_dep_exemption: true merton_council_tax_reduction_non_dep_deductions: [0, 0] + +# Round 3 of the independent review: gross and earned income, the Scottish +# working-age scheme, the benefit cap's childcare protection, and child +# benefit entitlement. + +- name: Statutory maternity pay counts towards a non-dependant couple's gross income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + worker: + age: 30 + employment_income: 13_520 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 28 + statutory_maternity_pay: 1_560 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + couple_unit: + members: [worker, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, worker, partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB Regs 2006 reg 35(1)(i): statutory maternity pay is earnings. The + # couple's joint gross income is 260 wages plus 30 SMP, 290 a week (reg + # 74(4); CTR para 8(4)): the HB 279 band (64.35) and the CTR 279 band + # (10.60). Without the SMP, 260 would be a band lower in each. + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A couple's income-related ESA counts towards their joint gross income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + recipient: + age: 45 + esa_income_reported: 5_200 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 40 + employment_income: 9_360 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 5_200 + pension_credit: 0 + households: + household: + members: [claimant, recipient, partner] + country: ENGLAND + local_authority: MAIDSTONE + output: + # The partner works 16 hours and is banded on the couple's 180 wages plus + # 100 ESA(IR), 280 a week: the HB 279 band (64.35) and the CTR 279 band + # (10.60). The recipient is out of remunerative work (reg 6(6)) and exempt + # from CTR (para 8(8)(a)); one deduction for the couple, the higher. + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: Statutory paternity pay is earned income for the Universal Credit exemption + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + lodger: + age: 24 + statutory_paternity_pay: 300 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 2_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, lodger] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 55(4)(c): statutory paternity pay is employed earnings, + # so the award is not calculated on no earned income and neither + # exemption applies (HB reg 74(8)(b); CTR para 8(8)(c)). Out of + # remunerative work, the lodger pays the lowest amounts, 20.40 and 5.20. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [270.40, 0] + +- name: Pension contributions are deducted from self-employed earnings in the start-up period + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + lodger: + age: 24 + self_employment_income: 6_000 + pension_contributions: 6_000 + uc_is_in_startup_period: true + weekly_hours: 10 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 2_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, lodger] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 57(2), step 4: with no employed earnings, the 6,000 of + # relievable pension contributions comes off the 6,000 profit, leaving nil. + # The minimum income floor does not apply in the start-up period (reg + # 62(5)), so the award is on no earned income: exempt under HB reg + # 74(8)(b) (aged under 25) and CTR para 8(8)(c). + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: Scotland's working-age scheme exempts the partner of an Income Support claimant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 5_000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2021/249 reg 90(8)(a) with reg 4(1): the partner "has a partner who + # is" on Income Support and is not on Universal Credit, so both are exempt + # from CTR. Housing Benefit has no such rule (reg 74(8)(a) is for the + # person on IS, and only under 25): both are out of remunerative work, and + # the couple pays one lowest amount, 20.40 a week. + council_tax_reduction_non_dep_deduction_exempt: [false, true, true] + council_tax_reduction_non_dep_deductions: [0, 0] + housing_benefit_non_dep_deductions: [1_060.80, 0] + +- name: Before April 2022 Scotland exempted only the Income Support claimant + period: 2021 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 5_000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2012/303 reg 67(8)(a): only a non-dependant "who is on income + # support" is exempt. The partner, out of remunerative work, pays the + # lowest 2021-22 amount, 4.30 a week. + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_non_dep_deductions: [223.60, 0] + +- name: Scotland's pension-age scheme exempts only the Income Support claimant + period: 2026 + input: + people: + applicant: + age: 80 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + would_claim_uc: false + income_support: 5_000 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2012/319 reg 48(8)(a): a non-dependant "who is on income support". + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + +- name: The benefit cap does not reduce Universal Credit when childcare costs exceed the excess + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + guardian: + age: 20 + employment_income: 9_360 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + older_child: + age: 4 + is_claimant_or_partner: false + younger_child: + age: 2 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + guardian_unit: + members: [guardian, older_child, younger_child] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 22_000 + uc_childcare_element: 12_000 + child_benefit: 2_500 + benefit_cap: 22_020 + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, guardian, older_child, younger_child] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 81(1)-(2): the excess over the cap is 2,480, less than + # the 12,000 childcare costs element, so the award is not reduced. Gross + # income is 180 wages plus 22,000 UC and 2,500 child benefit, 651.15 a + # week: the top bands, 131.45 HB and 15.95 CTR. + housing_benefit_non_dep_deductions: [6_835.40, 0] + council_tax_reduction_non_dep_deductions: [829.40, 0] + +- name: A young person no one claims child benefit for is not disregarded for CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + parent: + age: 45 + employment_income: 10_400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + young_person: + age: 19 + current_education: NOT_IN_EDUCATION + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 16 + weekly_hours: 0 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [parent, young_person] + benunit_is_rent_liable: false + would_claim_child_benefit: false + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + would_claim_uc: false + households: + household: + members: [claimant, parent, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # LGFA 1992 Sch 1 para 3 needs another person to be "entitled to child + # benefit" for the young person, and entitlement needs a claim (SSAA 1992 + # s.1(1)). With no claim, both are charged: the parent's 200 a week is + # under the CTR 279 band (5.20), and the young person, out of remunerative + # work, pays the lowest amount (5.20). + council_tax_reduction_non_dep_deduction_exempt: [false, false, false] + council_tax_reduction_non_dep_deductions: [540.80, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 8741b3e64c..64e47752ee 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -19,8 +19,9 @@ 3. A non-dependant not in remunerative work (under 16 hours or, for Housing Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)) pays the lowest amount, whatever their income. -4. If the claimant or partner is exempt, the claimant family's deductions - are 0. +4. If the claimant or partner is blind or receives a qualifying disability + benefit (read from the generated inputs, and checked against the model's + flags), the claimant family's deductions are 0. 5. Aggregation: a family's deductions are the sum, over the other families in the household, of their claimant and partner's higher amount (both amounts for a Welsh working-age applicant's non-dependant couple with a Universal @@ -39,10 +40,16 @@ income-based JSA or Universal Credit without earned income. CTR (any age): a full-time student, on IS, JSA(IB), ESA(IR) or SPC, or, in a national scheme, on Universal Credit without earned income, or an adult for whom - someone else is entitled to child benefit (LGFA 1992 Sch 1 para 3). IS, - JSA(IB), ESA(IR) and SPC count only for the person they are payable to; - Universal Credit for both joint claimants; a self-employed loss counts as - nil earnings (UC reg 57(2)). + someone else is entitled to child benefit, which needs a claim (LGFA 1992 + Sch 1 para 3). In Scotland's working-age scheme from April 2022, also + anyone whose partner is on IS, JSA(IB) or ESA(IR) and who is not on + Universal Credit (SSI 2021/249 reg 90(8)(a), reg 4(1)). IS, JSA(IB), + ESA(IR) and SPC count only for the person they are payable to: the member + who reports them, or the benefit unit's head. Universal Credit counts for + both joint claimants. Earned income includes statutory sick, maternity and + paternity pay, less relievable pension contributions, which come off + self-employed earnings only when there are no employed earnings; a + self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). """ from pathlib import Path @@ -95,6 +102,17 @@ "pension_credit", "universal_credit_pre_benefit_cap", ] +REPORTED = { + "income_support": "income_support_reported", + "jsa_income": "jsa_income_reported", + "esa_income": "esa_income_reported", + "pension_credit": "pension_credit_reported", +} +STATUTORY_PAY = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", +] # Family benefits counted in gross income, held at zero so the oracle knows # each family's gross income. OTHER_FAMILY_BENEFITS = ["child_tax_credit", "working_tax_credit", "child_benefit"] @@ -129,6 +147,12 @@ def households(draw): earnings=draw(st.one_of(st.just(0.0), st.floats(0, 60_000))), other_income=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), self_employment=draw(st.one_of(st.just(0.0), st.floats(-20_000, 20_000))), + statutory_pay={ + name: draw(st.one_of(st.just(0.0), st.floats(0, 10_000))) + for name in STATUTORY_PAY + }, + pension_contributions=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + startup_period=draw(st.booleans()), hours=draw(st.sampled_from([0.0, 10.0, 15.9, 16.0, 35.0, 45.0])), income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), approved_training=False, @@ -148,6 +172,9 @@ def households(draw): earnings=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), other_income=0.0, self_employment=0.0, + statutory_pay=dict.fromkeys(STATUTORY_PAY, 0.0), + pension_contributions=0.0, + startup_period=False, hours=draw(st.sampled_from([0.0, 8.0, 16.0, 35.0])), income_rise=draw(st.one_of(st.just(0.0), st.floats(0, 10_000))), approved_training=training, @@ -161,6 +188,10 @@ def households(draw): benefit: draw(st.sampled_from([0.0, 0.0, 0.0, 1_000.0])) for benefit in BENEFITS }, + # Which adult non-dependant reports IS, JSA(IB), ESA(IR) and SPC, if + # anyone does. + reporter=draw(st.sampled_from([None, 0, 1])), + claims_child_benefit=draw(st.sampled_from([True, True, False])), non_dependants=non_dependants, ) @@ -191,12 +222,20 @@ def build(population, year, raise_income=False): for n, nd in enumerate(household["non_dependants"]): name = f"non_dependant_{h}_{n}" other = nd["other_income"] + (nd["income_rise"] if raise_income else 0) + reports = household.get("reporter") == n people[name] = dict( age=nd["age"], current_education=nd["education"], total_income=nd["earnings"] + other, employment_income=nd["earnings"], self_employment_income=nd["self_employment"], + **nd.get("statutory_pay", dict.fromkeys(STATUTORY_PAY, 0.0)), + pension_contributions=nd.get("pension_contributions", 0.0), + uc_is_in_startup_period=nd.get("startup_period", False), + **{ + reported: household["benefits"][benefit] if reports else 0.0 + for benefit, reported in REPORTED.items() + }, is_in_approved_training=nd["approved_training"], # Young people started at 16; adults' start ages don't matter. age_started_or_accepted_current_education_or_training=16, @@ -209,7 +248,9 @@ def build(population, year, raise_income=False): }, ) members.append(name) - persons.append(dict(household=h, benunit=2 * h + 1, non_dependant=nd)) + persons.append( + dict(household=h, benunit=2 * h + 1, non_dependant=nd, position=n) + ) benunits[f"claimant_unit_{h}"] = dict( members=[claimant], benunit_is_rent_liable=True, @@ -218,6 +259,7 @@ def build(population, year, raise_income=False): benunits[f"non_dependant_unit_{h}"] = dict( members=members, benunit_is_rent_liable=False, + would_claim_child_benefit=household.get("claims_child_benefit", True), **household["benefits"], **{benefit: 0.0 for benefit in OTHER_FAMILY_BENEFITS}, ) @@ -242,6 +284,8 @@ def calculate(sim, year): "is_benunit_head", "is_claimant_or_partner", "is_qualifying_young_person_for_child_benefit", + "uc_mif_applies", + "uc_minimum_income_floor", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -360,6 +404,24 @@ def test_non_dependant_deduction_invariants(population, year): ) # Claimant or partner of each person's own family (SSCBA s.137 couple). couple = r["is_claimant_or_partner"].astype(bool) + members_of = {} + for i, person in enumerate(persons): + members_of.setdefault(person["benunit"], []).append(i) + + def payee_of(i, benefit): + """The award's payee: the member who reports it, or the head.""" + person = persons[i] + household = population[person["household"]] + reporter = household.get("reporter") + reported = ( + person["non_dependant"] is not None + and reporter is not None + and reporter < len(household["non_dependants"]) + and household["benefits"][benefit] > 0 + ) + if reported: + return person["position"] == reporter + return bool(r["is_benunit_head"][i]) for i, person in enumerate(persons): household = population[person["household"]] @@ -374,23 +436,31 @@ def test_non_dependant_deduction_invariants(population, year): if nd is None: continue # 9. The exemptions, read straight from the inputs. - # IS, JSA(IB), ESA(IR) and SPC count for their payee: here the benefit - # unit's head, as no member reports them (HB reg 2(3)-(3A)). Universal - # Credit counts for both joint claimants. A self-employed loss counts as - # nil earnings (UC reg 57(2)). - payee = bool(r["is_benunit_head"][i]) - on = { - b: (bool(couple[i]) if b == "universal_credit_pre_benefit_cap" else payee) - and household["benefits"][b] > 0 - for b in BENEFITS - } + # IS, JSA(IB), ESA(IR) and SPC count for their payee: the member who + # reports them, or the benefit unit's head where none does (HB reg + # 2(3)-(3A)). Universal Credit counts for both joint claimants. + on = {b: payee_of(i, b) and household["benefits"][b] > 0 for b in REPORTED} + on["universal_credit_pre_benefit_cap"] = ( + bool(couple[i]) + and household["benefits"]["universal_credit_pre_benefit_cap"] > 0 + ) student = nd["education"] != "NOT_IN_EDUCATION" - # Inputs are stored as float32, so a tiny draw can underflow to nil. - earned = ( - max(0.0, float(np.float32(nd["earnings"]))) - + max(0.0, float(np.float32(nd["self_employment"]))) - > 0 + # Earned income (UC regs 55(4)-(5), 57(2), 62), in the inputs' float32. + f32 = np.float32 + statutory_pay = nd.get("statutory_pay", {}) + employed_gross = f32(nd["earnings"]) + sum( + (f32(statutory_pay.get(name, 0.0)) for name in STATUTORY_PAY), f32(0) ) + pension = f32(nd.get("pension_contributions", 0.0)) + employed = max(f32(0), employed_gross - pension) + self_employed = max( + f32(0), + max(f32(0), f32(nd["self_employment"])) + - (f32(0) if employed_gross > 0 else pension), + ) + if r["uc_mif_applies"][i]: + self_employed = max(self_employed, f32(r["uc_minimum_income_floor"][i])) + earned = employed + self_employed > 0 uc_without_earnings = on["universal_credit_pre_benefit_cap"] and not earned hb_exempt = ( student @@ -402,29 +472,40 @@ def test_non_dependant_deduction_invariants(population, year): ) assert bool(r["housing_benefit_non_dep_deduction_exempt"][i]) == hb_exempt national = household["country"] != "ENGLAND" or pension_age[i] + legacy = ["income_support", "jsa_income", "esa_income"] + on_legacy = any(on[b] for b in legacy) + # Scotland's working-age scheme (SSI 2021/249, from April 2022): a + # non-dependant who is, or whose partner is, on IS, JSA(IB) or ESA(IR) + # and who is not on Universal Credit. + if household["country"] == "SCOTLAND" and not pension_age[i]: + couple_on_legacy = bool(couple[i]) and any( + couple[j] + and any(payee_of(j, b) and household["benefits"][b] > 0 for b in legacy) + for j in members_of[person["benunit"]] + ) + qualifying = (on_legacy or couple_on_legacy) and not on[ + "universal_credit_pre_benefit_cap" + ] + else: + qualifying = on_legacy ctr_exempt = ( student - or any( - on[b] - for b in [ - "income_support", - "jsa_income", - "esa_income", - "pension_credit", - ] - ) + or qualifying + or on["pension_credit"] or (national and uc_without_earnings) - # LGFA 1992 Sch 1 para 3: an adult qualifying young person in - # someone else's family. + # LGFA 1992 Sch 1 para 3: an adult in someone else's family for + # whom child benefit is claimed. or ( nd["age"] >= 18 and bool(r["is_qualifying_young_person_for_child_benefit"][i]) and not couple[i] + and household.get("claims_child_benefit", True) ) ) assert ( bool(r["council_tax_reduction_non_dep_deduction_exempt"][i]) == ctr_exempt ) + # 2-3. Housing Benefit bounds and the not-in-work amount. if ( r["housing_benefit_individual_non_dep_deduction_eligible"][i] @@ -465,9 +546,15 @@ def test_non_dependant_deduction_invariants(population, year): benunit_count, [False] * benunit_count, ) - expected_hb = np.where( - r["housing_benefit_non_dep_deductions_claimant_exempt"], 0, expected_hb + # 4. The claimant exemption, from the generated disability inputs: only + # the claimant family's claimant can be blind or on a qualifying benefit. + disabled = np.array([h["claimant_disability"] is not None for h in population]) + claimant_exempt = np.repeat(disabled, 2) & (np.arange(benunit_count) % 2 == 0) + np.testing.assert_array_equal( + r["housing_benefit_non_dep_deductions_claimant_exempt"].astype(bool), + claimant_exempt, ) + expected_hb = np.where(claimant_exempt, 0, expected_hb) np.testing.assert_allclose( r["housing_benefit_non_dep_deductions"], expected_hb, atol=0.01 ) @@ -485,9 +572,12 @@ def test_non_dependant_deduction_invariants(population, year): benunit_count, welsh_working_age, ) - household_exempt = np.repeat( - r["council_tax_reduction_household_has_non_dep_exemption"], 2 + # The applicant is the claimant family (its member is the oldest). + np.testing.assert_array_equal( + r["council_tax_reduction_household_has_non_dep_exemption"].astype(bool), + disabled, ) + household_exempt = np.repeat(disabled, 2) expected_ctr = np.where(household_exempt, 0, expected_ctr) np.testing.assert_allclose( r["council_tax_reduction_non_dep_deductions"], expected_ctr, atol=0.01 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 58d69dd498..8d7eaf9d20 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -4,10 +4,35 @@ from policyengine_uk.model_api import * # Family benefits counted in a couple's gross income, besides Universal Credit. +# None is taxable, so total_income leaves them out. FAMILY_GROSS_INCOME_BENEFITS = [ "child_tax_credit", "working_tax_credit", "child_benefit", + "income_support", + "jsa_income", + "esa_income", + "pension_credit", +] + +# A person's gross income outside total_income: statutory payments, which are +# earnings (HB Regs 2006 reg 35(1)(i); UC Regs 2013 reg 55(4)) the model +# keeps apart from employment income, and Maternity Allowance, which is +# untaxed. +PERSONAL_GROSS_INCOME_OUTSIDE_TOTAL_INCOME = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", + "maternity_allowance", +] + +# Statutory payments treated as employed earnings (UC Regs 2013 reg 55(4)). +# Statutory adoption, shared parental, parental bereavement and neonatal care +# pay have no input. +STATUTORY_PAY_EARNINGS = [ + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", ] # Benefits the benefit cap counts, other than Housing Benefit (benefit_cap_reduction). @@ -28,12 +53,17 @@ def non_dependant_weekly_gross_income(person, period): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 - para 8(4)): each member's taxable income plus the family's Universal - Credit, tax credits and child benefit. Anyone else in the benefit unit is a - separate non-dependant, banded on their own taxable income. Taxable income - excludes the disregarded disability benefits (HB reg 74(9)).""" + para 8(4)): each member's own income plus the family's Universal Credit, + tax credits, child benefit, Income Support, income-based JSA, + income-related ESA and Pension Credit. Anyone else in the benefit unit is a + separate non-dependant, banded on their own income. A person's own income + is their taxable income plus statutory sick, maternity and paternity pay + and Maternity Allowance. It leaves out the disability benefits HB reg + 74(9) disregards, which are untaxed.""" claimant_or_partner = person("is_claimant_or_partner", period) - own_income = max_(0, person("total_income", period)) + own_income = max_(0, person("total_income", period)) + add( + person, period, PERSONAL_GROSS_INCOME_OUTSIDE_TOTAL_INCOME + ) family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS ) @@ -49,30 +79,37 @@ def universal_credit_after_benefit_cap(benunit, period): reg 81), for a family that is not liable for rent. universal_credit itself depends on Housing Benefit through the cap, so the cap is applied here to the capped benefits other than Housing Benefit, which such a family does - not receive.""" + not receive. The award is reduced by the excess minus the childcare costs + element, and not at all where that element is greater than the excess + (reg 81(1)-(2)).""" capped = sum( benunit(benefit, period) for benefit in CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT ) - reduction = max_(0, capped - benunit("benefit_cap", period)) + excess = max_(0, capped - benunit("benefit_cap", period)) + reduction = max_(0, excess - benunit("uc_childcare_element", period)) return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) def has_earned_income(person, period): """Whether the person has earned income (UC Regs 2013 reg 52): employed - earnings, including statutory sick and maternity pay and less relievable - pension contributions (reg 55(4)-(5)); self-employed earnings, a loss - counting as nil (reg 57(2)) and the minimum income floor applying where - the Universal Credit model applies it (reg 62); and other paid work.""" - employed = max_( + earnings, including statutory sick, maternity and paternity pay and less + relievable pension contributions (reg 55(4)-(5)); self-employed earnings, + a loss counting as nil and less any relievable pension contributions not + already deducted from employed earnings (reg 57(2), steps 3-4), with the + minimum income floor applying where the Universal Credit model applies it + (reg 62); and other paid work.""" + pension_contributions = person("pension_contributions", period) + employed_gross = add(person, period, ["employment_income", *STATUTORY_PAY_EARNINGS]) + has_employed_earnings = employed_gross > 0 + employed = max_(0, employed_gross - pension_contributions) + # Contributions are deducted from employed earnings where there are any + # (reg 55(5)(a)), and otherwise from self-employed earnings (reg 57(2), + # step 4). + self_employed = max_( 0, - add( - person, - period, - ["employment_income", "statutory_sick_pay", "statutory_maternity_pay"], - ) - - person("pension_contributions", period), + max_(0, person("self_employment_income", period)) + - where(has_employed_earnings, 0, pension_contributions), ) - self_employed = max_(0, person("self_employment_income", period)) self_employed = where( person("uc_mif_applies", period), max_(self_employed, person("uc_minimum_income_floor", period)), diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 0b1721e365..433ef49c7d 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -21,8 +21,11 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "Modelled exemptions in the England pensioner, Scottish and Welsh " "schemes: a full-time student; a non-dependant on Income Support, " "income-based JSA, income-related ESA or State Pension Credit (any " - "age); an adult for whom someone else is entitled to child benefit " - "(LGFA 1992 Sch 1 para 3); and, from the year each scheme added it, " + "age), and in Scotland's working-age scheme from April 2022 one whose " + "partner is on Income Support, income-based JSA or income-related ESA " + "and who is not on Universal Credit; an adult for whom someone else is " + "entitled to child benefit (LGFA 1992 Sch 1 para 3); and, from the year " + "each scheme added it, " "one entitled to Universal Credit calculated on no earned income. Wales excludes " "members of the ESA work-related activity group; with no ESA group " "input, income-related ESA is treated as exempt. Not modelled: other " @@ -37,6 +40,8 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", "https://www.legislation.gov.uk/ssi/2012/319/regulation/48", "https://www.legislation.gov.uk/ssi/2021/249/regulation/90", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/4", + "https://www.legislation.gov.uk/ukpga/1992/14/schedule/1/paragraph/3", ) def formula(person, period, parameters): @@ -57,20 +62,46 @@ def formula(person, period, parameters): # IS, income-based JSA, income-related ESA and SPC count for the person # they are payable to; Universal Credit for both joint claimants. claimant_or_partner = person("is_claimant_or_partner", period) - on_income_related_benefit = ( + on_legacy_income_related_benefit = ( is_award_payee(person, period, "income_support", "income_support_reported") | is_award_payee(person, period, "jsa_income", "jsa_income_reported") | is_award_payee(person, period, "esa_income", "esa_income_reported") - | is_award_payee( - person, period, "pension_credit", "pension_credit_reported" - ) + ) + on_pension_credit = is_award_payee( + person, period, "pension_credit", "pension_credit_reported" + ) + entitled_to_universal_credit = claimant_or_partner & ( + person.benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + # SSI 2021/249 reg 90(8)(a): a "qualifying income-related benefit + # claimant", defined in reg 4(1) as one "who is, or who has a partner + # who is," on IS, income-based JSA or income-related ESA "and is not on + # universal credit". Read for the non-dependant, as the definition's + # "applicant" would leave the limb no application. + couple_on_legacy_income_related_benefit = claimant_or_partner & ( + person.benunit.any(on_legacy_income_related_benefit & claimant_or_partner) + ) + partner_rule = ( + is_scotland_scheme(country) + & ~has_pensioner + & scotland.non_dep_deduction.working_age_exempt_partner_of_income_related_benefit_claimant + ) + qualifying_income_related_benefit_claimant = where( + partner_rule, + (on_legacy_income_related_benefit | couple_on_legacy_income_related_benefit) + & ~entitled_to_universal_credit, + on_legacy_income_related_benefit, ) # LGFA 1992 Sch 1 para 3 (via para 8(8)(b) and equivalents): an adult - # for whom another person is entitled to child benefit, that is a - # qualifying young person in someone else's family. + # "in respect of whom another person is entitled to child benefit". + # Entitlement needs a claim (SSAA 1992 s.1(1)); an election not to be + # paid keeps it (s.13A), so the opt-out flag is not read. + entitled_to_child_benefit_for = ( + add(person, period, ["child_benefit_respective_amount"]) > 0 + ) & person.benunit("would_claim_child_benefit", period) disregarded_for_child_benefit = ( (person("age", period) >= 18) - & person("is_qualifying_young_person_for_child_benefit", period) + & entitled_to_child_benefit_for & ~claimant_or_partner ) universal_credit_limb = select( @@ -82,9 +113,6 @@ def formula(person, period, parameters): ], default=False, ) - entitled_to_universal_credit = claimant_or_partner & ( - person.benunit("universal_credit_pre_benefit_cap", period) > 0 - ) universal_credit_without_earned_income = ( universal_credit_limb & entitled_to_universal_credit @@ -92,7 +120,8 @@ def formula(person, period, parameters): ) return ( full_time_student - | on_income_related_benefit + | qualifying_income_related_benefit_claimant + | on_pension_credit | disregarded_for_child_benefit | universal_credit_without_earned_income ) From aa44a1a07d62a8904c001b2af7d45adbff6681a9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 3 Oct 2026 12:10:06 -0400 Subject: [PATCH 18/67] Address review round 4: Maternity Allowance in the cap basket, losses in gross income - The after-cap UC used in a couple's gross income counts Maternity Allowance among the capped benefits (Welfare Reform Act 2012 s.96(10)(i)). - A self-employment or property loss no longer cancels other gross income: it is added back to taxable income (DWP HB guidance A5.551). - A YAML case for the Scottish partner rule's "not on Universal Credit" condition. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 165 ++++++++++++++++++ .../non_dep_deduction/_non_dependants.py | 22 ++- 2 files changed, 180 insertions(+), 7 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 10f51ccfee..5020b099f9 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1008,3 +1008,168 @@ # work, pays the lowest amount (5.20). council_tax_reduction_non_dep_deduction_exempt: [false, false, false] council_tax_reduction_non_dep_deductions: [540.80, 0] + +# Round 4 of the independent review. + +- name: Maternity Allowance counts towards the benefit cap on a non-dependant's Universal Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + worker: + age: 20 + employment_income: 9_200 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 38 + maternity_allowance: 6_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + child_1: + age: 16 + is_claimant_or_partner: false + child_2: + age: 15 + is_claimant_or_partner: false + child_3: + age: 13 + is_claimant_or_partner: false + child_4: + age: 11 + is_claimant_or_partner: false + child_5: + age: 0 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + family_unit: + members: [worker, partner, child_1, child_2, child_3, child_4, child_5] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 14_000 + uc_childcare_element: 0 + child_benefit: 5_129.80 + benefit_cap: 22_020 + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, worker, partner, child_1, child_2, child_3, child_4, child_5] + country: ENGLAND + local_authority: MAIDSTONE + output: + # Welfare Reform Act 2012 s.96(10)(i): Maternity Allowance is a capped + # benefit. Capped benefits are 25,129.80, 3,109.80 over the cap, so UC + # is 10,890.20 (UC reg 81). Joint gross income is 9,200 wages + 6,000 MA + # + 10,890.20 UC + 5,129.80 child benefit = 600.38 a week: the HB 485 + # band (119.85) and the CTR 485 band (13.30). + housing_benefit_non_dep_deductions: [6_232.20, 0] + council_tax_reduction_non_dep_deductions: [691.60, 0] + +- name: A self-employment loss does not cancel a non-dependant's gross wages + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + lodger: + age: 30 + employment_income: 15_600 + self_employment_income: -10_400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + lodger_unit: + members: [lodger] + benunit_is_rent_liable: false + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, lodger] + country: ENGLAND + local_authority: MAIDSTONE + output: + # HB reg 74(2) and SI 2006/214 reg 55(2): "normal gross weekly income". + # Taxable income nets the 10,400 loss against the 15,600 wages; gross + # income does not (DWP HB guidance A5.551), so the lodger is banded on + # 300 a week: the HB 279 band (64.35) and the CTR 279 band (10.60). + housing_benefit_non_dep_deductions: [3_346.20, 0] + council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: Scotland's partner rule does not cover a couple on Universal Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + is_claimant_or_partner: true + recipient: + age: 35 + income_support_reported: 5_000 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + partner: + age: 34 + employment_income: 5_200 + weekly_hours: 10 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + benunits: + applicant_unit: + members: [applicant] + benunit_is_rent_liable: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + universal_credit_pre_benefit_cap: 0 + couple_unit: + members: [recipient, partner] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 1_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 5_000 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, recipient, partner] + country: SCOTLAND + local_authority: GLASGOW_CITY + output: + # SSI 2021/249 reg 4(1): a qualifying income-related benefit claimant "is + # not on universal credit". Here the couple is, so the partner rule does + # not apply. The recipient is exempt as on UC without earned income (reg + # 90(8)(c)); the partner has wages, so is charged the lowest amount (5.55, + # under 16 hours), the couple's one deduction. + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_non_dep_deductions: [288.60, 0] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 8d7eaf9d20..2f81307bd1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -35,7 +35,8 @@ "statutory_paternity_pay", ] -# Benefits the benefit cap counts, other than Housing Benefit (benefit_cap_reduction). +# Benefits the benefit cap counts (Welfare Reform Act 2012 s.96(10)), other than +# Housing Benefit. Bereavement allowance and the widow's benefits have no input. CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ "child_benefit", "child_tax_credit", @@ -47,6 +48,7 @@ "incapacity_benefit", "esa_contrib", "sda", + "maternity_allowance", ] @@ -57,11 +59,19 @@ def non_dependant_weekly_gross_income(person, period): tax credits, child benefit, Income Support, income-based JSA, income-related ESA and Pension Credit. Anyone else in the benefit unit is a separate non-dependant, banded on their own income. A person's own income - is their taxable income plus statutory sick, maternity and paternity pay - and Maternity Allowance. It leaves out the disability benefits HB reg + is their taxable income, with any self-employment or property loss added + back, plus statutory sick, maternity and paternity pay and Maternity + Allowance. It leaves out the disability benefits HB reg 74(9) disregards, which are untaxed.""" claimant_or_partner = person("is_claimant_or_partner", period) - own_income = max_(0, person("total_income", period)) + add( + # Gross income: a self-employment or property loss does not reduce other + # income, as it does in taxable income. DWP HB guidance A5.551 takes a + # self-employed non-dependant's gross income as "their total income with no + # deductions for business expenses"; the model has profit, not turnover. + losses = max_(0, -person("self_employment_income", period)) + max_( + 0, -person("property_income", period) + ) + own_income = max_(0, person("total_income", period) + losses) + add( person, period, PERSONAL_GROSS_INCOME_OUTSIDE_TOTAL_INCOME ) family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( @@ -82,9 +92,7 @@ def universal_credit_after_benefit_cap(benunit, period): not receive. The award is reduced by the excess minus the childcare costs element, and not at all where that element is greater than the excess (reg 81(1)-(2)).""" - capped = sum( - benunit(benefit, period) for benefit in CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT - ) + capped = add(benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT) excess = max_(0, capped - benunit("benefit_cap", period)) reduction = max_(0, excess - benunit("uc_childcare_element", period)) return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) From ae4e42bf78211edd71537f428edf2610b5ec6bef Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 19:14:36 -0400 Subject: [PATCH 19/67] Make the in-unit property oracle scheme-aware and follow the UC counted claim Since #2015 each Council Tax Reduction claim is assessed under its own family's scheme, and #2007 recomputes a jointly liable applicant's deductions under the other scheme. The oracle and the conservation and no-op invariants charged every claiming family the head's-scheme amounts, so a pensioner head with working-age sharers in England (no national working-age scale) failed against a correct model. They now take each family's own-scheme amounts, other-scheme amounts from the aggregator's helper, and each family's scheme from the model. UC attribution follows main's uc_non_dependants_counted: other families' non-dependants count in the first liable claiming family's claim, and a family's own in-unit non-dependant in its own claim if it claims UC. The no-op invariant uses the counted claim instead of the head's family. Co-Authored-By: Claude Opus 5.5 --- ...t_benefit_unit_non_dependant_properties.py | 111 +++++++++++++----- 1 file changed, 81 insertions(+), 30 deletions(-) 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 0e1c7cd98f..8a2428b98f 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -17,7 +17,8 @@ counted once) is borne in full across the joint occupiers, and once more by a boarder or lodger for a non-dependant in its own benefit unit, unless a claimant is exempt; each Council Tax Reduction claiming family - bears every non-dependant in proportion to its joint-liability share + bears every non-dependant, under its own scheme, in proportion to its + joint-liability share unless the applicant is exempt; 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 (UC Sch 4 para 15; HB reg @@ -33,7 +34,13 @@ from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st +from policyengine_core.periods import period as make_period + from policyengine_uk import Simulation +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction._national_non_dependants import ( + household_head_family_is_pensioner, + individual_non_dep_deduction, +) # 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. @@ -231,6 +238,8 @@ def oracle( ctr_share, uc_counted, uc_own_claim, + ctr_other, + ctr_own_scheme_is_heads, ): """Expected deductions per family from the generated structure. @@ -239,12 +248,16 @@ def oracle( Universal Credit claim counts the household's non-dependants (uc_non_dependants_counted), whether a family makes its own claim, the CTR claim flags and the joint-liability shares are read from the model. + ctr is each person's deduction under the scheme of the household head's + family; ctr_other under the other scheme, which a claiming family on that + scheme bears (SI 2012/2885 reg 3). """ 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)) hb = counted_once_per_couple(person_rows, hb) ctr = counted_once_per_couple(person_rows, ctr) + ctr_other = counted_once_per_couple(person_rows, ctr_other) 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] @@ -300,7 +313,8 @@ def oracle( for i in fams: if i == f and not own_family: continue - exp_ctr[i] += claims[i] * ctr_share[i] * ctr[p] + amount = ctr[p] if ctr_own_scheme_is_heads[i] else ctr_other[p] + exp_ctr[i] += claims[i] * ctr_share[i] * amount # A claimant or partner on PIP daily living exempts their own family # from all HB and CTR deductions (reg 74(6); Sch 1 para 8(6)). for i in fams: @@ -310,6 +324,54 @@ def oracle( return exp_uc, exp_hb, exp_ctr, expected_uc_individual +def ctr_by_scheme(sim, person_rows, family_rows): + """Each person's CTR deduction under the other scheme (pensioner or + working-age) from the household head's family, and whether each family is + on the head's scheme. Computed with the aggregator's own helper, since a + jointly liable applicant on the other scheme has its deductions + recomputed under that scheme.""" + person = sim.populations["person"] + period = make_period(YEAR) + head_pensioner = np.asarray(household_head_family_is_pensioner(person, period)) + eligible = ( + calc(sim, "council_tax_reduction_individual_non_dep_deduction_eligible") > 0 + ) + other = np.asarray( + individual_non_dep_deduction( + person, period, sim.tax_benefit_system.parameters, ~head_pensioner + ), + dtype=float, + ) + first_member = {} + for p, row in enumerate(person_rows): + first_member.setdefault(row["family"], p) + family_head_pensioner = np.array( + [head_pensioner[first_member[f]] for f in range(len(family_rows))] + ) + family_pensioner = calc(sim, "council_tax_reduction_pensioner") > 0 + return dict( + ctr_other=eligible * other, + ctr_own_scheme_is_heads=family_pensioner == family_head_pensioner, + ) + + +def pool_under_own_scheme(person_rows, family_rows, v, mask=None): + """For each family, the household's CTR deductions (a couple's once) + under that family's own scheme, over the members in mask (everyone by + default).""" + mask = np.ones(len(person_rows), dtype=bool) if mask is None else mask + person_household = np.array([row["household"] for row in person_rows]) + family_household = np.array([row["household"] for row in family_rows]) + pools = {} + for scheme, amounts in (("head", v["ctr"]), ("other", v["ctr_other"])): + counted = counted_once_per_couple(person_rows, amounts) * mask + by_household = np.bincount( + person_household, weights=counted, minlength=family_household.max() + 1 + ) + pools[scheme] = by_household[family_household] + return np.where(v["ctr_own_scheme_is_heads"], pools["head"], pools["other"]) + + def run(population, convert=None): situation, person_rows, family_rows = build(population, convert) sim = Simulation(situation=situation) @@ -321,6 +383,7 @@ def run(population, convert=None): claims=calc(sim, "council_tax_reduction_claimant_benunit"), ctr_share=calc(sim, "council_tax_reduction_joint_liability_share"), uc_counted=calc(sim, "uc_non_dependants_counted") > 0, + **ctr_by_scheme(sim, person_rows, family_rows), uc_own_claim=( (calc(sim, "benunit_is_rent_liable") > 0) & (calc(sim, "is_uc_eligible") > 0) @@ -393,7 +456,6 @@ def test_conservation_and_exemptions(population): # across the joint occupiers, and a lodger's own non-dependant once more # by the lodger, where no claimant is exempt. hb_counted = counted_once_per_couple(person_rows, v["hb"]) - ctr_counted = counted_once_per_couple(person_rows, v["ctr"]) hb_families = sim.map_result( calc(sim, "housing_benefit_non_dep_deductions"), "benunit", "household" ) @@ -415,30 +477,21 @@ def test_conservation_and_exemptions(population): ) assert np.allclose(hb_families[~hb_exempt], hb_people[~hb_exempt], atol=0.01) assert np.all(hb_families <= hb_people + 0.01) - # 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); none - # where the applicant is exempt. - 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(ctr_counted, "person", "household") - ctr_exempt = ( - sim.map_result( - calc(sim, "council_tax_reduction_applicant_has_non_dep_exemption") - * v["claims"], - "benunit", - "household", - ) - > 0 - ) - assert np.allclose( - ctr_families[~ctr_exempt], - (claim_shares * ctr_people)[~ctr_exempt], - atol=0.01, + # CTR: every non-dependant is borne by each claiming family, under its + # own scheme, in proportion to its joint-liability share (which follows + # the regulations' per-person wording, so shares need not sum to one); + # none where the applicant is exempt. This holds on the generator's + # domain (rent > 0, claimants liable for it), where no claiming family's + # own claimant or partner has a deduction. + ctr_families = calc(sim, "council_tax_reduction_non_dep_deductions") + expected_ctr = ( + v["claims"] + * v["ctr_share"] + * pool_under_own_scheme(person_rows, family_rows, v) ) - assert np.all(ctr_families <= claim_shares * ctr_people + 0.01) + ctr_exempt = calc(sim, "council_tax_reduction_applicant_has_non_dep_exemption") > 0 + assert np.allclose(ctr_families[~ctr_exempt], expected_ctr[~ctr_exempt], atol=0.01) + assert np.all(ctr_families[ctr_exempt] == 0) # 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" @@ -507,11 +560,9 @@ def other_families(amounts): atol=0.01, ) ctr_exempt = calc(sim, "council_tax_reduction_applicant_has_non_dep_exemption") > 0 - ctr_counted = counted_once_per_couple(person_rows, v["ctr"]) + pool = pool_under_own_scheme(person_rows, family_rows, v, mask=other) assert np.allclose( calc(sim, "council_tax_reduction_non_dep_deductions"), - np.where( - ctr_exempt, 0, v["claims"] * v["ctr_share"] * other_families(ctr_counted) - ), + np.where(ctr_exempt, 0, v["claims"] * v["ctr_share"] * pool), atol=0.01, ) From 8e60349ff4ca763086814b7b3e783cb15f3354c8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 20:10:55 -0400 Subject: [PATCH 20/67] Apply reg 74(3) and 74(6) in the sharers property test Main's HB conservation check asserted that families' deductions sum to the household total of individual deductions. #2007 makes a non-dependant couple pay one deduction, the higher (HB Regs 2006 reg 74(3)), and charges nothing to a family whose claimant or partner is exempt (reg 74(6)). The check now expects the pool of per-family amounts times the share of the rent borne by families that are not exempt, and keeps an upper bound at the household total. Co-Authored-By: Claude Opus 5.5 --- ...f_sharers_boarders_and_lodgers_properties.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 0b68d8b3b1..2350446393 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -282,7 +282,22 @@ def test_non_dependants(population): ) hb_total = sim.map_result(hb_individual, "person", "household") assert hb_total.max() > 0 - assert np.allclose(per_household(sim, hb), hb_total, rtol=1e-5, atol=0.01) + # Each non-dependant family pays one deduction for its couple, the higher + # (HB Regs 2006 reg 74(3)), and one for each other member. The pool is + # apportioned over the families liable for the rent, and none falls on a + # family whose claimant or partner is exempt (reg 74(6)). + families = sim.populations["benunit"] + couple = calc(sim, "is_claimant_or_partner").astype(bool) + family_amount = families.max(hb_individual * couple) + families.sum( + hb_individual * ~couple + ) + pool = per_household(sim, family_amount) + exempt = calc(sim, "housing_benefit_non_dep_deductions_claimant_exempt").astype( + bool + ) + borne = per_household(sim, calc(sim, "share_of_household_rent") * ~exempt) + assert np.allclose(per_household(sim, hb), pool * borne, rtol=1e-5, atol=0.01) + assert np.all(per_household(sim, hb) <= hb_total + 0.01) assert np.all(hb[payer] == 0) assert np.all(uc[payer] == 0) # Council Tax Reduction: sharers, boarders and lodgers are not From 257447381a191f4c2741dfb53e179c1722277c26 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:11:18 -0400 Subject: [PATCH 21/67] Count only the couple's capped benefits; test mixed-scheme applicants Round 6 of the independent review: - The after-cap UC used in a non-dependant family's gross income counts a person's own capped benefits (contribution-based JSA and ESA, incapacity benefit, SDA, Maternity Allowance) only for the claimant or partner. UC Regs 2013 reg 79(1) caps the benefits "to which a single person or couple is entitled". A YAML case gives a 17-year-old in the family her own Maternity Allowance; it fails on the previous head (HB 5,470.40, CTR 551.20 instead of 6,232.20 and 691.60). - New council_tax_reduction_non_dep_deductions_mixed_schemes.yaml covers jointly liable applicants under different national schemes in England, Wales and Scotland, including the recompute path where the applicant's scheme differs from the household head's. - 2025-26 second- and middle-band HB cases (45.15 and 101.35 a week). - The sharers property test's docstring states invariant 4 as tested. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- ...benefits_individual_non_dep_deduction.yaml | 30 +++ .../non_dependant_deduction_review_cases.yaml | 54 ++++ ...tion_non_dep_deductions_mixed_schemes.yaml | 241 ++++++++++++++++++ ...sharers_boarders_and_lodgers_properties.py | 5 +- .../non_dep_deduction/_non_dependants.py | 18 +- 6 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 3ad3195c5f..40b5dd5254 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap and its childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap, counting only the couple's own capped benefits and keeping the childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts, with each jointly liable applicant's deduction taken under its own scheme. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 4684b12782..effa50cf33 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -347,3 +347,33 @@ child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 19.65 * 52 + +- name: 2025-26 second band in remunerative work + period: 2025 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 10_400 # 200 a week + weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + household_benefits_individual_non_dep_deduction: 45.15 * 52 + +- name: 2025-26 middle band in remunerative work + period: 2025 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + total_income: 20_800 # 400 a week + weekly_hours: 35 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + household_benefits_individual_non_dep_deduction: 101.35 * 52 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 3542d69496..84f05e8771 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1173,3 +1173,57 @@ # under 16 hours), the couple's one deduction. council_tax_reduction_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_non_dep_deductions: [288.60, 0] + +# Round 6 of the independent review. + +- name: A young person's own Maternity Allowance does not cap the family's Universal Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + is_household_head: true + worker: + age: 20 + employment_income: 7_000 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + young_person: + age: 17 + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 16 + maternity_allowance: 6_000 + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + family_unit: + members: [worker, young_person] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 14_000 + uc_childcare_element: 0 + child_benefit: 5_129.80 + uc_benefit_cap: 22_020 + child_tax_credit: 0 + working_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, worker, young_person] + country: ENGLAND + local_authority: MAIDSTONE + output: + # UC Regs 2013 reg 79(1): the cap counts the welfare benefits "to which a + # single person or couple is entitled". The young person's own 6,000 is + # not the claimant's, so the capped total is 19,129.80, under the cap, and + # UC stays 14,000. The worker is banded on 7,000 + 14,000 + 5,129.80 = + # 502.50 a week: the HB 485 band (119.85) and the CTR 485 band (13.30). + housing_benefit_non_dep_deductions: [6_232.20, 0] + council_tax_reduction_non_dep_deductions: [691.60, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml new file mode 100644 index 0000000000..6cd1e3895c --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml @@ -0,0 +1,241 @@ +# Jointly liable applicants under different national schemes. Each applicant's +# scheme follows its own family (SI 2012/2885 reg 3; council_tax_reduction_ +# pensioner), and a non-dependant of two jointly liable people is apportioned +# equally between them (Sch 1 para 8(5); WSI 2013/3029 Sch 1 para 3(5) and +# Sch 6 para 5(5); SSI 2021/249 reg 90(5)). 2026-27 scales: England +# pensioners 279 band 10.60; Wales 279 band 13.35; Scotland lowest 5.55, 286 +# band 10.95. Cases from the round-6 review of #2007. + +- name: England, working-age head and pension-age sharer, deduction from the sharer's pensioner claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_household_head: true + current_education: NOT_IN_EDUCATION + sharer: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 25 + employment_income: 15_600 # 300 a week + hours_worked: 1_040 # 20 a week + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + households: + home: + members: [head, sharer, non_dependant] + country: ENGLAND + local_authority: MAIDSTONE + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + # The pension-age sharer's claim bears half of 10.60 * 52 = 551.20. The + # working-age head is in Maidstone's local scheme, so no national + # deduction; the person-level variable reports the head's scheme. + council_tax_reduction_non_dep_deductions: [0, 275.60, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 0, 0] + +- name: Wales, pension-age head and working-age sharer with a non-dependant UC couple + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + current_education: NOT_IN_EDUCATION + first_partner: + age: 30 + employment_income: 10_400 + hours_worked: 832 # 16 a week + current_education: NOT_IN_EDUCATION + second_partner: + age: 30 + employment_income: 5_200 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + sharer: + age: 30 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + couple_family: + members: [first_partner, second_partner] + universal_credit_pre_benefit_cap: 3_000 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + households: + home: + members: [head, first_partner, second_partner, sharer] + country: WALES + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + # The couple's joint 357.69 a week is in the 279 band (13.35 * 52 = + # 694.20). The pension-age head's claim takes one deduction for the couple + # (Sch 1 para 3(3)); the working-age sharer's takes one for each member + # of a couple with a UC award (Sch 6 para 5(3)). Each bears half. + council_tax_reduction_non_dep_deductions: [347.10, 0, 694.20] + +- name: Wales, working-age head and pension-age sharer with a non-dependant UC couple + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 30 + is_household_head: true + current_education: NOT_IN_EDUCATION + first_partner: + age: 30 + employment_income: 10_400 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + second_partner: + age: 30 + employment_income: 5_200 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + sharer: + age: 70 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + couple_family: + members: [first_partner, second_partner] + universal_credit_pre_benefit_cap: 3_000 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + households: + home: + members: [head, first_partner, second_partner, sharer] + country: WALES + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_non_dep_deductions: [694.20, 0, 347.10] + +- name: Scotland, working-age head and pension-age sharer, the partner rule only for the working-age claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + is_household_head: true + current_education: NOT_IN_EDUCATION + sharer: + age: 70 + current_education: NOT_IN_EDUCATION + recipient: + age: 35 + esa_income_reported: 5_200 + current_education: NOT_IN_EDUCATION + worker: + age: 35 + employment_income: 15_600 + hours_worked: 1_560 # 30 a week + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + couple_family: + members: [recipient, worker] + esa_income: 5_200 + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + households: + home: + members: [head, sharer, recipient, worker] + country: SCOTLAND + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + # Working-age head (SSI 2021/249 reg 90(8)(a), reg 4(1)): the worker is + # the partner of an income-related ESA claimant not on UC, so no + # deduction. Pension-age sharer (SSI 2012/319 reg 48): only the recipient + # is exempt; the worker is banded on the couple's 400 a week, 10.95 * 52 + # = 569.40, half each. The person-level flags report the head's scheme. + council_tax_reduction_non_dep_deductions: [0, 284.70, 0] + council_tax_reduction_non_dep_deduction_exempt: [false, false, true, true] + +- name: Scotland, pension-age head and working-age sharer, the partner rule only for the working-age claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + current_education: NOT_IN_EDUCATION + sharer: + age: 40 + current_education: NOT_IN_EDUCATION + recipient: + age: 35 + esa_income_reported: 5_200 + current_education: NOT_IN_EDUCATION + worker: + age: 35 + employment_income: 15_600 + hours_worked: 1_560 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + couple_family: + members: [recipient, worker] + esa_income: 5_200 + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + households: + home: + members: [head, sharer, recipient, worker] + country: SCOTLAND + rent: 12_000 + tenure_type: RENT_PRIVATELY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_non_dep_deductions: [284.70, 0, 0] + council_tax_reduction_non_dep_deduction_exempt: [false, false, true, false] diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 2350446393..c770dc202b 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -19,7 +19,10 @@ most one family per household counts the household's non-dependants for Universal Credit, it is liable for the household's rent, and only it has Universal Credit deductions; Housing Benefit deductions over the families - liable for the rent sum to the household's non-dependants' deductions; a + liable for the rent sum to the household's non-dependant deductions (one + per couple, the higher: HB Regs 2006 reg 74(3)) times the share of the + rent borne by families whose claimant or partner is not exempt (reg + 74(6)), and never exceed the sum of individual deductions; a boarder's or lodger's family has no Universal Credit or Housing Benefit deductions; no sharer, boarder or lodger is a Council Tax Reduction non-dependant. diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 99888289c2..9dfdcb7ddf 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -36,14 +36,20 @@ ] # Benefits the benefit cap counts (Welfare Reform Act 2012 s.96(10)), other than -# Housing Benefit. Bereavement allowance and the widow's benefits have no input. -CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ +# Housing Benefit, awarded to the family. Bereavement allowance and the +# widow's benefits have no input. +CAPPED_FAMILY_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ "child_benefit", "child_tax_credit", "jsa_income", "income_support", "esa_income", "universal_credit_pre_benefit_cap", +] + +# Capped benefits a person receives in their own right. Only the single person's +# or couple's count (UC Regs 2013 reg 79(1)). +CAPPED_PERSONAL_BENEFITS = [ "jsa_contrib", "incapacity_benefit", "esa_contrib", @@ -93,7 +99,13 @@ def universal_credit_after_benefit_cap(benunit, period): receive (UC reg 80(2A) counts it as nil in any case). The award is reduced by the excess minus the childcare costs element, and not at all where that element is greater than the excess (reg 81(1)-(2)).""" - capped = add(benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT) + person = benunit.members + capped = add(benunit, period, CAPPED_FAMILY_BENEFITS_EXCEPT_HOUSING_BENEFIT) + ( + benunit.sum( + add(person, period, CAPPED_PERSONAL_BENEFITS) + * person("is_claimant_or_partner", period) + ) + ) excess = max_(0, capped - benunit("uc_benefit_cap", period)) reduction = max_(0, excess - benunit("uc_childcare_element", period)) return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) From 9ae8f16db1a0316da6c5ca47b91a841d0c83d924 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:13:37 -0400 Subject: [PATCH 22/67] Test that a worker's HB band counts their investment income Main's case "Other investment income does not move a worker's band" pinned the stopgap that left investment income out of the band until the reg 74(2) remunerative-work test was modelled. With the test in place, a worker is banded on all gross income: 739.02 a week, the 2025-26 top band. Co-Authored-By: Claude Opus 5.5 --- ...benefits_individual_non_dep_deduction.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index effa50cf33..7120539a0b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -377,3 +377,22 @@ child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 101.35 * 52 + +- name: A worker's band counts their investment income + period: 2025 + absolute_error_margin: 0.01 + input: + housing_benefit_individual_non_dep_deduction_eligible: true + age: 30 + hours_worked: 1_950 # 37.5 hours a week + employment_income: 9_517 # just over 183 a week + other_investment_income: 28_912 # 556 a week + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + output: + # In remunerative work, banded on all gross income (reg 74(2)), 739.02 a + # week: the band from £577, 126.65 * 52. Banding the earnings alone would + # give 45.15 * 52 = 2,347.80. + household_benefits_individual_non_dep_deduction: 126.65 * 52 From 89ba1a1276428dee825c3e412cdfe624413abc26 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:21:13 -0400 Subject: [PATCH 23/67] Correct the Scottish mixed-scheme cases' person-level flags The 70-year-old applicant in each Scottish case is on Pension Credit, so their own council_tax_reduction_non_dep_deduction_exempt is true. The flag matters only for a non-dependant, so the cases now also assert who is eligible as a non-dependant. The deductions are unchanged. Reported by the #2017 session. The non-dependant property test's docstring now states that invariant 5 multiplies by the rent share (reg 74(5)) or the joint-liability share (Sch 1 para 8(5)), as its oracle already does. Co-Authored-By: Claude Opus 5.5 --- ...reduction_non_dep_deductions_mixed_schemes.yaml | 14 +++++++++++--- .../test_non_dependant_deduction_properties.py | 3 ++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml index 6cd1e3895c..171fac905c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml @@ -190,9 +190,13 @@ # the partner of an income-related ESA claimant not on UC, so no # deduction. Pension-age sharer (SSI 2012/319 reg 48): only the recipient # is exempt; the worker is banded on the couple's 400 a week, 10.95 * 52 - # = 569.40, half each. The person-level flags report the head's scheme. + # = 569.40, half each. The person-level flags report the head's scheme, + # so the worker is exempt there. The 70-year-old sharer is on Pension + # Credit, so their own flag is true too; it matters only for a + # non-dependant. council_tax_reduction_non_dep_deductions: [0, 284.70, 0] - council_tax_reduction_non_dep_deduction_exempt: [false, false, true, true] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true, true, true] - name: Scotland, pension-age head and working-age sharer, the partner rule only for the working-age claim period: 2026 @@ -237,5 +241,9 @@ council_tax: 2_000 savings: 0 output: + # Under the pension-age head's scheme, only the ESA recipient is exempt; + # the 70-year-old head's own flag is true because they are on Pension + # Credit. council_tax_reduction_non_dep_deductions: [284.70, 0, 0] - council_tax_reduction_non_dep_deduction_exempt: [false, false, true, false] + council_tax_reduction_individual_non_dep_deduction_eligible: [false, false, true, true] + council_tax_reduction_non_dep_deduction_exempt: [true, false, true, false] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 86eee1b8ec..132592d1de 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -26,7 +26,8 @@ the household, of their claimant and partner's higher amount (both amounts for a Welsh working-age applicant's non-dependant couple with a Universal Credit award) plus each other member's, recomputed here from the - individual amounts. + individual amounts, times the family's share of the rent (Housing Benefit, + reg 74(5)) or of the council tax liability (CTR, Sch 1 para 8(5)). 6. Monotonicity (metamorphic): raising any non-dependant's income never lowers an individual deduction or a family's deductions. 7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, From 58db77c0ebd2809e0249db899f377f699d47984f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Tue, 20 Oct 2026 22:29:51 -0400 Subject: [PATCH 24/67] Keep the household head out of the UC and HB in-unit sets (review r4) F1. Where a liable household head is not her benefit unit's claimant or partner, she applies for Council Tax Reduction alone (#2078), but the UC and HB in-unit helpers made her a non-dependant of her own benefit unit's couple. She is the householder in whose name the accommodation is rented: UC Sch 4 para 9(2)(d) and HB reg 3(2)(d)-(e) exclude her. Both helpers now exclude is_resolved_household_head. A YAML case pins UC and HB at 0 while CTR keeps the couple as her non-dependants. F2. uc_non_dep_deductions' documentation states that "makes a claim" is the claims_universal_credit flag, so a family flagged as claiming counts its own non-dependants even with a nil award, and says where other families' go. A YAML case pins a non-claiming lodger's son in the householder's claim. F5. The changelog names the household head rule, joint-occupier apportionment and the narrowed para 16(2)(b) Pension Credit limb. The property oracle checks all three in-unit sets (UC, HB, CTR); the generator now states the child's Child Benefit status, since inputs set for some members are inputs for all. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2014.fixed.md | 2 +- .../family/benefit_unit_non_dependants.yaml | 68 +++++++++++++++++++ ...t_benefit_unit_non_dependant_properties.py | 19 ++++-- ..._unit_non_dependant_for_legacy_benefits.py | 12 +++- ...unit_non_dependant_for_universal_credit.py | 15 +++- .../uc_non_dep_deductions.py | 14 ++-- 6 files changed, 115 insertions(+), 15 deletions(-) diff --git a/changelog.d/2014.fixed.md b/changelog.d/2014.fixed.md index b3c0b44145..7f69203350 100644 --- a/changelog.d/2014.fixed.md +++ b/changelog.d/2014.fixed.md @@ -1 +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). +- 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). Housing Benefit and Council Tax Reduction apportion every non-dependant between joint occupiers or jointly liable applicants, a lodger's adult son included, and a household head is never her own benefit unit's non-dependant. Apply the Universal Credit renter exemption from housing cost contributions (Sch 4 para 15), and give the para 16(2)(b) Pension Credit exemption only to the claimant or partner who receives it, so a pensioner's adult son is no longer exempt by her award. 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 153438eef8..b8da4762e7 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 @@ -1224,3 +1224,71 @@ uc_non_dep_receives_pension_credit: [false, true] uc_non_dep_deduction_exempt: [false, true, false] uc_non_dep_deductions: [1_158.60, 0] + +# The household head is the householder in whose name the accommodation is +# rented, so she is no one's non-dependant for UC (Sch 4 para 9(2)(d)) or HB +# (reg 3(2)(d)), even where her benefit unit's claimant and partner are the +# young couple. For CTR she applies alone and they are her non-dependants. +- name: UC and HB, a head who applies for CTR alone is not her benefit unit's non-dependant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 70 + is_household_head: true + is_claimant_or_partner: false + son: + age: 30 + is_claimant_or_partner: true + employment_income: 25_000 + weekly_hours: 37.5 + partner: + age: 29 + is_claimant_or_partner: true + benunits: + benunit: + members: [grandmother, son, partner] + households: + household: + members: [grandmother, son, partner] + rent: 12_000 + output: + is_benefit_unit_non_dependant_for_universal_credit: [false, false, false] + is_benefit_unit_non_dependant_for_legacy_benefits: [false, false, false] + is_benefit_unit_non_dependant_for_council_tax_reduction: [false, true, true] + uc_non_dep_deductions: [0] + housing_benefit_non_dep_deductions: [0] + +# Sch 4 para 9(2)(f): a family that makes no Universal Credit claim treats +# no one as its non-dependant, so a non-claiming lodger's adult son counts in +# the householder's claim. "Making a claim" is the model's would-claim flag. +- name: UC, a non-claiming lodger's adult son counts in the householder's claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + householder: + age: 40 + is_household_head: true + is_claimant_or_partner: true + lodger: + age: 39 + is_claimant_or_partner: true + rent_paid_as_lodger: 5_200 + lodger_son: + age: 25 + is_claimant_or_partner: false + benunits: + benunit_1: + members: [householder] + benunit_2: + members: [lodger, lodger_son] + would_claim_uc: false + households: + household: + members: [householder, lodger, lodger_son] + rent: 9_000 + output: + uc_non_dependants_counted: [true, false] + uc_non_dep_deductions: [1_158.60, 0] 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 8a2428b98f..df83ec5ba1 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -166,10 +166,13 @@ def build(population, convert=None): ) if fam["child"]: pid = f"{name}_child" + # Inputs set for some members are inputs for all, so the + # child's Child Benefit status is stated, not left to default. people[pid] = { "age": 8, "is_claimant_or_partner": False, "is_household_head": False, + "is_child_or_qualifying_young_person_for_child_benefit": True, } ids.append(pid) person_rows.append( @@ -398,13 +401,17 @@ def run(population, convert=None): 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. The members classed as in-unit non-dependants are exactly - # the generated non-QYP members. + # 1. Oracle. The members each scheme classes as in-unit non-dependants + # are exactly the generated non-QYP members. kinds = np.array([row["kind"] for row in person_rows]) - assert np.array_equal( - calc(sim, "is_benefit_unit_non_dependant_for_universal_credit") > 0, - kinds == "non_dependant", - ) + for in_unit_variable in ( + "is_benefit_unit_non_dependant_for_universal_credit", + "is_benefit_unit_non_dependant_for_legacy_benefits", + "is_benefit_unit_non_dependant_for_council_tax_reduction", + ): + assert np.array_equal( + calc(sim, in_unit_variable) > 0, kinds == "non_dependant" + ), in_unit_variable 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( 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 7819dc646c..7069ff4524 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 @@ -19,7 +19,13 @@ class is_benefit_unit_non_dependant_for_legacy_benefits(Variable): "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." + "CTR Sch 1 para 8(8)) never apply to them. The household head " + "(is_resolved_household_head), a householder in whose name the " + "accommodation is rented, is liable for it and so never a " + "non-dependant (HB reg 3(2)(d)-(e)), even where the benefit unit's " + "claimant and partner are someone else. Council Tax Reduction uses " + "is_benefit_unit_non_dependant_for_council_tax_reduction, built on " + "the CTR applicant." ) definition_period = YEAR reference = ( @@ -43,4 +49,6 @@ def formula(person, period, parameters): child_or_young_person = person( "is_child_or_qualifying_young_person_for_child_benefit", period ) - return ~claimant_or_partner & ~child_or_young_person + # Reg 3(2)(d)-(e): the household head is liable for the dwelling. + household_head = person("is_resolved_household_head", period) + return ~claimant_or_partner & ~child_or_young_person & ~household_head 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 index bc6b71d000..80dcda54e2 100644 --- 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 @@ -16,7 +16,11 @@ class is_benefit_unit_non_dependant_for_universal_credit(Variable): "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." + "para 9(2)(g). A child, under 16, is always one or the other. The " + "household head (is_resolved_household_head), a householder in whose " + "name the accommodation is rented, is liable for it and so never a " + "non-dependant (para 9(2)(d)), even where the benefit unit's " + "claimant and partner are someone else." ) definition_period = YEAR reference = ( @@ -33,4 +37,11 @@ def formula(person, period, parameters): qualifying_young_person = person( "is_qualifying_young_person_for_universal_credit", period ) - return not_a_child & ~claimant_or_partner & ~qualifying_young_person + # Para 9(2)(d): the household head is liable for the accommodation. + household_head = person("is_resolved_household_head", period) + return ( + not_a_child + & ~claimant_or_partner + & ~qualifying_young_person + & ~household_head + ) 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 000f387d1d..9f06dbb16d 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 @@ -6,10 +6,16 @@ class uc_non_dep_deductions(Variable): 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." + "Housing cost contributions for the renter's non-dependants. Those in " + "other families of the household count in one claim, the one " + "uc_non_dependants_counted picks (the household head's family if it " + "claims, otherwise the first claiming sharer). Those within a " + "renter's own benefit unit count in that family's claim if it makes " + "one, otherwise in the same counted claim. Making a claim is the " + "model's claims_universal_credit flag for a renting, UC-eligible " + "family, so a family flagged as claiming counts its own non-dependants " + "even where its award is nil. None if the renter or a joint renter is " + "exempt." ) definition_period = YEAR unit = GBP From 4a0935b737cd71231e39340689b2148125f585c9 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 00:23:37 -0400 Subject: [PATCH 25/67] Never charge a family that bears a share of the rent as a non-dependant A family liable for a share of the rent is a joint occupier, not a non-dependant (HB Regs 2006 reg 3(2)(d)). is_non_dependant_of_household_head tests the family's own rent, which is nil whenever the household's is, so a sharer of a nil rent was both a non-dependant and a bearer of a share. Main's aggregation charged that family part of its own deduction. This PR's never charges a family for its own members, so the household's total fell short of the pool times the shares borne. The CI example (Hypothesis, derandomized) is a council tenancy at nil rent with a sharer: HB deductions 510.90 against a pool of 1,021.80 borne in full. The sharer is now left out of the pool, as in the HB aggregation itself. HB is nil at nil rent, so no award changes. A YAML case pins it, and main's sharers property test treats a sharer as never a non-dependant for the HB pool. Both the replayed CI example and the YAML case fail on the previous head and pass here. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 31 +++++++++++++++++++ ...sharers_boarders_and_lodgers_properties.py | 11 +++++-- .../housing_benefit_non_dep_deductions.py | 14 +++++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index c6adec9f83..845e7e1777 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -98,6 +98,37 @@ # Only person 3 is a non-dependant of the head's family: 3. housing_benefit_non_dep_deductions: [3, 0, 0] +- name: A sharer of a nil rent is not a non-dependant + period: 2025 + absolute_error_margin: 0 + input: + people: + person_1: + age: 40 + is_household_head: true + household_benefits_individual_non_dep_deduction: 0 + person_2: + age: 40 + household_benefits_individual_non_dep_deduction: 2 + benunits: + benunit_1: + members: [person_1] + housing_benefit_non_dep_deductions_claimant_exempt: false + benunit_2: + members: [person_2] + liable_for_share_of_household_rent: true + housing_benefit_non_dep_deductions_claimant_exempt: false + households: + household: + members: [person_1, person_2] + rent: 0 + output: + # Both families are liable for the rent, so each bears half. With a nil + # rent neither has any, but the sharer is still a joint occupier, not a + # non-dependant (HB Regs 2006 reg 3(2)(d)): no deduction for either. + share_of_household_rent: [0.5, 0.5] + housing_benefit_non_dep_deductions: [0, 0] + # The review case on #2184: an existing claimant in council housing with an # adult child whose only income is £28,912 of other investment income and who # does no paid work. The bands apply only in remunerative work (HB Regs 2006 diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index c770dc202b..0162003f6b 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -22,7 +22,8 @@ liable for the rent sum to the household's non-dependant deductions (one per couple, the higher: HB Regs 2006 reg 74(3)) times the share of the rent borne by families whose claimant or partner is not exempt (reg - 74(6)), and never exceed the sum of individual deductions; a + 74(6)), and never exceed the sum of individual deductions, with a sharer + never a non-dependant, even where the rent is nil (reg 3(2)(d)); a boarder's or lodger's family has no Universal Credit or Housing Benefit deductions; no sharer, boarder or lodger is a Council Tax Reduction non-dependant. @@ -280,8 +281,12 @@ def test_non_dependants(population): assert np.all(per_household(sim, uc) <= total + 0.01) # Housing Benefit: apportioned over the families liable for the rent. hb = calc(sim, "housing_benefit_non_dep_deductions") - hb_individual = ( - calc(sim, "household_benefits_individual_non_dep_deduction") * non_dependant + # A sharer's family is a joint occupier, never a non-dependant (HB Regs + # 2006 reg 3(2)(d)), even where the rent is nil and so the family is not + # benunit_is_rent_liable. + sharer = sim.map_result((roles == "sharer").astype(float), "benunit", "person") > 0 + hb_individual = calc(sim, "household_benefits_individual_non_dep_deduction") * ( + non_dependant & ~sharer ) hb_total = sim.map_result(hb_individual, "person", "household") assert hb_total.max() > 0 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index d4c3b27483..40710ed628 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -33,10 +33,18 @@ def formula(benunit, period, parameters): # occupier is apportioned between them by their shares of the # payments (reg 74(5); SPC reg 55(5)), so each family liable for the # household's rent bears its share; a boarder or lodger bears none. + # A family bearing a share is a joint occupier, never a + # non-dependant, even where the rent is nil and so the family is not + # benunit_is_rent_liable. person = benunit.members - deductions = person( - "household_benefits_individual_non_dep_deduction", period - ) * person("is_non_dependant_of_household_head", period) + joint_occupier = person.benunit("share_of_household_rent", period) > 0 + non_dependant = ( + person("is_non_dependant_of_household_head", period) & ~joint_occupier + ) + deductions = ( + person("household_benefits_individual_non_dep_deduction", period) + * non_dependant + ) deduction_for_benunit = deduction_per_family(benunit, period, deductions, False) share = benunit("share_of_household_rent", period) claimant_exempt = benunit( From 2e37b1e761a8f6d1fa7f90b8a6f8deb316152f5b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 00:36:01 -0400 Subject: [PATCH 26/67] Restate main's household-head agreement property for in-unit non-dependants (review r4) F7. #2062's test_every_programme_reads_one_head pinned three rules that assume no non-dependants inside a benefit unit; on this branch it failed deterministically (a 19-year-old presumed the 82-year-old head's child is an in-unit CTR non-dependant, as SI 2012/2885 reg 9(2)(a) gives). Restated: - UC deductions fall on the counting claim, or on a family's own claim for its in-unit non-dependants; - HB deductions only for families with a share of the rent, or a boarder or lodger with a non-dependant in its own benefit unit; - no one in the adult head's family is a CTR non-dependant except a member outside the applicant's family. F6. The property generator now draws would_claim_uc per family, so the in-unit routing's claim factor is exercised (its mutant now fails the oracle); a YAML case pins a non-claiming head whose son counts in a claiming sharer's claim. Co-Authored-By: Claude Opus 5.5 --- .../family/benefit_unit_non_dependants.yaml | 34 +++++++++++++++++++ ...t_benefit_unit_non_dependant_properties.py | 5 ++- ...est_household_head_agreement_properties.py | 34 ++++++++++++++----- 3 files changed, 64 insertions(+), 9 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 3c61bdbcf9..1edd5322e9 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 @@ -1327,3 +1327,37 @@ output: share_of_household_rent: [0.5, 0.5] housing_benefit_non_dep_deductions: [500, 500] + +# Sch 4 para 9(2)(f): the head's family is flagged not to claim Universal +# Credit, so its adult son counts in the jointly liable sharer's claim. +- name: UC, a non-claiming head's adult son counts in a claiming sharer's claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 50 + is_household_head: true + is_claimant_or_partner: true + employment_income: 150_000 + weekly_hours: 37.5 + son: + age: 25 + is_claimant_or_partner: false + sharer: + age: 45 + is_claimant_or_partner: true + benunits: + benunit_1: + members: [head, son] + would_claim_uc: false + benunit_2: + members: [sharer] + liable_for_share_of_household_rent: true + households: + household: + members: [head, son, sharer] + rent: 12_000 + output: + uc_non_dependants_counted: [false, true] + uc_non_dep_deductions: [0, 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 df83ec5ba1..bf211f4a82 100644 --- a/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py +++ b/policyengine_uk/tests/test_benefit_unit_non_dependant_properties.py @@ -85,6 +85,9 @@ def family(draw, role): extras=draw(st.lists(extra_member(), min_size=0, max_size=2)), child=draw(st.booleans()), renter_pip=draw(st.booleans()), + # Take-up: a family flagged not to claim Universal Credit passes its + # in-unit non-dependants to the claim that counts the household's. + would_claim_uc=draw(st.booleans()), payment=draw(st.floats(1_000, 10_000, allow_nan=False)), ) @@ -184,7 +187,7 @@ def build(population, convert=None): disability_benefit=False, ) ) - benunit = {"members": ids} + benunit = {"members": ids, "would_claim_uc": fam["would_claim_uc"]} if fam["role"] == "sharer": benunit["liable_for_share_of_household_rent"] = True if fam["role"] == "lodger": diff --git a/policyengine_uk/tests/test_household_head_agreement_properties.py b/policyengine_uk/tests/test_household_head_agreement_properties.py index 4862578ad5..26135828ed 100644 --- a/policyengine_uk/tests/test_household_head_agreement_properties.py +++ b/policyengine_uk/tests/test_household_head_agreement_properties.py @@ -28,13 +28,17 @@ if and only if their family is neither the head's nor liable for rent. - Universal Credit non-dependants: at most one family in a household counts them (uc_non_dependants_counted; UC Regs 2013 Sch 4 para - 9(2)(f)). Pin: deductions fall only on that family's claim, and where - the head's family is liable for the rent and claims Universal Credit, - it is the head's. Only families with a share of the rent have Housing - Benefit non-dependant deductions. + 9(2)(f)). Pin: other families' non-dependants fall only on that + family's claim, and a family's own in-unit non-dependants on its own + claim; where the head's family is liable for the rent and claims + Universal Credit, the counting claim is the head's. Only families with + a share of the rent have Housing Benefit non-dependant deductions, + except a boarder or lodger for a non-dependant in its own benefit unit. - Council Tax Reduction: only the head's family and sharers claim; where the head is 18 or over, the head's family claims and no one in it is a - non-dependant. + non-dependant except a member who is outside the applicant's family + (is_benefit_unit_non_dependant_for_council_tax_reduction; SI 2012/2885 + reg 9(2)(a)). - A family paying the householder as boarders or lodgers is never the head's, and the maintenance loan never has the head living with parents. 4. Metamorphic: every value the simulation computes, other than the @@ -395,7 +399,11 @@ def test_every_programme_reads_one_head(population): # counts the household's non-dependants: the head's family's wherever it # is liable for the rent and claims. counted = calc(sim, "uc_non_dependants_counted") - assert np.all(counted[calc(sim, "uc_non_dep_deductions") > 0]) + # A family's own in-unit non-dependants count in its own claim. + has_uc_in_unit = any_in_benunit( + calc(sim, "is_benefit_unit_non_dependant_for_universal_credit"), facts + ) + assert np.all((counted | has_uc_in_unit)[calc(sim, "uc_non_dep_deductions") > 0]) household_of_benunit = np.zeros(head_family.size, dtype=int) household_of_benunit[benunit_of] = facts["household"] assert np.all(sum_in_household(counted, household_of_benunit) <= 1) @@ -409,7 +417,16 @@ def test_every_programme_reads_one_head(population): in_household = head_claims_uc[household_of_benunit] assert np.array_equal(counted[in_household], head_family[in_household]) # Housing Benefit non-dependant deductions. - assert np.all(calc(sim, "housing_benefit_non_dep_deductions")[share == 0] == 0) + # A boarder or lodger bears a non-dependant in its own benefit unit. + has_legacy_in_unit = any_in_benunit( + calc(sim, "is_benefit_unit_non_dependant_for_legacy_benefits"), facts + ) + assert np.all( + calc(sim, "housing_benefit_non_dep_deductions")[ + (share == 0) & ~has_legacy_in_unit + ] + == 0 + ) # Council Tax Reduction. claimant = calc(sim, "council_tax_reduction_claimant_benunit") assert not np.any(claimant & ~head_family & ~sharer) @@ -418,7 +435,8 @@ def test_every_programme_reads_one_head(population): ctr_non_dependant = calc( sim, "council_tax_reduction_individual_non_dep_deduction_eligible" ) - assert not np.any(ctr_non_dependant & adult_head_family[benunit_of]) + ctr_in_unit = calc(sim, "is_benefit_unit_non_dependant_for_council_tax_reduction") + assert not np.any(ctr_non_dependant & adult_head_family[benunit_of] & ~ctr_in_unit) # Boarders and lodgers; the maintenance loan. assert not np.any(calc(sim, "pays_rent_to_householder") & person_head_family) arrangement = calc(sim, "maintenance_loan_living_arrangement") From 3efa8bd6082f9d7bf32d6351f21f1d729327b2e3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 21 Oct 2026 02:06:54 -0400 Subject: [PATCH 27/67] Restate main's CTR claimant property for in-unit non-dependants The full Python suite at 2e37b1e76 failed test_claimant_invariants: it pinned that no member of a sharer family is a CTR non-dependant, but an 18-year-old presumed the 66-year-old sharer's child, not in education, is outside the sharer's family and a non-dependant (SI 2012/2885 reg 9(1), (2)(a)). The assertion now excludes is_benefit_unit_non_dependant_for_ council_tax_reduction members, and "no claimant or partner is a non-dependant" now reads #2078's applicant identity, so a head applying alone may have her benefit unit's couple as non-dependants. Co-Authored-By: Claude Opus 5.5 --- ...council_tax_reduction_claimant_properties.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py b/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py index 6424555e09..214ad859bb 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_claimant_properties.py @@ -33,10 +33,13 @@ 3. Simulated reductions: no family outside the claimants gets one, and a household's total never exceeds its council tax. (Reported reductions, used where a scheme is not modelled, are outside this test.) -4. Non-dependants: no claimant or partner of a claimant family is a - non-dependant (the applicant's family, SI 2012/2885 reg 9(2)(a)). Pin: no - member of a sharer family is one, and every adult outside the claimant - families, the sharer families and boarders' and lodgers' families is one. +4. Non-dependants: no applicant or partner of a claimant family + (is_council_tax_reduction_applicant_or_partner) is a non-dependant (the + applicant's family, SI 2012/2885 reg 9(2)(a)). Pin: no member of a sharer + family is one except a member outside its applicant's family + (is_benefit_unit_non_dependant_for_council_tax_reduction), and every + adult outside the claimant families, the sharer families and boarders' + and lodgers' families is one. 5. Differential: where the input flags exactly one member of a household, or no household in the simulation has the input (so the flag's formula picks the eldest), the family holding the head is the one holding the @@ -314,8 +317,10 @@ def test_claimant_invariants(population): person_sharer = sharer[facts["benunit"]] lodger = calc(sim, "pays_rent_to_householder") adult = age >= 18 - assert not np.any(non_dep & person_claimant & claimant_or_partner) - assert not np.any(non_dep & person_sharer) + applicant_or_partner = calc(sim, "is_council_tax_reduction_applicant_or_partner") + in_unit = calc(sim, "is_benefit_unit_non_dependant_for_council_tax_reduction") + assert not np.any(non_dep & person_claimant & applicant_or_partner) + assert not np.any(non_dep & person_sharer & ~in_unit) assert np.all(non_dep[adult & ~person_claimant & ~person_sharer & ~lodger]) # 5. Differential with the person-level household head flag. Once anyone in From 4515cc8fd642d0ee02969abcde3f9f6f07fb2dad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 21:35:15 -0400 Subject: [PATCH 28/67] Bring main's non-dependant tests to each scheme's own law #2620's gate cases, gross-income cases and the national CTR maintenance test now expect each CTR scheme's own scale, the flat (1)(b) amount for non-workers and the exemptions; the PR's own cases band the programme gross-income variables instead of total_income, which main now defines as the Income Tax Step 1 total. The property tests read #2027's is_on_* status, check banding against gross income built from the inputs, and check the after-cap Universal Credit against universal_credit. Co-Authored-By: Claude Opus 5.5 --- ...benefits_individual_non_dep_deduction.yaml | 31 +- .../housing_benefit_non_dep_gross_income.yaml | 3 +- ...ing_benefit_non_dep_remunerative_work.yaml | 20 +- ...sion_age_severe_disability_conditions.yaml | 16 +- .../claimant_household_head.yaml | 5 +- ...il_tax_reduction_non_dep_gross_income.yaml | 96 ++-- ...x_reduction_non_dep_remunerative_work.yaml | 419 +++++++++++++++--- .../test_housing_benefit_non_dep_history.py | 2 +- ...test_non_dependant_deduction_properties.py | 197 +++++++- .../test_non_dependant_spousal_maintenance.py | 56 ++- .../non_dep_deduction/_non_dependants.py | 11 +- 11 files changed, 683 insertions(+), 173 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 7feffae878..6ecfaa2b49 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -328,9 +328,18 @@ council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week employment_income: 5_200 # £100 a week, in both programmes' gross income + age: 30 + current_education: NOT_IN_EDUCATION + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + country: WALES output: household_benefits_individual_non_dep_deduction: 793 - council_tax_reduction_individual_non_dep_deduction: 793 + # Council Tax Reduction has its own scale: Wales's lowest amount, £4.35 a + # week from 1 April 2018 (WSI 2018/14), £4.20 before it. + council_tax_reduction_individual_non_dep_deduction: 4.35 * 52 - name: Reduced rent without LHA period: 2025 @@ -338,6 +347,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 5_200 # 100 weekly + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 19.65 * 52 @@ -347,6 +360,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 30_056 # 578 weekly + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 126.65 * 52 @@ -356,6 +373,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 18_148 # 349 weekly + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 101.35 * 52 @@ -365,6 +386,10 @@ housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 # 16 hours a week housing_benefit_non_dep_gross_income: 9_517 # just over 183 weekly, the lower edge of the second band + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: household_benefits_individual_non_dep_deduction: 45.15 * 52 @@ -373,6 +398,10 @@ input: housing_benefit_individual_non_dep_deduction_eligible: true other_investment_income: 28_912 # 556 weekly, no hours + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 output: # Not in remunerative work, so reg 74(1)(b)'s amount whatever the income: # 19.65 * 52 = 1,021.80. Banding the £556 would give 115.45 * 52 = diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index a890f3a21e..e186f9be3a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -4,8 +4,7 @@ # Sch 1 para 3 and Sch 6 para 5. Gross income includes statutory pay and # maintenance; the listed disability payments are disregarded. # Hand arithmetic: wages 9,000 + statutory pay 520 = 9,520, just above -# 183/week. The existing national consumers use the HB scale (45.15/week -# at this income in 2025); correcting CTR rates is separate (#2007). +# 183/week, in the HB band from £183 (45.15/week in 2025). # Annual wages and statutory pay can relate to different parts of the year; # leave days themselves are not remunerative work (HB reg 6(7)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml index bb515785bf..c5d3406773 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml @@ -1,9 +1,8 @@ # HB Regs 2006 reg 74(1)(b), (2) and reg 6(1), (6); pension-age # HB Regs 2006 reg 55(1)(b), (2) and reg 6(1), (6). -# A non-worker or 15-hour worker with banding income pays 20.40 * 52 = -# 1,060.80 in 2026-27. One with no banding income keeps the scale's zero: -# the gate only lowers a deduction until #2007 models the exemptions in -# reg 74(6), (7), (8) and (10) and reg 55(6)-(9) with the flat charge. +# A non-worker or 15-hour worker pays 20.40 * 52 = 1,060.80 in 2026-27, +# whatever their income. The exemptions in reg 74(7), (8) and (10) and reg +# 55(7)-(9) act through eligibility, which most cases here take as an input. # A 16-hour worker's wages of 9,000 alone are below 192/week; adding # IIDB or child maintenance of 12,000 gives 403.85/week, so 105.20 * 52. @@ -175,7 +174,7 @@ output: household_benefits_individual_non_dep_deduction: 1060.8 -- name: housing_benefit keeps a non-worker with no banding income at zero (nil income) +- name: housing_benefit charges the flat amount to a non-worker with nil income period: 2026 absolute_error_margin: 0.01 input: @@ -188,24 +187,25 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - # Reg 74(1)(b) gives a non-exempt non-worker 1,060.80, but the zero is - # kept until #2007 models the exemptions alongside the flat charge. - household_benefits_individual_non_dep_deduction: 0 + # Reg 74(1)(b) gives a non-worker the flat amount whatever their income: + # 20.40 * 52. + household_benefits_individual_non_dep_deduction: 1060.8 - name: housing_benefit charges no new deduction to a non-dependant under 25 on Income Support # Reg 74(8)(a): no deduction for a non-dependant under 25 on Income Support. - # Income Support is not banding income, so the gate leaves the zero. + # The exemption makes them ineligible for a deduction. period: 2026 absolute_error_margin: 0.01 input: age: 22 - housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 0 social_security_income: 0 is_on_income_support: true is_on_income_based_jsa: false is_on_income_related_esa: false output: + housing_benefit_non_dep_deduction_exempt: true + housing_benefit_individual_non_dep_deduction_eligible: false household_benefits_individual_non_dep_deduction: 0 - name: housing_benefit excludes each income-related benefit recipient from remunerative work (Income Support) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml index a6a1b2bde1..88cc95616c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml @@ -1,8 +1,8 @@ # HB(SPC) Regs 2006 Sch 3 para 6, with the non-dependant definition in # reg 3: these cases assert final awards without a Guarantee Credit passport. -# The final HB and CTR awards include the model's current income rules, -# including a known defect outside this file: no non-dependant deduction -# exemption for a claimant on Attendance Allowance (#2007). The premium and +# The final HB and CTR awards include the model's current income rules. +# A claimant on Attendance Allowance has no non-dependant deductions (HB +# (SPC) Regs 2006 reg 55(6); SI 2012/2885 Sch 1 para 8(6)); the premium and # applicable-amount assertions do not depend on it. Each HB award is the # rent less 65% of income after tax, plus the savings credit where the award # is savings credit only (HB (SPC) Regs 2006 reg 27(4)(a)), above the @@ -129,9 +129,8 @@ guarantee_credit: [0, 0] has_non_dependant_for_severe_disability_premium: [true, false] pension_age_severe_disability_premium: [0, 0] - # The resident has no banding income, so the work gate leaves the - # scale's zero; reg 55(6) and Sch 1 para 8(6) exempt this claimant's - # non-dependants in any case (the AA exemption remains for #2007). + # Reg 55(6) and Sch 1 para 8(6) exempt this claimant's non-dependants: + # the claimant receives Attendance Allowance. housing_benefit: [3_898.70, 0] council_tax_benefit: [1_279.60, 0] @@ -175,9 +174,8 @@ # Savings credit only (908.46): HB income is Pension Credit income plus # the savings credit (HB (SPC) Regs 2006 reg 27(4)(a)), 0.65 x 908.46 = # 590.50 below the figure without it. - # Neither non-working resident has banding income, so the work gate - # leaves the scale's zero for both; the AA exemption (reg 55(6), Sch 1 - # para 8(6)) remains for #2007. + # The AA exemption (reg 55(6), Sch 1 para 8(6)) leaves no deduction for + # either resident. housing_benefit: [3_308.20, 0] council_tax_benefit: [1_097.91, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml index 720ebe1016..dd3a9e2b63 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_household_head.yaml @@ -718,9 +718,8 @@ # student shares liability with a non-student, the non-student is solely # liable (LGFA 1992 s.75(4)), so the co-tenant claims on the whole bill # and the head is the co-tenant's non-dependant. No deduction is made for - # a full-time student non-dependant (reg 90(7)(c)). That exemption - # remains for #2007; the model's national deduction is nil here because - # the head has no banding income, which the work gate leaves at zero. + # a full-time student non-dependant (reg 90(7)(c)), so the co-tenant's + # reduction covers the whole bill. council_tax_reduction_liable_person: [true, true] council_tax_reduction_claimant_benunit: [false, true] council_tax_reduction_joint_liability_share: [0.5, 1] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index ea70a23fdd..3e4f20ac4d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -4,8 +4,10 @@ # Sch 1 para 3 and Sch 6 para 5. Gross income includes statutory pay and # maintenance; the listed disability payments are disregarded. # Hand arithmetic: wages 9,000 + statutory pay 520 = 9,520, just above -# 183/week. The existing national consumers use the HB scale (45.15/week -# at this income in 2025); correcting CTR rates is separate (#2007). +# 183/week. Each national scheme bands it on its own 2025-26 scale, below +# the first band's lower edge, so the non-dependant pays the scheme's lowest +# amount: England 5.00 (SI 2025/39), Scotland 5.35 (SSI 2025/24) and Wales +# 6.25 (WSI 2025/59) a week. # Annual wages and statutory pay can relate to different parts of the year; # leave days themselves are not remunerative work (HB reg 6(7)). @@ -50,9 +52,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 260] + council_tax_reduction_non_dep_deductions: [260, 0] + simulated_council_tax_reduction_benunit: [3_740, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with ENGLAND claimant aged 70 period: 2025 @@ -95,9 +97,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 260] + council_tax_reduction_non_dep_deductions: [260, 0] + simulated_council_tax_reduction_benunit: [3_740, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with ENGLAND claimant aged 70 period: 2025 @@ -140,9 +142,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 260] + council_tax_reduction_non_dep_deductions: [260, 0] + simulated_council_tax_reduction_benunit: [3_740, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with SCOTLAND claimant aged 40 period: 2025 @@ -185,9 +187,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with SCOTLAND claimant aged 40 period: 2025 @@ -230,9 +232,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with SCOTLAND claimant aged 40 period: 2025 @@ -275,9 +277,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with SCOTLAND claimant aged 70 period: 2025 @@ -320,9 +322,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with SCOTLAND claimant aged 70 period: 2025 @@ -365,9 +367,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with SCOTLAND claimant aged 70 period: 2025 @@ -410,9 +412,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 278.20] + council_tax_reduction_non_dep_deductions: [278.20, 0] + simulated_council_tax_reduction_benunit: [3_721.80, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with WALES claimant aged 40 period: 2025 @@ -455,9 +457,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with WALES claimant aged 40 period: 2025 @@ -500,9 +502,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with WALES claimant aged 40 period: 2025 @@ -545,9 +547,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_sick_pay with WALES claimant aged 70 period: 2025 @@ -590,9 +592,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_maternity_pay with WALES claimant aged 70 period: 2025 @@ -635,9 +637,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income includes statutory_paternity_pay with WALES claimant aged 70 period: 2025 @@ -680,9 +682,9 @@ council_tax_reduction_maximum_eligible_liability: 4_000 output: council_tax_reduction_non_dep_gross_income: [0, 9_520] - council_tax_reduction_individual_non_dep_deduction: [0, 2_347.80] - council_tax_reduction_non_dep_deductions: [2_347.80, 0] - simulated_council_tax_reduction_benunit: [1_652.20, 0] + council_tax_reduction_individual_non_dep_deduction: [0, 325] + council_tax_reduction_non_dep_deductions: [325, 0] + simulated_council_tax_reduction_benunit: [3_675, 0] - name: council_tax_reduction_non_dep_gross_income counts tax-exempt maintenance and periodic benefits period: 2025 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index 4c9caa26cc..2cd2277ab9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -1,19 +1,28 @@ # England SI 2012/2885 Sch 1 para 8 and reg 10; Scotland SSI 2021/249 # reg 90 and reg 10, SSI 2012/319 reg 48 and reg 6; Wales WSI 2013/3029 # Sch 1 para 3, Sch 6 para 5 and reg 10: income banding requires remunerative -# work. This scoped fix retains the consumer's inherited HB scale: the -# 2026-27 flat amount is 20.40/week and 403.85/week of income gives 105.20. -# Actual national CTR flat amounts are England 5.20, Scotland 5.55, Wales -# 6.70; their scales remain for #2007. These are work-gate regressions. A -# non-worker with no banding income keeps the scale's zero: the gate only -# lowers a deduction until #2007 models the exemptions in paragraphs (6) to -# (8) with the flat charge. +# work. Each scheme's 2026-27 scale: a non-dependant outside remunerative work +# pays the lowest amount (England 5.20, SI 2026/27; Scotland 5.55, SSI +# 2026/56; Wales 6.70, WSI 2026/13), and 403.85/week of income in work falls +# in the band from £279 in England (10.60) and Wales (13.35) and from £286 in +# Scotland (10.95). Family awards, which a single non-dependant's gross income +# would count, are held at zero; English cases take the pension-age scheme, +# the only national one in England. - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -27,12 +36,21 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -46,12 +64,21 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -65,12 +92,21 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -84,12 +120,21 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -103,12 +148,21 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -122,12 +176,21 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -141,12 +204,21 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 country: ENGLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -160,12 +232,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -179,12 +259,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -198,12 +286,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -217,12 +313,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -236,12 +340,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -255,12 +367,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -274,12 +394,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -293,12 +421,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: SCOTLAND council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: SCOTLAND council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -312,12 +448,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -331,12 +475,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -350,12 +502,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -369,12 +529,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: WALES council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -388,12 +556,20 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -407,12 +583,20 @@ output: weekly_hours: 0 council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -426,12 +610,20 @@ output: weekly_hours: 15 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -445,12 +637,20 @@ output: weekly_hours: 16 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: WALES council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: WALES council_tax_reduction_individual_non_dep_deduction_eligible: true @@ -464,12 +664,21 @@ output: weekly_hours: 37.5 council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 - name: council_tax_reduction charges the flat amount on a non-worker's pension or nil income (pension income) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 70 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -479,12 +688,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 -- name: council_tax_reduction keeps a non-worker with no banding income at zero (nil income) +- name: council_tax_reduction charges the flat amount to a non-worker with nil income period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 70 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -494,14 +712,15 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - # Paragraph (1)(b) gives a non-exempt non-worker the flat amount, but the - # zero is kept until #2007 models the exemptions alongside the flat charge. - council_tax_reduction_individual_non_dep_deduction: 0 + # Paragraph (1)(b) gives a non-worker the flat amount whatever their + # income, here 5.20 * 52; Pension Credit, which would exempt them, is held + # at zero. + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: council_tax_reduction charges no new deduction to a non-dependant on Universal Credit without earnings # SI 2012/2885 Sch 1 para 8(8)(c): no deduction for a non-dependant - # entitled to Universal Credit with no earned income. Universal Credit is - # not banding income, so the gate leaves the zero. + # entitled to Universal Credit with no earned income. The family is + # pension-age for CTR, so England's national scheme applies. period: 2026 absolute_error_margin: 0.01 input: @@ -517,7 +736,8 @@ benunits: non_dependant_unit: members: [non_dependant] - universal_credit: 6_000 + universal_credit_pre_benefit_cap: 6_000 + council_tax_reduction_pensioner: true households: household: members: [non_dependant] @@ -526,12 +746,20 @@ council_tax_reduction_individual_non_dep_deduction: [0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (Income Support) - # The recipient is not in remunerative work, so the flat amount applies - # rather than a band. SI 2012/2885 Sch 1 para 8(8)(a) exempts this - # non-dependant altogether; that exemption remains for #2007. + # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and + # Sch 1 para 8(8)(a) exempts them altogether: no deduction. period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -542,15 +770,23 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 0 - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-based JSA) - # The recipient is not in remunerative work, so the flat amount applies - # rather than a band. SI 2012/2885 Sch 1 para 8(8)(a) exempts this - # non-dependant altogether; that exemption remains for #2007. + # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and + # Sch 1 para 8(8)(a) exempts them altogether: no deduction. period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -561,15 +797,23 @@ is_on_income_based_jsa: true is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 0 - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-related ESA) - # The recipient is not in remunerative work, so the flat amount applies - # rather than a band. SI 2012/2885 Sch 1 para 8(8)(a) exempts this - # non-dependant altogether; that exemption remains for #2007. + # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and + # Sch 1 para 8(8)(a) exempts them altogether: no deduction. period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -580,12 +824,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: true output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 0 - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (no excluded benefit) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true age: 30 council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -596,12 +849,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND @@ -614,12 +876,21 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 270.40 - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND @@ -632,12 +903,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 551.20 - name: SCOTLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.scotland.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: SCOTLAND @@ -650,12 +929,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 288.60 - name: SCOTLAND council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.scotland.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: SCOTLAND @@ -668,12 +955,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 569.40 - name: WALES council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.wales.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: WALES @@ -686,12 +981,20 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 1060.8 + council_tax_reduction_individual_non_dep_deduction: 348.40 - name: WALES council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.local_authorities.wales.council_tax_reduction.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: WALES @@ -704,4 +1007,4 @@ is_on_income_based_jsa: false is_on_income_related_esa: false output: - council_tax_reduction_individual_non_dep_deduction: 5470.4 + council_tax_reduction_individual_non_dep_deduction: 694.20 diff --git a/policyengine_uk/tests/test_housing_benefit_non_dep_history.py b/policyengine_uk/tests/test_housing_benefit_non_dep_history.py index 47d4e369b0..0c45be6875 100644 --- a/policyengine_uk/tests/test_housing_benefit_non_dep_history.py +++ b/policyengine_uk/tests/test_housing_benefit_non_dep_history.py @@ -42,7 +42,7 @@ def test_all_historical_scale_cells(year, thresholds, amounts): # SI 2015/457 art 18(3); SI 2016/242 reg 5; SI 2017/260 art 22(4); # SI 2018/281 arts 1(2)(h), 23(3) and 24(3). - # Test the parameter itself, not the separate #2007 eligibility/banding fix. + # Test the parameter itself, not eligibility or banding. scale = load_parameter_file(str(PATH), "hb_non_dep_scale")(f"{year}-04-30") assert scale.thresholds == thresholds assert scale.amounts == amounts diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 132592d1de..f59085ce4d 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -45,12 +45,28 @@ Sch 1 para 3). In Scotland's working-age scheme from April 2022, also anyone whose partner is on IS, JSA(IB) or ESA(IR) and who is not on Universal Credit (SSI 2021/249 reg 90(8)(a), reg 4(1)). IS, JSA(IB), - ESA(IR) and SPC count only for the person they are payable to: the member - who reports them, or the benefit unit's head. Universal Credit counts for - both joint claimants. Earned income includes statutory sick, maternity and - paternity pay, less relievable pension contributions, which come off - self-employed earnings only when there are no employed earnings; a - self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). + ESA(IR) and SPC count only for the person they are payable to, read from + the model's is_on_* status (HB Regs 2006 reg 2(1), (3), (3A)); where the + claimant or partner reports the award, or no one does, that is the + reporter or the benefit unit's head, which the test checks. Universal + Credit counts for both joint claimants. Earned income includes statutory + sick, maternity and paternity pay, less relievable pension contributions, + which come off self-employed earnings only when there are no employed + earnings; a self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). +10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) +11. Banding: a non-dependant in remunerative work who is neither exempt nor + on IS, JSA(IB) or ESA(IR) pays the amount of the band whose lower edge is + the highest not above their normal gross weekly income, computed here from + the generated inputs: earnings, statutory sick, maternity and paternity + pay, self-employed profit with a loss as nil, and other investment income; + for the claimant or partner of the non-dependant's family, the couple's + sum plus the family's IS, JSA(IB), ESA(IR), Pension Credit and Universal + Credit. Incomes within a penny a week of an edge are skipped, since the + inputs are stored as float32. +12. Differential: the after-cap Universal Credit used in a non-dependant + family's gross income equals the model's universal_credit for a family + with no Housing Benefit, no Maternity Allowance and no deductions from + its award. """ from pathlib import Path @@ -109,6 +125,12 @@ "esa_income": "esa_income_reported", "pension_credit": "pension_credit_reported", } +IS_ON = { + "income_support": "is_on_income_support", + "jsa_income": "is_on_income_based_jsa", + "esa_income": "is_on_income_related_esa", + "pension_credit": "is_on_pension_credit", +} STATUTORY_PAY = [ "statutory_sick_pay", "statutory_maternity_pay", @@ -205,7 +227,6 @@ def build(population, year, raise_income=False): people[claimant] = dict( age=household["claimant_age"], current_education="NOT_IN_EDUCATION", - total_income=0.0, employment_income=0.0, self_employment_income=0.0, is_in_approved_training=False, @@ -227,8 +248,8 @@ def build(population, year, raise_income=False): people[name] = dict( age=nd["age"], current_education=nd["education"], - total_income=nd["earnings"] + other, employment_income=nd["earnings"], + other_investment_income=other, self_employment_income=nd["self_employment"], **nd.get("statutory_pay", dict.fromkeys(STATUTORY_PAY, 0.0)), pension_contributions=nd.get("pension_contributions", 0.0), @@ -237,6 +258,9 @@ def build(population, year, raise_income=False): reported: household["benefits"][benefit] if reports else 0.0 for benefit, reported in REPORTED.items() }, + # Held at zero so the oracle knows each person's gross income. + state_pension=0.0, + carers_allowance=0.0, is_in_approved_training=nd["approved_training"], # Young people started at 16; adults' start ages don't matter. age_started_or_accepted_current_education_or_training=16, @@ -287,6 +311,10 @@ def calculate(sim, year): "is_qualifying_young_person_for_child_benefit", "uc_mif_applies", "uc_minimum_income_floor", + "is_on_income_support", + "is_on_income_based_jsa", + "is_on_income_related_esa", + "is_on_pension_credit", ], benunit=[ "housing_benefit_non_dep_deductions", @@ -427,6 +455,32 @@ def payee_of(i, benefit): return person["position"] == reporter return bool(r["is_benunit_head"][i]) + f32 = np.float32 + + def own_gross(j): + """A person's own gross income from the generated inputs (invariant + 11): a self-employment loss counts as nil.""" + nd_j = persons[j]["non_dependant"] + if nd_j is None: + return 0.0 + return float( + f32(nd_j["earnings"]) + + sum( + f32(nd_j.get("statutory_pay", {}).get(name, 0.0)) + for name in STATUTORY_PAY + ) + + max(f32(0), f32(nd_j["self_employment"])) + + f32(nd_j["other_income"]) + ) + + def band_amount(scale, weekly): + """The amount of the band whose lower edge is the highest not above + weekly income, or None within a penny of an edge.""" + if any(abs(weekly - t) < 0.01 for t in scale.thresholds[1:]): + return None + index = max(k for k, t in enumerate(scale.thresholds) if t <= weekly) + return scale.amounts[index] * WEEKS + for i, person in enumerate(persons): household = population[person["household"]] hb_deduction = r["household_benefits_individual_non_dep_deduction"][i] @@ -440,17 +494,30 @@ def payee_of(i, benefit): if nd is None: continue # 9. The exemptions, read straight from the inputs. - # IS, JSA(IB), ESA(IR) and SPC count for their payee: the member who - # reports them, or the benefit unit's head where none does (HB reg - # 2(3)-(3A)). Universal Credit counts for both joint claimants. - on = {b: payee_of(i, b) and household["benefits"][b] > 0 for b in REPORTED} + # IS, JSA(IB), ESA(IR) and SPC count for their payee (HB reg 2(1), + # (3)-(3A)): the model's is_on_* status. Where the reporter, if any, is + # the claimant or partner, that is the reporter, or the benefit unit's + # head where none reports. Universal Credit counts for both joint + # claimants. + on = {b: bool(r[IS_ON[b]][i]) for b in REPORTED} + reporter = household.get("reporter") + reporter_index = ( + None + if reporter is None or reporter >= len(household["non_dependants"]) + else members_of[person["benunit"]][reporter] + ) + if reporter_index is None or couple[reporter_index]: + for b in REPORTED: + assert on[b] == (payee_of(i, b) and household["benefits"][b] > 0), ( + b, + i, + ) on["universal_credit_pre_benefit_cap"] = ( bool(couple[i]) and household["benefits"]["universal_credit_pre_benefit_cap"] > 0 ) student = nd["education"] != "NOT_IN_EDUCATION" # Earned income (UC regs 55(4)-(5), 57(2), 62), in the inputs' float32. - f32 = np.float32 statutory_pay = nd.get("statutory_pay", {}) employed_gross = f32(nd["earnings"]) + sum( (f32(statutory_pay.get(name, 0.0)) for name in STATUTORY_PAY), f32(0) @@ -483,8 +550,7 @@ def payee_of(i, benefit): # and who is not on Universal Credit. if household["country"] == "SCOTLAND" and not pension_age[i]: couple_on_legacy = bool(couple[i]) and any( - couple[j] - and any(payee_of(j, b) and household["benefits"][b] > 0 for b in legacy) + couple[j] and any(r[IS_ON[b]][j] for b in legacy) for j in members_of[person["benunit"]] ) qualifying = (on_legacy or couple_on_legacy) and not on[ @@ -541,6 +607,33 @@ def payee_of(i, benefit): if nd["hours"] < scale.remunerative_work_hours: assert ctr_deduction == pytest.approx(lowest, abs=0.01) + # 11. Banding on gross income computed from the inputs. + if couple[i]: + weekly = ( + sum(own_gross(j) for j in members_of[person["benunit"]] if couple[j]) + + sum(household["benefits"].values()) + ) / WEEKS + else: + weekly = own_gross(i) / WEEKS + if ( + r["housing_benefit_individual_non_dep_deduction_eligible"][i] + and nd["hours"] >= hb.remunerative_work_hours + and not (on["income_support"] or on["jsa_income"] or on["esa_income"]) + ): + expected = band_amount(hb.amount, weekly) + if expected is not None: + assert hb_deduction == pytest.approx(expected, abs=0.01), (i, weekly) + if ( + national + and r["council_tax_reduction_individual_non_dep_deduction_eligible"][i] + and not r["council_tax_reduction_non_dep_deduction_exempt"][i] + and nd["hours"] >= scale.remunerative_work_hours + and not qualifying + ): + expected = band_amount(scale.amount, weekly) + if expected is not None: + assert ctr_deduction == pytest.approx(expected, abs=0.01), (i, weekly) + benunit_count = 2 * len(population) # 4-5. Housing Benefit aggregation and the claimant exemption. expected_hb = aggregate( @@ -661,8 +754,8 @@ def test_band_edges_are_inclusive(scale_name, years): age=40, current_education="NOT_IN_EDUCATION", weekly_hours={str(y): 35.0 for y in years}, - total_income=incomes, - employment_income={str(y): 0.0 for y in years}, + housing_benefit_non_dep_gross_income=incomes, + council_tax_reduction_non_dep_gross_income=incomes, ) benunits[f"claimant_unit_{k}"] = dict( members=[claimant], @@ -718,3 +811,73 @@ def test_ctr_scales_match_enacted_sums(): year, ) assert list(node.amounts) == pytest.approx(row["amounts"]), (scheme, year) + + +@PROPERTY_SETTINGS +@given( + families=st.lists( + st.fixed_dictionaries( + dict( + couple=st.booleans(), + universal_credit_pre_benefit_cap=st.one_of( + st.just(0.0), st.floats(0, 40_000) + ), + child_benefit=st.sampled_from([0.0, 1_500.0, 5_000.0]), + child_tax_credit=st.sampled_from([0.0, 3_000.0]), + uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0, 25_323.0]), + uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), + jsa_contrib=st.sampled_from([0.0, 4_000.0]), + esa_contrib=st.sampled_from([0.0, 5_000.0]), + ) + ), + min_size=1, + max_size=8, + ), + year=st.integers(2025, 2030), +) +def test_capped_universal_credit_matches_the_model(families, year): + """Invariant 12: the after-cap Universal Credit counted in a + non-dependant family's gross income equals universal_credit for a family + with no Housing Benefit, Maternity Allowance or deductions.""" + from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + universal_credit_after_benefit_cap, + ) + + people, benunits, households_ = {}, {}, {} + for f, family in enumerate(families): + members = [f"adult_{f}_{k}" for k in range(2 if family["couple"] else 1)] + for k, name in enumerate(members): + people[name] = dict( + age=30 - k, + jsa_contrib=family["jsa_contrib"] if k == 0 else 0.0, + esa_contrib=family["esa_contrib"] if k == 0 else 0.0, + maternity_allowance=0.0, + ) + benunits[f"family_{f}"] = dict( + members=members, + **{ + key: family[key] + for key in [ + "universal_credit_pre_benefit_cap", + "child_benefit", + "child_tax_credit", + "uc_benefit_cap", + "uc_childcare_element", + ] + }, + income_support=0.0, + jsa_income=0.0, + esa_income=0.0, + housing_benefit_pre_benefit_cap=0.0, + uc_deductions=0.0, + ) + households_[f"household_{f}"] = dict(members=members, country="ENGLAND") + sim = Simulation( + situation=for_years( + dict(people=people, benunits=benunits, households=households_), [year] + ) + ) + helper = universal_credit_after_benefit_cap(sim.populations["benunit"], year) + np.testing.assert_allclose( + helper, sim.calculate("universal_credit", year), atol=0.01 + ) diff --git a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py index e829efd5ef..bfe7580b84 100644 --- a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py +++ b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py @@ -13,17 +13,18 @@ YEAR = 2026 -@pytest.fixture(scope="module") -def non_dependants(): +CASES = [ + ("earnings_only", 7_800, 0), + ("maintenance_100", 7_800, 5_200), + ("maintenance_150", 7_800, 7_800), + ("same_gross_earnings", 13_000, 0), +] + + +def build(country, local_authority): # Separate benefit units isolate each non-dependant's own gross income. - cases = [ - ("earnings_only", 7_800, 0), - ("maintenance_100", 7_800, 5_200), - ("maintenance_150", 7_800, 7_800), - ("same_gross_earnings", 13_000, 0), - ] people, benunits, households = {}, {}, {} - for name, earnings, maintenance in cases: + for name, earnings, maintenance in CASES: people[name] = { "age": {YEAR: 30}, "weekly_hours": {YEAR: 16}, @@ -35,18 +36,30 @@ def non_dependants(): benunits[name] = { "members": [name], "universal_credit": {YEAR: 0}, + "universal_credit_pre_benefit_cap": {YEAR: 0}, "pension_credit": {YEAR: 0}, } households[name] = { "members": [name], - "country": {YEAR: "ENGLAND"}, - "local_authority": {YEAR: "MERTON"}, + "country": {YEAR: country}, + "local_authority": {YEAR: local_authority}, } return Simulation( situation={"people": people, "benunits": benunits, "households": households} ) +@pytest.fixture(scope="module") +def non_dependants(): + return build("ENGLAND", "MERTON") + + +@pytest.fixture(scope="module") +def welsh_non_dependants(): + # Wales's national scheme covers working-age applicants too. + return build("WALES", "CARDIFF") + + def values(simulation, variable): return np.asarray(simulation.calculate(variable, YEAR), dtype=float) @@ -62,16 +75,19 @@ def test_housing_benefit_non_dependant_band_counts_spousal_maintenance(non_depen ) -def test_national_ctr_non_dependant_band_counts_spousal_maintenance(non_dependants): - # SI 2012/2885 Sch 1 para 8(2), (9): £150 earnings plus £100 - # maintenance and £250 earnings have the same normal gross weekly income. - # Compare bands without endorsing the pre-existing shared HB rate scale - # in the national CTR formula (#2007). - deductions = values( - non_dependants, "council_tax_reduction_individual_non_dep_deduction" +def test_national_ctr_non_dependant_band_counts_spousal_maintenance( + welsh_non_dependants, +): + # WSI 2013/3029 Sch 6 para 5(2), (9), as up-rated by WSI 2026/13: £150 + # earnings plus £100 maintenance and £250 earnings both fall below £279 + # (£6.70 a week); £150 plus £150 = £300 falls in £279–484.99 (£13.35). + np.testing.assert_allclose( + values( + welsh_non_dependants, "council_tax_reduction_individual_non_dep_deduction" + ), + [348.40, 348.40, 694.20, 348.40], + atol=0.01, ) - assert deductions[1] == pytest.approx(deductions[3], abs=0.01) - assert deductions[1] > deductions[0] def test_local_ctr_non_dependant_band_counts_spousal_maintenance(non_dependants): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index acebeffe72..899373c74d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -58,12 +58,13 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): names the programme's person-level gross income variable.""" claimant_or_partner = person("is_claimant_or_partner", period) own_income = person(own_gross_income, period) - family_benefits = universal_credit_after_benefit_cap(person.benunit, period) + sum( - person.benunit(benefit, period) for benefit in FAMILY_GROSS_INCOME_BENEFITS - ) - couple_income = ( - person.benunit.sum(own_income * claimant_or_partner) + family_benefits + benunit = person.simulation.populations["benunit"] + family_benefits = universal_credit_after_benefit_cap(benunit, period) + add( + benunit, period, FAMILY_GROSS_INCOME_BENEFITS ) + couple_income = person.benunit.sum( + own_income * claimant_or_partner + ) + benunit.project(family_benefits) annual = where(claimant_or_partner, couple_income, own_income) return annual / WEEKS_IN_YEAR From b97699433a28bcb52222cb69c20b37fa04374e62 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 22:07:32 -0400 Subject: [PATCH 29/67] Pin investment income and losses in the gross-income cases; restate the fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- .../housing_benefit_non_dep_gross_income.yaml | 22 +++++++++++++++++++ ...il_tax_reduction_non_dep_gross_income.yaml | 22 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 40b5dd5254..bd8311447b 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated: bands include their lower edge; only a non-dependant in remunerative work, and not on IS, JSA(IB) or ESA(IR), is banded, on the couple's gross income (including statutory pay, Maternity Allowance, UC after the benefit cap, counting only the couple's own capped benefits and keeping the childcare protection, tax credits, child benefit and the income-related benefits); a couple pays one deduction and any other adult in the family their own; the claimant-side and non-dependant exemptions apply, with the claimant or partner read from `is_claimant_or_partner`; and the England pensioner, Scottish and Welsh CTR schemes use their own scales (2013-14 to 2026-27) instead of the Housing Benefit amounts, with each jointly liable applicant's deduction taken under its own scheme. +- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated. Bands include their lower edge. A non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, pays the lowest amount whatever their income. A couple pays one deduction, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and any other adult in the family pays their own. Gross income counts other investment income and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index e186f9be3a..4753918418 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -601,3 +601,25 @@ miscellaneous_income: -300 output: housing_benefit_non_dep_gross_income: 1_000 + +- name: housing_benefit_non_dep_gross_income counts other investment income + # Gross income is earnings and any other income (DWP HB guidance A5.542). + period: 2025 + input: + social_security_income: 0 + employment_income: 1_000 + other_investment_income: 2_000 + output: + housing_benefit_non_dep_gross_income: 3_000 + +- name: housing_benefit_non_dep_gross_income counts a self-employment or property loss as nil + # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # does not reduce other income. + period: 2025 + input: + social_security_income: 0 + employment_income: 5_000 + self_employment_income: -3_000 + property_income: -1_000 + output: + housing_benefit_non_dep_gross_income: 5_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index 3e4f20ac4d..5736b27177 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -721,3 +721,25 @@ miscellaneous_income: -300 output: council_tax_reduction_non_dep_gross_income: 1_000 + +- name: council_tax_reduction_non_dep_gross_income counts other investment income + # Gross income is earnings and any other income (DWP HB guidance A5.542). + period: 2025 + input: + social_security_income: 0 + employment_income: 1_000 + other_investment_income: 2_000 + output: + council_tax_reduction_non_dep_gross_income: 3_000 + +- name: council_tax_reduction_non_dep_gross_income counts a self-employment or property loss as nil + # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # does not reduce other income. + period: 2025 + input: + social_security_income: 0 + employment_income: 5_000 + self_employment_income: -3_000 + property_income: -1_000 + output: + council_tax_reduction_non_dep_gross_income: 5_000 From 634047bcaea40143ecc331919b8a5e74db9869a6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 22:18:07 -0400 Subject: [PATCH 30/67] Count only the couple's income-based JSA and ESA in a couple's gross income Another member of the benefit unit claims those awards in their own right (#2027's claimant_or_partner_* awards), so they no longer raise the couple's band. The HB work-gate cases hold family awards at zero so a nil income is nil, and the property oracle follows the same rules. Co-Authored-By: Claude Opus 5.5 --- ...ing_benefit_non_dep_remunerative_work.yaml | 138 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 24 ++- .../non_dep_deduction/_non_dependants.py | 8 +- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml index c5d3406773..7d0dde3004 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml @@ -3,6 +3,8 @@ # A non-worker or 15-hour worker pays 20.40 * 52 = 1,060.80 in 2026-27, # whatever their income. The exemptions in reg 74(7), (8) and (10) and reg # 55(7)-(9) act through eligibility, which most cases here take as an input. +# Family awards, which a single non-dependant's gross income would count, are +# held at zero. # A 16-hour worker's wages of 9,000 alone are below 192/week; adding # IIDB or child maintenance of 12,000 gives 403.85/week, so 105.20 * 52. @@ -11,6 +13,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -30,6 +40,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -49,6 +67,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -68,6 +94,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -87,6 +121,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -106,6 +148,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -125,6 +175,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -144,6 +202,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 country: ENGLAND housing_benefit_individual_non_dep_deduction_eligible: true @@ -163,6 +229,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 70 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -178,6 +252,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 70 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 0 @@ -197,6 +279,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 22 hours_worked: 0 social_security_income: 0 @@ -212,6 +302,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -228,6 +326,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -244,6 +350,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -260,6 +374,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 age: 30 housing_benefit_individual_non_dep_deduction_eligible: true hours_worked: 832 @@ -276,6 +398,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.dwp.housing_benefit.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND @@ -294,6 +424,14 @@ period: 2026 absolute_error_margin: 0.01 input: + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 gov.dwp.housing_benefit.non_dep_deduction.remunerative_work_hours: 20 age: 30 country: ENGLAND diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index f59085ce4d..270be009a1 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -506,8 +506,10 @@ def band_amount(scale, weekly): if reporter is None or reporter >= len(household["non_dependants"]) else members_of[person["benunit"]][reporter] ) + # Pension Credit's payee must also have reached the qualifying age + # (SPCA 2002 s.1(2)(b)), which payee_of doesn't model. if reporter_index is None or couple[reporter_index]: - for b in REPORTED: + for b in ["income_support", "jsa_income", "esa_income"]: assert on[b] == (payee_of(i, b) and household["benefits"][b] > 0), ( b, i, @@ -607,11 +609,27 @@ def band_amount(scale, weekly): if nd["hours"] < scale.remunerative_work_hours: assert ctr_deduction == pytest.approx(lowest, abs=0.01) - # 11. Banding on gross income computed from the inputs. + # 11. Banding on gross income computed from the inputs. Income-based + # JSA and income-related ESA count only where the claimant or partner + # reports them, or no one does. + couple_awards = ( + ["jsa_income", "esa_income"] + if reporter_index is None or couple[reporter_index] + else [] + ) + family_awards = sum( + household["benefits"][b] + for b in [ + "income_support", + "pension_credit", + "universal_credit_pre_benefit_cap", + *couple_awards, + ] + ) if couple[i]: weekly = ( sum(own_gross(j) for j in members_of[person["benunit"]] if couple[j]) - + sum(household["benefits"].values()) + + family_awards ) / WEEKS else: weekly = own_gross(i) / WEEKS diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 899373c74d..e003e7e50f 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -17,14 +17,16 @@ # Family benefits counted in a couple's gross income, besides Universal Credit. # They are awarded to the benefit unit, so the person-level gross income -# variables leave them out. +# variables leave them out. Income-based JSA and income-related ESA count the +# claimant's and partner's award only: another member claims in their own +# right. FAMILY_GROSS_INCOME_BENEFITS = [ "child_tax_credit", "working_tax_credit", "child_benefit", "income_support", - "jsa_income", - "esa_income", + "claimant_or_partner_jsa_income", + "claimant_or_partner_esa_income", "pension_credit", ] From f9f62902b37555bb2d4e6bc67feaecb1e9051c88 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 22:47:24 -0400 Subject: [PATCH 31/67] Count Universal Credit in the income of the claimants it assesses A family's UC counts in the couple's joint gross income where either of them claims it, and in another member's own income where is_uc_claimant makes them the claimant; the UC limb of the exemptions reads the same assessed claimants. Two cases pin it, and that another adult's own ESA stays out of the couple's income. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 107 ++++++++++++++++++ .../non_dep_deduction/_non_dependants.py | 36 ++++-- ...ousing_benefit_non_dep_deduction_exempt.py | 6 +- .../_national_non_dependants.py | 4 +- 4 files changed, 137 insertions(+), 16 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 845e7e1777..40edc163e7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -206,3 +206,110 @@ output: # HB Regs 2006 reg 74(6). housing_benefit_non_dep_deductions: [0, 0] + +- name: Universal Credit counts in the income of the claimant it assesses + # The non-dependant family's Universal Credit is the adult son's own claim + # (is_uc_claimant), so it counts in his gross income, not his parents' + # (HB Regs 2006 reg 74(2), (4)). The parents' joint income is the father's + # 180 a week, under the 192 band: 20.40, one deduction for the couple + # (reg 74(3)). The son works 2 hours, so he pays 20.40 (reg 74(1)(b)). + # (20.40 + 20.40) * 52 = 2,121.60. Counting the son's 100 a week of UC in + # the parents' income would give 280 a week and 64.35. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_household_head: true + father: + age: 50 + employment_income: 9_360 # 180 a week + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + mother: + age: 48 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + son: + age: 27 + employment_income: 1_040 # 20 a week + weekly_hours: 2 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: false + is_uc_claimant: true + benunits: + claimant_unit: + members: [claimant] + non_dependant_unit: + members: [father, mother, son] + universal_credit_pre_benefit_cap: 5_200 # 100 a week + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, father, mother, son] + tenure_type: RENT_FROM_COUNCIL + rent: 10_000 + output: + household_benefits_individual_non_dep_deduction: [0, 1_060.80, 1_060.80, 1_060.80] + housing_benefit_non_dep_deductions: [2_121.60, 0] + +- name: Another adult's own income-related ESA stays out of the couple's income + # The adult son claims income-related ESA in his own right, so it is his + # income, not his parents' (HB Regs 2006 reg 74(4)). The parents' joint + # income is the father's 180 a week, under the 192 band: 20.40, one + # deduction for the couple. The son does no work and pays 20.40 (reg + # 74(1)(b)). (20.40 + 20.40) * 52 = 2,121.60. Counting his 100 a week of + # ESA in the parents' income would give 280 a week and 64.35. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_household_head: true + father: + age: 50 + employment_income: 9_360 # 180 a week + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + mother: + age: 48 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + son: + age: 27 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: false + benunits: + claimant_unit: + members: [claimant] + non_dependant_unit: + members: [father, mother, son] + esa_income: 5_200 # the son's own award, 100 a week + claimant_or_partner_esa_income: 0 + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, father, mother, son] + tenure_type: RENT_FROM_COUNCIL + rent: 10_000 + output: + household_benefits_individual_non_dep_deduction: [0, 1_060.80, 1_060.80, 1_060.80] + housing_benefit_non_dep_deductions: [2_121.60, 0] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index e003e7e50f..91c61a5004 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -53,21 +53,35 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 - para 8(4)): each member's own gross income plus the family's Universal - Credit, tax credits, child benefit, Income Support, income-based JSA, - income-related ESA and Pension Credit. Anyone else in the benefit unit is - a separate non-dependant, banded on their own income. own_gross_income - names the programme's person-level gross income variable.""" + para 8(4)): each member's own gross income plus the family's tax credits, + child benefit, Income Support, income-based JSA, income-related ESA and + Pension Credit, and its Universal Credit where either member claims it. + Anyone else in the benefit unit is a separate non-dependant, banded on + their own income, with the Universal Credit where they alone claim it. + own_gross_income names the programme's person-level gross income + variable.""" claimant_or_partner = person("is_claimant_or_partner", period) own_income = person(own_gross_income, period) benunit = person.simulation.populations["benunit"] - family_benefits = universal_credit_after_benefit_cap(benunit, period) + add( - benunit, period, FAMILY_GROSS_INCOME_BENEFITS + other_awards = benunit.project(add(benunit, period, FAMILY_GROSS_INCOME_BENEFITS)) + # Universal Credit is the income of the claimants it assesses, the + # claimant and partner unless is_uc_claimant names someone else. + universal_credit = benunit.project( + universal_credit_after_benefit_cap(benunit, period) ) - couple_income = person.benunit.sum( - own_income * claimant_or_partner - ) + benunit.project(family_benefits) - annual = where(claimant_or_partner, couple_income, own_income) + uc_claimant = person("is_uc_assessed_claimant", period) + couple_claims_universal_credit = person.benunit.any( + uc_claimant & claimant_or_partner + ) + couple_income = ( + person.benunit.sum(own_income * claimant_or_partner) + + other_awards + + where(couple_claims_universal_credit, universal_credit, 0) + ) + single_income = own_income + where( + uc_claimant & ~couple_claims_universal_credit, universal_credit, 0 + ) + annual = where(claimant_or_partner, couple_income, single_income) return annual / WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 8c31b84f3e..3ef4cd0518 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -33,14 +33,14 @@ def formula(person, period, parameters): full_time_student = is_full_time_student_non_dep(person, period) # IS, income-based JSA and SPC count for the person they are payable # to (reg 2(1), (3)); Universal Credit for both joint claimants. - claimant_or_partner = person("is_claimant_or_partner", period) on_pension_credit = person("is_on_pension_credit", period) on_legacy_income_related_benefit = person( "is_on_income_support", period ) | person("is_on_income_based_jsa", period) # Entitlement before the benefit cap: universal_credit itself depends on - # Housing Benefit through the cap. - entitled_to_universal_credit = claimant_or_partner & ( + # Housing Benefit through the cap. The award is the claimants' it + # assesses. + entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) universal_credit_without_earned_income = ( diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py index 76da080f73..13198d5d67 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py @@ -52,7 +52,7 @@ def is_qualifying_income_related_benefit_claimant( couple_on_benefit = claimant_or_partner & ( person.benunit.any(on_benefit & claimant_or_partner) ) - entitled_to_universal_credit = claimant_or_partner & ( + entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) partner_rule = ( @@ -85,7 +85,7 @@ def non_dep_deduction_exempt(person, period, parameters, pensioner_applicant): # they are payable to; Universal Credit for both joint claimants. claimant_or_partner = person("is_claimant_or_partner", period) on_pension_credit = person("is_on_pension_credit", period) - entitled_to_universal_credit = claimant_or_partner & ( + entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) qualifying_income_related_benefit_claimant = ( From 86e86d3dbb87fd393f7a19539cb605b96a9d31e5 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 31 Oct 2026 23:28:25 -0400 Subject: [PATCH 32/67] Pin that another member's Universal Credit claim exempts no one else Co-Authored-By: Claude Opus 5.5 --- ...sing_benefit_non_dep_deduction_exempt.yaml | 50 +++++++++++++++++++ .../test_non_dependant_spousal_maintenance.py | 5 +- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml index 5699336677..7412593b5b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.yaml @@ -117,3 +117,53 @@ universal_credit_pre_benefit_cap: 1_000 output: housing_benefit_non_dep_deduction_exempt: false + +- name: Another member's Universal Credit claim doesn't exempt a non-dependant under 25 + # The family's Universal Credit is the 40-year-old's own claim + # (is_uc_claimant), so the 22-year-old partner with no earnings is not + # entitled to it (HB Regs 2006 reg 74(8)(b); SI 2012/2885 Sch 1 para + # 8(8)(c)). The 40-year-old has earnings, so the limb exempts no one. + period: 2026 + input: + people: + claimant: + age: 70 + is_household_head: true + partner_a: + age: 22 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + partner_b: + age: 30 + employment_income: 9_360 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: true + is_uc_claimant: false + other_adult: + age: 40 + employment_income: 1_040 + weekly_hours: 2 + current_education: NOT_IN_EDUCATION + is_claimant_or_partner: false + is_uc_claimant: true + benunits: + claimant_unit: + members: [claimant] + pension_credit: 0 + non_dependant_unit: + members: [partner_a, partner_b, other_adult] + universal_credit_pre_benefit_cap: 5_200 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, partner_a, partner_b, other_adult] + country: ENGLAND + local_authority: MAIDSTONE + output: + housing_benefit_non_dep_deduction_exempt: [false, false, false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false, false, false] diff --git a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py index bfe7580b84..ab7872153d 100644 --- a/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py +++ b/policyengine_uk/tests/test_non_dependant_spousal_maintenance.py @@ -1,7 +1,8 @@ """Spousal maintenance is gross income for non-dependant deductions. -HB Regs 2006 reg 74(2), (9), pension-age HB reg 55(2), (10), and -SI 2012/2885 Sch 1 para 8(2), (9) do not disregard spousal maintenance. +HB Regs 2006 reg 74(2), (9), pension-age HB reg 55(2), (10), SI 2012/2885 +Sch 1 para 8(2), (9) and WSI 2013/3029 Sch 6 para 5(2), (9) do not disregard +spousal maintenance. It remains exempt from income tax under ITTOIA 2005 s 727. """ From 008b8b0311d50658265d7615958177911c6b6024 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:51:25 -0500 Subject: [PATCH 33/67] Read Universal Credit's own earned income and capped award for non-dependants The exemption for a non-dependant on Universal Credit "calculated on the basis that [they do] not have any earned income" now reads uc_individual_earned_income, so allowable expenses, company income, trading losses and the couple minimum income floor follow UC's own rules. The capped award counted in a couple's gross income now shares the award's calculation of the regulation 81 reduction and of the part a protected minimum floor lets it take, and reads a stored or neutralized universal_credit, a universal_credit formula a reform replaces, and a stored cap reduction or welfare-benefit total. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/utils/supplied_inputs.py | 4 + .../non_dep_deduction/_non_dependants.py | 122 +++++++++++------- ...d_benefits_individual_non_dep_deduction.py | 2 +- .../benefit_cap/uc_benefit_cap_reduction.py | 18 ++- .../uc_benefit_cap_reduction_after_floor.py | 28 +++- .../_national_non_dependants.py | 2 +- 6 files changed, 115 insertions(+), 61 deletions(-) diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index 38897f81bf..f6ff5000a6 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -55,6 +55,7 @@ "receives_carer_benefit", "receives_carers_allowance", "receives_carer_support_payment", + "benefit_cap_welfare_benefits", "bus_fare_spending", "bus_in_london_trips", "current_education", @@ -83,9 +84,12 @@ "tenure_type", "targeted_childcare_entitlement", "trading_loss", + "uc_benefit_cap_reduction", + "uc_benefit_cap_reduction_after_floor", "uc_carer_element", "uc_LCWRA_element", "universal_childcare_entitlement", + "universal_credit", } ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 91c61a5004..827c86f9a4 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -4,10 +4,20 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.benefit_cap import benefit_cap_couple from policyengine_uk.utils.benefit_unit import add_for_members, award_of_couple +from policyengine_uk.utils.supplied_inputs import stored_as_input from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( CLAIMANT_OR_PARTNER_BENEFITS as CAPPED_CLAIMANT_OR_PARTNER_BENEFITS, FAMILY_BENEFITS as CAPPED_FAMILY_BENEFITS, ) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_reduction import ( + benefit_cap_reduction_of, +) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_reduction_after_floor import ( + benefit_cap_reduction_applied, +) +from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( + universal_credit as universal_credit_variable, +) from policyengine_uk.variables.gov.dwp.esa_income import ( income_related_esa_award_on_reports, ) @@ -30,15 +40,6 @@ "pension_credit", ] -# Statutory payments treated as employed earnings (UC Regs 2013 reg 55(4)). -# Statutory adoption, shared parental, parental bereavement and neonatal care -# pay have no input. -STATUTORY_PAY_EARNINGS = [ - "statutory_sick_pay", - "statutory_maternity_pay", - "statutory_paternity_pay", -] - # The welfare benefits the benefit cap counts (benefit_cap_welfare_benefits) # other than Housing Benefit, and Maternity Allowance, a welfare benefit # (Welfare Reform Act 2012 s.96(10)(i)) that total leaves out. Bereavement @@ -50,7 +51,7 @@ ] + ["maternity_allowance"] -def non_dependant_weekly_gross_income(person, period, own_gross_income): +def non_dependant_weekly_gross_income(person, period, parameters, own_gross_income): """Normal weekly gross income for the deduction bands. A claimant or partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 para 8(4)): each member's own gross income plus the family's tax credits, @@ -67,7 +68,7 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): # Universal Credit is the income of the claimants it assesses, the # claimant and partner unless is_uc_claimant names someone else. universal_credit = benunit.project( - universal_credit_after_benefit_cap(benunit, period) + universal_credit_after_benefit_cap(benunit, period, parameters) ) uc_claimant = person("is_uc_assessed_claimant", period) couple_claims_universal_credit = person.benunit.any( @@ -85,18 +86,61 @@ def non_dependant_weekly_gross_income(person, period, own_gross_income): return annual / WEEKS_IN_YEAR -def universal_credit_after_benefit_cap(benunit, period): +def reads_simulation_value(benunit, period, variable_name, default_formula=None): + """Whether to read the variable rather than recalculate it here: a value + stored as an input for the period, a neutralized variable, or, where + default_formula is given, a formula a reform has replaced.""" + variable = benunit.simulation.tax_benefit_system.get_variable(variable_name) + if variable.is_neutralized: + return True + if default_formula is not None: + formula = variable.get_formula(period) + if formula is None or formula.__code__ != default_formula.__code__: + return True + return stored_as_input(benunit, variable_name, period) + + +def universal_credit_after_benefit_cap(benunit, period, parameters): """A family's Universal Credit award after the benefit cap (UC Regs 2013 reg 81), for a family that is not liable for rent. universal_credit and uc_benefit_cap_reduction depend on Housing Benefit through the welfare - benefits they count, so the cap (uc_benefit_cap) is applied here to the - same welfare benefits other than Housing Benefit, which such a family does - not receive (UC reg 80(2A) counts it as nil in any case), for the same - single person or couple (benefit_cap_couple; reg 79(1)). The award is - reduced by the excess minus the childcare costs element, and not at all - where that element is greater than the excess (reg 81(1)-(2)). Deductions + benefits they count, so this applies the cap to the same welfare benefits + other than Housing Benefit, which such a family does not receive (UC reg + 80(2A) counts it as nil in any case), for the same single person or couple + (benefit_cap_couple; reg 79(1)). It shares the award's own calculation of + the reduction (benefit_cap_reduction_of) and of the part a protected + minimum floor lets it take (benefit_cap_reduction_applied). Deductions paid from the award to third parties or for debts are still the family's - income, so they are not taken off.""" + income, so they are not taken off, and the floor is applied as for a + family with none: the deduction rate depends on the cap reduction, and so + on Housing Benefit. A universal_credit stored as an input or given a new + formula by a reform is read as it is, and so is a stored or neutralized + uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or + benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" + if reads_simulation_value( + benunit, period, "universal_credit", universal_credit_variable.formula + ): + return benunit("universal_credit", period) + if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction_after_floor"): + applied = benunit("uc_benefit_cap_reduction_after_floor", period) + else: + if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction"): + reduction = benunit("uc_benefit_cap_reduction", period) + else: + reduction = benefit_cap_reduction_of( + benunit, period, capped_welfare_benefits(benunit, period) + ) + applied = benefit_cap_reduction_applied( + benunit, period, parameters, reduction, 0 + ) + return max_(benunit("universal_credit_pre_benefit_cap", period) - applied, 0) + + +def capped_welfare_benefits(benunit, period): + """The welfare benefits the Universal Credit cap counts, with Housing + Benefit at nil (reg 80(2A)) and Maternity Allowance in.""" + if reads_simulation_value(benunit, period, "benefit_cap_welfare_benefits"): + return benunit("benefit_cap_welfare_benefits", period) couple = benefit_cap_couple(benunit, period) income_based = award_of_couple( benunit, @@ -113,41 +157,21 @@ def universal_credit_after_benefit_cap(benunit, period): income_related_esa_award_on_reports, couple, ) - capped = income_based + add_for_members( + return income_based + add_for_members( benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT, couple ) - excess = max_(0, capped - benunit("uc_benefit_cap", period)) - reduction = max_(0, excess - benunit("uc_childcare_element", period)) - return max_(0, benunit("universal_credit_pre_benefit_cap", period) - reduction) def has_earned_income(person, period): - """Whether the person has earned income (UC Regs 2013 reg 52): employed - earnings, including statutory sick, maternity and paternity pay and less - relievable pension contributions (reg 55(4)-(5)); self-employed earnings, - a loss counting as nil and less any relievable pension contributions not - already deducted from employed earnings (reg 57(2), steps 3-4), with the - minimum income floor applying where the Universal Credit model applies it - (reg 62); and other paid work.""" - pension_contributions = person("pension_contributions", period) - employed_gross = add(person, period, ["employment_income", *STATUTORY_PAY_EARNINGS]) - has_employed_earnings = employed_gross > 0 - employed = max_(0, employed_gross - pension_contributions) - # Contributions are deducted from employed earnings where there are any - # (reg 55(5)(a)), and otherwise from self-employed earnings (reg 57(2), - # step 4). - self_employed = max_( - 0, - max_(0, person("self_employment_income", period)) - - where(has_employed_earnings, 0, pension_contributions), - ) - self_employed = where( - person("uc_mif_applies", period), - max_(self_employed, person("uc_minimum_income_floor", period)), - self_employed, - ) - other = max_(0, person("miscellaneous_income", period)) - return (employed + self_employed + other) > 0 + """Whether the person has earned income as Universal Credit calculates it + (HB Regs 2006 reg 74(8)(b), (11) and the CTR schemes' equivalents give + "earned income" the meaning in UC Regs 2013 reg 52): + uc_individual_earned_income, which takes off allowable expenses, tax, + National Insurance and relievable pension contributions (reg 55(3), (5)), + counts a company's income attributed to its owner and sets trading losses + against profits (regs 57(2), 77(3)), and applies the minimum income floor + only where reg 62 does, a couple's floor included.""" + return person("uc_individual_earned_income", period) > 0 def is_on_legacy_income_related_benefit(person, period): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index 313c681e39..d9389129d4 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -30,7 +30,7 @@ class household_benefits_individual_non_dep_deduction(Variable): def formula(person, period, parameters): p = parameters(period).gov.dwp.housing_benefit.non_dep_deduction weekly_income = non_dependant_weekly_gross_income( - person, period, "housing_benefit_non_dep_gross_income" + person, period, parameters, "housing_benefit_non_dep_gross_income" ) # Reg 6(6), or pension-age reg 6(6): a person on Income Support, # income-based JSA or income-related ESA is not in remunerative work. diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index eacb266686..adb72a2912 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -1,6 +1,19 @@ from policyengine_uk.model_api import * +def benefit_cap_reduction_of(benunit, period, welfare_benefits): + """The regulation 81 reduction for welfare benefits that already count + Housing Benefit as nil (reg. 80(2A)): the excess over the Universal Credit + cap, minus the childcare costs element, and nothing where that element is + greater than the excess or the family has no award (reg. 78(1)). The + non-dependant deductions call this too, with their own total of welfare + benefits (housing_benefit/non_dep_deduction/_non_dependants.py).""" + excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) + reduction = max_(excess - benunit("uc_childcare_element", period), 0) + has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 + return where(has_award, reduction, 0) + + class uc_benefit_cap_reduction(Variable): value_type = float entity = BenUnit @@ -34,7 +47,4 @@ def formula(benunit, period, parameters): welfare_benefits = benunit("benefit_cap_welfare_benefits", period) - benunit( "housing_benefit_pre_benefit_cap", period ) - excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) - reduction = max_(excess - benunit("uc_childcare_element", period), 0) - has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 - return where(has_award, reduction, 0) + return benefit_cap_reduction_of(benunit, period, welfare_benefits) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py index b40442dc06..dbe7c0b409 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py @@ -4,6 +4,24 @@ ) +def benefit_cap_reduction_applied( + benunit, period, parameters, cap_reduction, deduction_demand +): + """The part of a regulation 81 reduction taken off the award, given the + deductions demanded from it, under any protected minimum floor. The + non-dependant deductions call this too + (housing_benefit/non_dep_deduction/_non_dependants.py).""" + p = parameters(period).gov.dwp.universal_credit.deductions + _, applied = allocate_uc_reductions( + benunit("universal_credit_pre_benefit_cap", period), + cap_reduction, + deduction_demand, + benunit("uc_maximum_reduction", period), + p.minimum_payable * MONTHS_IN_YEAR, + ) + return applied + + class uc_benefit_cap_reduction_after_floor(Variable): label = "UC benefit cap reduction applied" documentation = ( @@ -24,18 +42,16 @@ class uc_benefit_cap_reduction_after_floor(Variable): defined_for = "claims_universal_credit" def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.universal_credit.deductions # JRF's September 2026 example (standard allowance £98 a week, a £15 # Fair Repayment Rate deduction, an £80 cap reduction, an 85% floor) # limits the total to £15. Meeting the deduction first, as here, # removes the whole cap reduction. - _, cap_reduction = allocate_uc_reductions( - benunit("universal_credit_pre_benefit_cap", period), + return benefit_cap_reduction_applied( + benunit, + period, + parameters, benunit("uc_benefit_cap_reduction", period), benunit("uc_deduction_rate", period) * benunit("uc_standard_allowance", period) * benunit("uc_award_carries_deductions", period), - benunit("uc_maximum_reduction", period), - p.minimum_payable * MONTHS_IN_YEAR, ) - return cap_reduction diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py index 13198d5d67..e5bc21bb04 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_national_non_dependants.py @@ -148,7 +148,7 @@ def individual_non_dep_deduction(person, period, parameters, pensioner_applicant is_wales_scheme(country), ] weekly_income = non_dependant_weekly_gross_income( - person, period, "council_tax_reduction_non_dep_gross_income" + person, period, parameters, "council_tax_reduction_non_dep_gross_income" ) no_income = np.zeros_like(weekly_income) remunerative_work_hours = select( From eeb0cf8659cf89e965ff831c7fc708480b9faaac Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:51:32 -0500 Subject: [PATCH 34/67] Take both Welsh deductions only for the couple's own Universal Credit award WSI 2013/3029 Sch 6 para 5(3) lifts the one-deduction rule where there is an award of Universal Credit to the couple. Another member's award as a single claimant now leaves the couple one deduction. Co-Authored-By: Claude Opus 5.5 --- ...ouncil_tax_reduction_non_dep_deductions.py | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index f1741d1f5c..99297c3b37 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -20,9 +20,10 @@ class council_tax_reduction_non_dep_deductions(Variable): "Deductions for the non-dependants in other benefit units of the " "household: one per couple, the higher of the two members' amounts " "(both, in the Welsh scheme for people who are not pensioners, when " - "the couple has a Universal Credit award), one for each other member, " - "each under the applicant family's own scheme and apportioned between " - "jointly liable applicants; none if the applicant or partner is exempt." + "the couple's own Universal Credit award assesses either member), one " + "for each other member, each under the applicant family's own scheme " + "and apportioned between jointly liable applicants; none if the " + "applicant or partner is exempt." ) definition_period = YEAR unit = GBP @@ -39,9 +40,17 @@ def formula(benunit, period, parameters): country = benunit.household("country", period) # WSI 2013/3029 Sch 6 para 5(3): a working-age applicant's deduction is # for each member of a non-dependant couple with a Universal Credit - # award. - universal_credit_couple = is_wales_scheme(country) & ( - benunit("universal_credit_pre_benefit_cap", period) > 0 + # award. The award is the couple's own: one that assesses the + # claimant or partner. Another member's award, as a single claimant, + # leaves the couple with one deduction. + couple_claims_universal_credit = benunit.any( + person("is_uc_assessed_claimant", period) + & person("is_claimant_or_partner", period) + ) + universal_credit_couple = ( + is_wales_scheme(country) + & couple_claims_universal_credit + & (benunit("universal_credit_pre_benefit_cap", period) > 0) ) # council_tax_reduction_individual_non_dep_deduction is each person's # deduction under the scheme of the household head's family. A jointly From 44985a8c29b09e5afd7f46c7b6eb19871a7397c8 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:51:36 -0500 Subject: [PATCH 35/67] Test the earned-income, capped-award and Welsh couple rules Four earned-income cases (expenses, a trading loss, company income, a couple's floor), two Welsh cases where a third adult alone claims Universal Credit, the capped award under an 85% protected floor and with stored and replaced values, and an invariant comparing the exemption with Universal Credit's earned income. The property generator now varies is_uc_claimant. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 354 +++++++++++++++ ...test_non_dependant_deduction_properties.py | 404 +++++++++++++++--- 2 files changed, 687 insertions(+), 71 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index f05106199d..49605e630c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1226,3 +1226,357 @@ # 502.50 a week: the HB 485 band (119.85) and the CTR 485 band (13.30). housing_benefit_non_dep_deductions: [6_232.20, 0] council_tax_reduction_non_dep_deductions: [691.60, 0] + +# Round 7 of the independent review: the Universal Credit exemption reads +# Universal Credit's own calculation of earned income (uc_individual_earned_income). + +- name: Employment expenses that cancel the wages leave no earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + employment_income: 1_000 + employment_expenses: 1_000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 55(3)(a): allowable employment expenses come off + # employed earnings, leaving none (HB reg 74(8)(b), (11); WSI 2013/3029 + # Sch 1 para 3(8)(c), (10)). + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: A trading loss that cancels the profit leaves no earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + self_employment_income: 1_000 + trading_loss: 1_000 + uc_mif_applies: false + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 57(2): the trades' profits and losses are added + # together before any nil floor, so the loss cancels the profit. The + # minimum income floor is held off, so only the loss rule decides. + housing_benefit_non_dep_deduction_exempt: [false, true] + council_tax_reduction_non_dep_deduction_exempt: [false, true] + housing_benefit_non_dep_deductions: [0, 0] + council_tax_reduction_non_dep_deductions: [0, 0] + +- name: A company's income attributed to its owner is earned income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + stands_as_sole_owner_or_partner_of_company: true + owned_company_carries_on_trade: true + owned_company_income_share: 1_000 + weekly_hours: 0 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 77(3)(b): the company's income is treated as the + # owner's self-employed earnings, so the exemption doesn't apply. Outside + # remunerative work the non-dependant pays the lowest amounts: HB 20.40 + # and the Welsh pensioner scale's 6.70 a week. + housing_benefit_non_dep_deduction_exempt: [false, false] + council_tax_reduction_non_dep_deduction_exempt: [false, false] + housing_benefit_non_dep_deductions: [1_060.80, 0] + council_tax_reduction_non_dep_deductions: [348.40, 0] + +- name: The minimum income floor doesn't apply where a partner's earnings meet the couple threshold + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + current_education: NOT_IN_EDUCATION + non_dependant: + age: 24 + current_education: NOT_IN_EDUCATION + weekly_hours: 0 + employment_income: 0 + uc_mif_applies: true + uc_minimum_income_floor: 10_000 + partner: + age: 30 + current_education: NOT_IN_EDUCATION + weekly_hours: 15 + employment_income: 30_000 + uc_mif_applies: false + uc_minimum_income_floor: 10_000 + benunits: + claimant_unit: + members: [claimant] + benunit_is_rent_liable: true + non_dependant_unit: + members: [non_dependant, partner] + benunit_is_rent_liable: false + universal_credit_pre_benefit_cap: 5000 + uc_benefit_cap: 100_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [claimant, non_dependant, partner] + country: WALES + local_authority: CARDIFF + output: + # UC Regs 2013 reg 62(3): a member of a couple is treated as having the + # floor only while the couple's earned income is below the couple + # threshold. The partner's earned income exceeds it, so the non-dependant + # has none and is exempt. The Welsh working-age scheme deducts for each + # member of a Universal Credit couple (Sch 6 para 5(3)): the exempt + # non-dependant's nil and the partner's 6.70 a week, as the partner works + # under 16 hours. + housing_benefit_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_non_dep_deduction_exempt: [false, true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0, 348.40] + council_tax_reduction_non_dep_deductions: [348.40, 0] + +- name: A third adult's Universal Credit leaves a Welsh non-dependant couple one deduction + period: 2026 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 40 + is_household_head: true + is_claimant_or_partner: true + is_uc_claimant: false + current_education: NOT_IN_EDUCATION + first: + age: 35 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 10_400 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + second: + age: 34 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 5_200 + weekly_hours: 16 + current_education: NOT_IN_EDUCATION + third: + age: 25 + is_claimant_or_partner: false + is_uc_claimant: true + employment_income: 0 + weekly_hours: 0 + current_education: NOT_IN_EDUCATION + benunits: + applicant_unit: + members: [applicant] + would_claim_uc: false + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + non_dependant_unit: + members: [first, second, third] + universal_credit_pre_benefit_cap: 3_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + home: + members: [applicant, first, second, third] + country: WALES + local_authority: CARDIFF + rent: 6_000 + council_tax: 2_000 + tenure_type: RENT_PRIVATELY + savings: 0 + output: + # WSI 2013/3029 Sch 6 para 5(3): only the couple's own Universal Credit + # award takes it out of the one-deduction rule. Here the third adult + # claims alone, so the couple, banded on 15,600 a year (300 a week), pays + # one 13.35 deduction, and the third adult, on Universal Credit with no + # earned income, pays none (para 5(8)(c)). + council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 694.20, 0] + council_tax_reduction_non_dep_deductions: [694.20, 0] + +- name: A third adult's Universal Credit leaves the couple one deduction from each Welsh applicant + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 70 + is_household_head: true + is_claimant_or_partner: true + is_uc_claimant: false + current_education: NOT_IN_EDUCATION + sharer: + age: 30 + is_claimant_or_partner: true + is_uc_claimant: false + current_education: NOT_IN_EDUCATION + first: + age: 35 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 10_400 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + second: + age: 34 + is_claimant_or_partner: true + is_uc_claimant: false + employment_income: 5_200 + hours_worked: 832 + current_education: NOT_IN_EDUCATION + third: + age: 25 + is_claimant_or_partner: false + is_uc_claimant: true + employment_income: 9_100 + hours_worked: 1_040 + current_education: NOT_IN_EDUCATION + benunits: + head_family: + members: [head] + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + would_claim_uc: false + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + non_dependant_family: + members: [first, second, third] + would_claim_uc: false + universal_credit_pre_benefit_cap: 3_000 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 520 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + home: + members: [head, sharer, first, second, third] + country: WALES + local_authority: CARDIFF + rent: 12_000 + council_tax: 2_000 + tenure_type: RENT_PRIVATELY + savings: 0 + output: + # The pensioner head and the working-age sharer each apply under their + # own Welsh scheme and share the deductions equally (Sch 1 para 3(5); Sch + # 6 para 5(5)). The couple, banded on 15,600 + 520 = 16,120 a year (310 + # a week), pays one deduction under either scheme, as the Universal + # Credit is the third adult's; the third adult, banded on 9,100 + 3,000 + # (232.69 a week), pays their own. + council_tax_reduction_non_dep_deductions: [521.30, 521.30, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 270be009a1..1d43da7e1f 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -24,10 +24,12 @@ flags), the claimant family's deductions are 0. 5. Aggregation: a family's deductions are the sum, over the other families in the household, of their claimant and partner's higher amount (both amounts - for a Welsh working-age applicant's non-dependant couple with a Universal - Credit award) plus each other member's, recomputed here from the - individual amounts, times the family's share of the rent (Housing Benefit, - reg 74(5)) or of the council tax liability (CTR, Sch 1 para 8(5)). + for a Welsh working-age applicant's non-dependant couple whose own + Universal Credit award assesses either member; another member's award as + a single claimant leaves the couple one deduction) plus each other + member's, recomputed here from the individual amounts, times the family's + share of the rent (Housing Benefit, reg 74(5)) or of the council tax + liability (CTR, Sch 1 para 8(5)). 6. Monotonicity (metamorphic): raising any non-dependant's income never lowers an individual deduction or a family's deductions. 7. Exhaustive: in every year from 2015 (2019 for Housing Benefit) to 2030, @@ -49,10 +51,10 @@ the model's is_on_* status (HB Regs 2006 reg 2(1), (3), (3A)); where the claimant or partner reports the award, or no one does, that is the reporter or the benefit unit's head, which the test checks. Universal - Credit counts for both joint claimants. Earned income includes statutory - sick, maternity and paternity pay, less relievable pension contributions, - which come off self-employed earnings only when there are no employed - earnings; a self-employed loss counts as nil (UC regs 55(4)-(5), 57(2)). + Credit counts for the claimants it assesses: the members the generated + is_uc_claimant flags name (by default the claimant and partner), the two + eldest where more are flagged, which the test checks. Earned income is + Universal Credit's own calculation (invariant 13). 10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) 11. Banding: a non-dependant in remunerative work who is neither exempt nor on IS, JSA(IB) or ESA(IR) pays the amount of the band whose lower edge is @@ -60,13 +62,23 @@ the generated inputs: earnings, statutory sick, maternity and paternity pay, self-employed profit with a loss as nil, and other investment income; for the claimant or partner of the non-dependant's family, the couple's - sum plus the family's IS, JSA(IB), ESA(IR), Pension Credit and Universal - Credit. Incomes within a penny a week of an edge are skipped, since the + sum plus the family's IS, JSA(IB), ESA(IR) and Pension Credit, and its + Universal Credit where the award assesses either of them. Another member + who claims Universal Credit alone is banded on their own income and that + award. Incomes within a penny a week of an edge are skipped, since the inputs are stored as float32. 12. Differential: the after-cap Universal Credit used in a non-dependant family's gross income equals the model's universal_credit for a family with no Housing Benefit, no Maternity Allowance and no deductions from - its award. + its award, under current law and under an 85% protected minimum floor; + and it reads a stored universal_credit, cap reduction or welfare-benefit + total, and a universal_credit formula a reform replaces. +13. Differential: the Universal Credit exemption's earned-income test agrees + with Universal Credit's own calculation (uc_individual_earned_income) + across employment expenses, trading losses, company income attributed to + its owner, pension contributions and the minimum income floor of single + claimants and couples; and it agrees with the inputs where they settle it + without the tax and National Insurance calculation. """ from pathlib import Path @@ -214,6 +226,13 @@ def households(draw): # Which adult non-dependant reports IS, JSA(IB), ESA(IR) and SPC, if # anyone does. reporter=draw(st.sampled_from([None, 0, 1])), + # Explicit is_uc_claimant flags for the non-dependants, by position, + # or None to leave the claimant and partner as the claimants. A flag + # on the young person alone makes them a single claimant beside the + # couple. + uc_claimants=draw( + st.one_of(st.none(), st.lists(st.booleans(), min_size=3, max_size=3)) + ), claims_child_benefit=draw(st.sampled_from([True, True, False])), non_dependants=non_dependants, ) @@ -272,6 +291,8 @@ def build(population, year, raise_income=False): if benefit != "is_blind" }, ) + if household.get("uc_claimants") is not None: + people[name]["is_uc_claimant"] = household["uc_claimants"][n] members.append(name) persons.append( dict(household=h, benunit=2 * h + 1, non_dependant=nd, position=n) @@ -308,9 +329,9 @@ def calculate(sim, year): "council_tax_reduction_individual_non_dep_deduction", "is_benunit_head", "is_claimant_or_partner", + "is_uc_assessed_claimant", "is_qualifying_young_person_for_child_benefit", - "uc_mif_applies", - "uc_minimum_income_floor", + "uc_individual_earned_income", "is_on_income_support", "is_on_income_based_jsa", "is_on_income_related_esa", @@ -439,6 +460,31 @@ def test_non_dependant_deduction_invariants(population, year): members_of = {} for i, person in enumerate(persons): members_of.setdefault(person["benunit"], []).append(i) + # Universal Credit's claimants: the members is_uc_claimant flags (by + # default the claimant and partner), the two eldest where inputs flag + # more (WRA 2012 ss. 2(1), 40), in the order entered at equal ages. + assessed = r["is_uc_assessed_claimant"].astype(bool) + for members in members_of.values(): + household = population[persons[members[0]]["household"]] + flags = household.get("uc_claimants") + + def flagged(j): + if flags is None or persons[j]["non_dependant"] is None: + return bool(couple[j]) + return flags[persons[j]["position"]] + + def age(j): + nd = persons[j]["non_dependant"] + return household["claimant_age"] if nd is None else nd["age"] + + eldest = sorted(filter(flagged, members), key=lambda j: -age(j))[:2] + assert sorted(j for j in members if assessed[j]) == sorted(eldest) + # Whether a family's Universal Credit is the couple's own: it assesses + # the claimant or partner. + couple_claims_universal_credit = { + b: any(couple[j] and assessed[j] for j in members) + for b, members in members_of.items() + } def payee_of(i, benefit): """The award's payee: the member who reports it, or the head.""" @@ -497,8 +543,8 @@ def band_amount(scale, weekly): # IS, JSA(IB), ESA(IR) and SPC count for their payee (HB reg 2(1), # (3)-(3A)): the model's is_on_* status. Where the reporter, if any, is # the claimant or partner, that is the reporter, or the benefit unit's - # head where none reports. Universal Credit counts for both joint - # claimants. + # head where none reports. Universal Credit counts for the claimants + # it assesses. on = {b: bool(r[IS_ON[b]][i]) for b in REPORTED} reporter = household.get("reporter") reporter_index = ( @@ -515,25 +561,14 @@ def band_amount(scale, weekly): i, ) on["universal_credit_pre_benefit_cap"] = ( - bool(couple[i]) + bool(assessed[i]) and household["benefits"]["universal_credit_pre_benefit_cap"] > 0 ) student = nd["education"] != "NOT_IN_EDUCATION" - # Earned income (UC regs 55(4)-(5), 57(2), 62), in the inputs' float32. - statutory_pay = nd.get("statutory_pay", {}) - employed_gross = f32(nd["earnings"]) + sum( - (f32(statutory_pay.get(name, 0.0)) for name in STATUTORY_PAY), f32(0) - ) - pension = f32(nd.get("pension_contributions", 0.0)) - employed = max(f32(0), employed_gross - pension) - self_employed = max( - f32(0), - max(f32(0), f32(nd["self_employment"])) - - (f32(0) if employed_gross > 0 else pension), - ) - if r["uc_mif_applies"][i]: - self_employed = max(self_employed, f32(r["uc_minimum_income_floor"][i])) - earned = employed + self_employed > 0 + # Earned income is Universal Credit's own calculation (HB reg 74(11), + # CTR Sch 1 para 8(11A) and equivalents: UC Regs 2013 reg 52), which + # invariant 13 checks against the inputs. + earned = r["uc_individual_earned_income"][i] > 0 uc_without_earnings = on["universal_credit_pre_benefit_cap"] and not earned hb_exempt = ( student @@ -619,20 +654,23 @@ def band_amount(scale, weekly): ) family_awards = sum( household["benefits"][b] - for b in [ - "income_support", - "pension_credit", - "universal_credit_pre_benefit_cap", - *couple_awards, - ] + for b in ["income_support", "pension_credit", *couple_awards] ) + # Universal Credit is the income of the claimants it assesses: the + # couple's, or a single claimant's beside it. + universal_credit = household["benefits"]["universal_credit_pre_benefit_cap"] + couple_claims = couple_claims_universal_credit[person["benunit"]] if couple[i]: weekly = ( sum(own_gross(j) for j in members_of[person["benunit"]] if couple[j]) + family_awards + + (universal_credit if couple_claims else 0.0) ) / WEEKS else: - weekly = own_gross(i) / WEEKS + weekly = ( + own_gross(i) + + (universal_credit if assessed[i] and not couple_claims else 0.0) + ) / WEEKS if ( r["housing_benefit_individual_non_dep_deduction_eligible"][i] and nd["hours"] >= hb.remunerative_work_hours @@ -679,6 +717,7 @@ def band_amount(scale, weekly): welsh_working_age = [ population[b // 2]["country"] == "WALES" and not applicant_pensioner[b // 2] + and couple_claims_universal_credit[b] and r["universal_credit_pre_benefit_cap"][b] > 0 for b in range(benunit_count) ] @@ -831,36 +870,9 @@ def test_ctr_scales_match_enacted_sums(): assert list(node.amounts) == pytest.approx(row["amounts"]), (scheme, year) -@PROPERTY_SETTINGS -@given( - families=st.lists( - st.fixed_dictionaries( - dict( - couple=st.booleans(), - universal_credit_pre_benefit_cap=st.one_of( - st.just(0.0), st.floats(0, 40_000) - ), - child_benefit=st.sampled_from([0.0, 1_500.0, 5_000.0]), - child_tax_credit=st.sampled_from([0.0, 3_000.0]), - uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0, 25_323.0]), - uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), - jsa_contrib=st.sampled_from([0.0, 4_000.0]), - esa_contrib=st.sampled_from([0.0, 5_000.0]), - ) - ), - min_size=1, - max_size=8, - ), - year=st.integers(2025, 2030), -) -def test_capped_universal_credit_matches_the_model(families, year): - """Invariant 12: the after-cap Universal Credit counted in a - non-dependant family's gross income equals universal_credit for a family - with no Housing Benefit, Maternity Allowance or deductions.""" - from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - universal_credit_after_benefit_cap, - ) - +def capped_universal_credit_families(families, year, **extra): + """One household per family: the generated awards, no Housing Benefit, + no Maternity Allowance and no deductions from the award.""" people, benunits, households_ = {}, {}, {} for f, family in enumerate(families): members = [f"adult_{f}_{k}" for k in range(2 if family["couple"] else 1)] @@ -887,15 +899,265 @@ def test_capped_universal_credit_matches_the_model(families, year): jsa_income=0.0, esa_income=0.0, housing_benefit_pre_benefit_cap=0.0, + uc_deduction_rate=0.0, uc_deductions=0.0, + **extra, ) households_[f"household_{f}"] = dict(members=members, country="ENGLAND") + return for_years( + dict(people=people, benunits=benunits, households=households_), [year] + ) + + +def capped_universal_credit(sim, year): + from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + universal_credit_after_benefit_cap, + ) + + return universal_credit_after_benefit_cap( + sim.populations["benunit"], year, sim.tax_benefit_system.parameters + ) + + +CAPPED_FAMILY = st.fixed_dictionaries( + dict( + couple=st.booleans(), + universal_credit_pre_benefit_cap=st.one_of(st.just(0.0), st.floats(0, 40_000)), + child_benefit=st.sampled_from([0.0, 1_500.0, 5_000.0]), + child_tax_credit=st.sampled_from([0.0, 3_000.0]), + uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0, 25_323.0]), + uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), + jsa_contrib=st.sampled_from([0.0, 4_000.0]), + esa_contrib=st.sampled_from([0.0, 5_000.0]), + ) +) +ONE_CAPPED_FAMILY = dict( + couple=False, + universal_credit_pre_benefit_cap=20_000.0, + child_benefit=0.0, + child_tax_credit=0.0, + uc_benefit_cap=14_753.0, + uc_childcare_element=0.0, + jsa_contrib=0.0, + esa_contrib=0.0, +) + + +@PROPERTY_SETTINGS +@given( + families=st.lists(CAPPED_FAMILY, min_size=1, max_size=8), + year=st.integers(2025, 2030), + protected_floor=st.sampled_from([0.0, 0.85]), +) +@example(families=[ONE_CAPPED_FAMILY], year=2025, protected_floor=0.85) +def test_capped_universal_credit_matches_the_model(families, year, protected_floor): + """Invariant 12: the after-cap Universal Credit counted in a + non-dependant family's gross income equals universal_credit for a family + with no Housing Benefit, Maternity Allowance or deductions, with or + without a protected minimum floor.""" + reform = ( + { + "gov.dwp.universal_credit.deductions.protected_floor": { + f"{year}-01-01.{year}-12-31": protected_floor + } + } + if protected_floor + else None + ) + sim = Simulation( + situation=capped_universal_credit_families(families, year), reform=reform + ) + np.testing.assert_allclose( + capped_universal_credit(sim, year), + sim.calculate("universal_credit", year), + atol=0.01, + ) + + +def test_capped_universal_credit_reads_stored_and_replaced_values(): + """Invariant 12: a stored award, cap reduction or welfare-benefit total, + and a replaced universal_credit formula, reach the gross income. The + family's Universal Credit before the cap is 20,000 against a 14,753 cap.""" + from policyengine_core.reforms import Reform + from policyengine_core.simulations import Simulation as CoreSimulation + + from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( + universal_credit as current_law, + ) + + year = 2025 + for stored, expected in [ + (dict(), 14_753), + (dict(universal_credit=15_000.0), 15_000), + (dict(uc_benefit_cap_reduction=0.0), 20_000), + (dict(uc_benefit_cap_reduction_after_floor=1_000.0), 19_000), + (dict(benefit_cap_welfare_benefits=10_000.0), 20_000), + ]: + sim = Simulation( + situation=capped_universal_credit_families( + [ONE_CAPPED_FAMILY], year, **stored + ) + ) + helper = capped_universal_credit(sim, year) + assert helper == pytest.approx([expected], abs=0.01), stored + assert helper == pytest.approx( + sim.calculate("universal_credit", year), abs=0.01 + ), stored + + constant_award = type( + "universal_credit", + (current_law,), + dict(formula=lambda benunit, period, parameters: benunit.filled_array(12_345)), + ) + + class ConstantAward(Reform): + def apply(self): + self.update_variable(constant_award) + + sim = CoreSimulation( + tax_benefit_system=ConstantAward(SYSTEM), + situation=capped_universal_credit_families([ONE_CAPPED_FAMILY], year), + default_input_period=year, + default_calculation_period=year, + ) + assert capped_universal_credit(sim, year) == pytest.approx([12_345], abs=0.01) + + +# Earned-income cases for invariant 13. Each sets the person's earnings +# inputs; "settled" is what the inputs alone say, where they say it without +# the tax and National Insurance calculation: no earnings left after +# expenses, pension contributions and trading losses (False), or a small +# positive amount no deduction can remove (True). +EARNINGS = { + "none": (dict(), False), + "wages": (dict(employment_income=1_000.0), True), + "wages_cancelled_by_expenses": ( + dict(employment_income=1_000.0, employment_expenses=1_000.0), + False, + ), + "wages_partly_cancelled_by_expenses": ( + dict(employment_income=1_000.0, employment_expenses=500.0), + True, + ), + "wages_into_pension": ( + dict(employment_income=1_000.0, employee_pension_contributions=1_000.0), + None, + ), + "profit": (dict(self_employment_income=1_000.0), None), + "profit_cancelled_by_loss": ( + dict(self_employment_income=1_000.0, trading_loss=1_000.0), + None, + ), + "loss": (dict(self_employment_income=-500.0), None), + "company_income": ( + dict( + stands_as_sole_owner_or_partner_of_company=True, + owned_company_carries_on_trade=True, + owned_company_income_share=1_000.0, + ), + True, + ), + "company_income_without_owner_treatment": ( + dict(owned_company_carries_on_trade=True, owned_company_income_share=1_000.0), + False, + ), + "high_wages": (dict(employment_income=30_000.0), True), +} + + +@PROPERTY_SETTINGS +@given( + families=st.lists( + st.lists( + st.fixed_dictionaries( + dict( + age=st.integers(18, 89), + earnings=st.sampled_from(sorted(EARNINGS)), + # None leaves the minimum income floor to the model. + floor_applies=st.sampled_from([None, None, True, False]), + ) + ), + min_size=1, + max_size=2, + ), + min_size=1, + max_size=6, + ), + year=st.integers(2025, 2030), +) +@example( + # A couple's floor: the partner's earnings meet the couple threshold, so + # the floor doesn't apply to the other member (UC Regs 2013 reg 62(3)). + families=[ + [ + dict(age=24, earnings="none", floor_applies=True), + dict(age=30, earnings="high_wages", floor_applies=False), + ], + [dict(age=24, earnings="none", floor_applies=True)], + ], + year=2026, +) +def test_earned_income_exemption_reads_universal_credit(families, year): + """Invariant 13: the Universal Credit exemption's earned-income test is + Universal Credit's own calculation (HB Regs 2006 reg 74(8)(b), (11); WSI + 2013/3029 Sch 1 para 3(8)(c), (10)).""" + people, benunits, households_, cases = {}, {}, {}, [] + for f, family in enumerate(families): + claimant = f"claimant_{f}" + people[claimant] = dict(age=90, current_education="NOT_IN_EDUCATION") + cases.append(None) + members = [] + for k, member in enumerate(family): + name = f"member_{f}_{k}" + inputs, settled = EARNINGS[member["earnings"]] + people[name] = dict( + age=member["age"], + current_education="NOT_IN_EDUCATION", + weekly_hours=0.0, + **inputs, + ) + if member["floor_applies"] is not None: + people[name]["uc_mif_applies"] = member["floor_applies"] + people[name]["uc_minimum_income_floor"] = 10_000.0 + members.append(name) + cases.append((member, settled)) + no_awards = {benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS} + benunits[f"claimant_unit_{f}"] = dict( + members=[claimant], benunit_is_rent_liable=True, **no_awards + ) + benunits[f"family_{f}"] = dict( + members=members, + benunit_is_rent_liable=False, + **{**no_awards, "universal_credit_pre_benefit_cap": 1_000.0}, + ) + households_[f"household_{f}"] = dict( + members=[claimant, *members], + country="WALES", + local_authority=COUNTRIES["WALES"], + ) sim = Simulation( situation=for_years( dict(people=people, benunits=benunits, households=households_), [year] ) ) - helper = universal_credit_after_benefit_cap(sim.populations["benunit"], year) - np.testing.assert_allclose( - helper, sim.calculate("universal_credit", year), atol=0.01 + earned = np.asarray(sim.calculate("uc_individual_earned_income", year)) > 0 + claimant_ = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) + floor = np.asarray(sim.calculate("uc_mif_applies", year)) + hb_exempt = np.asarray( + sim.calculate("housing_benefit_non_dep_deduction_exempt", year) + ) + ctr_exempt = np.asarray( + sim.calculate("council_tax_reduction_non_dep_deduction_exempt", year) ) + for i, case in enumerate(cases): + if case is None: + continue + member, settled = case + assert claimant_[i] + # The predicate is Universal Credit's earned income: the Welsh + # pensioner scheme at any age, Housing Benefit under 25. + assert bool(ctr_exempt[i]) == (not earned[i]), (i, member) + assert bool(hb_exempt[i]) == (member["age"] < 25 and not earned[i]), (i, member) + # Where the inputs settle it, and the floor doesn't apply. + if settled is not None and not floor[i]: + assert bool(earned[i]) == settled, (i, member) From 23aba8c80b9ad7d0703a5f324aa16b20bbb64939 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:30 -0500 Subject: [PATCH 36/67] Count ISA interest and dividends in non-dependants' gross income Both gross-income variables now add tax_free_savings_income (ISA interest unless a total is supplied) and individual_savings_account_dividend_income, as main's local CTR schemes do. The documentation describes treating a loss as nil as an approximation: the model holds profits, not the receipts DWP guidance A5.551 counts. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_gross_income.yaml | 17 +++++- ...il_tax_reduction_non_dep_gross_income.yaml | 17 +++++- .../housing_benefit_non_dep_gross_income.py | 57 +++++++++++-------- ...ncil_tax_reduction_non_dep_gross_income.py | 57 +++++++++++-------- 4 files changed, 96 insertions(+), 52 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index 4753918418..a7b9638754 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -613,7 +613,8 @@ housing_benefit_non_dep_gross_income: 3_000 - name: housing_benefit_non_dep_gross_income counts a self-employment or property loss as nil - # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # The model has profits, not the receipts DWP HB guidance A5.551 counts + # before business expenses. As an approximation, a loss counts as nil and # does not reduce other income. period: 2025 input: @@ -623,3 +624,17 @@ property_income: -1_000 output: housing_benefit_non_dep_gross_income: 5_000 + +- name: housing_benefit_non_dep_gross_income counts ISA interest and dividends + # Gross income is earnings and any other income (DWP HB guidance A5.542), + # savings interest included (A5.570). The supplied exempt-interest total + # includes the ISA interest, which counts once. + period: 2025 + input: + social_security_income: 0 + employment_income: 9_000 + individual_savings_account_interest_income: 500 + tax_free_savings_income: 20_000 + individual_savings_account_dividend_income: 30_000 + output: + housing_benefit_non_dep_gross_income: 59_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index 5736b27177..ec89611965 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -733,7 +733,8 @@ council_tax_reduction_non_dep_gross_income: 3_000 - name: council_tax_reduction_non_dep_gross_income counts a self-employment or property loss as nil - # No deduction for business expenses (DWP HB guidance A5.551), so a loss + # The model has profits, not the receipts DWP HB guidance A5.551 counts + # before business expenses. As an approximation, a loss counts as nil and # does not reduce other income. period: 2025 input: @@ -743,3 +744,17 @@ property_income: -1_000 output: council_tax_reduction_non_dep_gross_income: 5_000 + +- name: council_tax_reduction_non_dep_gross_income counts ISA interest and dividends + # Gross income is earnings and any other income (DWP HB guidance A5.542), + # savings interest included (A5.570). The supplied exempt-interest total + # includes the ISA interest, which counts once. + period: 2025 + input: + social_security_income: 0 + employment_income: 9_000 + individual_savings_account_interest_income: 500 + tax_free_savings_income: 20_000 + individual_savings_account_dividend_income: 30_000 + output: + council_tax_reduction_non_dep_gross_income: 59_000 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py index e485277a20..a43c9be498 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py @@ -16,31 +16,33 @@ class housing_benefit_non_dep_gross_income(Variable): "https://www.legislation.gov.uk/uksi/2006/214/regulation/55", ) documentation = ( - "Annual gross income represented by the model for non-dependant " + "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "pension contributions. A self-employment or property loss counts as " - "nil, so it does not reduce other income: gross income from " - "self-employment has no deduction for business expenses (DWP HB " - "guidance A5.551), and the model has profit, not turnover. Claimant " - "war-pension and supplementary-pension disregards do not apply to " - "these non-dependant bands. It includes statutory sick, maternity and " - "paternity pay once through employment_benefits, child and spousal " - "maintenance, the tax-purpose social security income sources, and " - "the separately represented non-taxable periodic benefits. " - "Attendance Allowance, DLA and PIP are omitted under the specific " - "disability-payment disregards. The other disregarded disability " - "and compensation/trust payments have no separate model inputs. " - "Benefit-unit awards (including Child Benefit, tax credits and UC) " - "are not allocated to an individual recipient here. " - "BSP and AFCS inputs do not separate periodic income from capital " - "payments, so they are not added. Gross income includes earnings " - "and any other income (DWP HB guidance A5.542), so other investment " - "income counts. This is a person's own income: a claimant or partner " - "of the non-dependant's family is banded on the couple's joint " - "income, with the family's awards added, in the deduction formula, " - "which also applies the remunerative-work test. The gross-income " - "variable itself does not decide whether someone is in remunerative " - "work or whether a deduction is due." + "pension contributions. DWP HB guidance A5.551 counts a " + "self-employed non-dependant's total income with no deduction for " + "business expenses. The model holds self-employment and property " + "profits, not receipts, so this approximates gross income by adding " + "each profit and counting a loss as nil, which stops a loss reducing " + "other income. The claimant war-pension and supplementary-pension " + "disregards do not apply to these bands. The total includes " + "statutory sick, maternity and paternity pay once, through " + "employment_benefits; child and spousal maintenance; the tax-purpose " + "social security income sources; and the non-taxable periodic " + "benefits the model represents separately. It leaves out Attendance " + "Allowance, DLA and PIP under the disability-payment disregards; the " + "other disregarded disability, compensation and trust payments have " + "no model inputs. BSP and AFCS inputs do not separate periodic income " + "from capital payments, so the total leaves them out. Gross income " + "includes earnings and any other income (A5.542), and A5.570 adds " + "savings interest to earnings, so other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends count. This is " + "the person's own income. Benefit-unit awards, such as Child Benefit, " + "tax credits and Universal Credit, stay out of it: the deduction " + "formula adds them for the claimant and partner of the " + "non-dependant's family, whom it bands on their joint income, and " + "applies the remunerative-work test. This variable decides neither " + "whether someone is in remunerative work nor whether a deduction is " + "due." ) def formula(person, period, parameters): @@ -53,8 +55,11 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", + # Exempt interest, ISA interest unless a total is supplied. + "tax_free_savings_income", "other_investment_income", "dividend_income", + "individual_savings_account_dividend_income", "miscellaneous_income_before_trading_allowance", "maternity_allowance", "iidb", @@ -62,7 +67,9 @@ def formula(person, period, parameters): "war_widows_pension", ], ) - # A loss counts as nil (DWP HB guidance A5.551). + # The model has profits, not the receipts DWP HB guidance A5.551 + # counts before business expenses. As an approximation, a loss + # counts as nil, so it never reduces other income. losses_as_nil = max_(0, person("self_employment_income", period)) + max_( 0, person("property_income", period) ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py index e5bc8c7ec5..86f8ef5a3f 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py @@ -20,31 +20,33 @@ class council_tax_reduction_non_dep_gross_income(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/5", ) documentation = ( - "Annual gross income represented by the model for non-dependant " + "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "pension contributions. A self-employment or property loss counts as " - "nil, so it does not reduce other income: gross income from " - "self-employment has no deduction for business expenses (DWP HB " - "guidance A5.551), and the model has profit, not turnover. Claimant " - "war-pension and supplementary-pension disregards do not apply to " - "these non-dependant bands. It includes statutory sick, maternity and " - "paternity pay once through employment_benefits, child and spousal " - "maintenance, the tax-purpose social security income sources, and " - "the separately represented non-taxable periodic benefits. " - "Attendance Allowance, DLA and PIP are omitted under the specific " - "disability-payment disregards. The other disregarded disability " - "and compensation/trust payments have no separate model inputs. " - "Benefit-unit awards (including Child Benefit, tax credits and UC) " - "are not allocated to an individual recipient here. " - "BSP and AFCS inputs do not separate periodic income from capital " - "payments, so they are not added. Gross income includes earnings " - "and any other income (DWP HB guidance A5.542), so other investment " - "income counts. This is a person's own income: a claimant or partner " - "of the non-dependant's family is banded on the couple's joint " - "income, with the family's awards added, in the deduction formula, " - "which also applies the remunerative-work test. The gross-income " - "variable itself does not decide whether someone is in remunerative " - "work or whether a deduction is due." + "pension contributions. DWP HB guidance A5.551 counts a " + "self-employed non-dependant's total income with no deduction for " + "business expenses. The model holds self-employment and property " + "profits, not receipts, so this approximates gross income by adding " + "each profit and counting a loss as nil, which stops a loss reducing " + "other income. The claimant war-pension and supplementary-pension " + "disregards do not apply to these bands. The total includes " + "statutory sick, maternity and paternity pay once, through " + "employment_benefits; child and spousal maintenance; the tax-purpose " + "social security income sources; and the non-taxable periodic " + "benefits the model represents separately. It leaves out Attendance " + "Allowance, DLA and PIP under the disability-payment disregards; the " + "other disregarded disability, compensation and trust payments have " + "no model inputs. BSP and AFCS inputs do not separate periodic income " + "from capital payments, so the total leaves them out. Gross income " + "includes earnings and any other income (A5.542), and A5.570 adds " + "savings interest to earnings, so other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends count. This is " + "the person's own income. Benefit-unit awards, such as Child Benefit, " + "tax credits and Universal Credit, stay out of it: the deduction " + "formula adds them for the claimant and partner of the " + "non-dependant's family, whom it bands on their joint income, and " + "applies the remunerative-work test. This variable decides neither " + "whether someone is in remunerative work nor whether a deduction is " + "due." ) def formula(person, period, parameters): @@ -57,8 +59,11 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", + # Exempt interest, ISA interest unless a total is supplied. + "tax_free_savings_income", "other_investment_income", "dividend_income", + "individual_savings_account_dividend_income", "miscellaneous_income_before_trading_allowance", "maternity_allowance", "iidb", @@ -66,7 +71,9 @@ def formula(person, period, parameters): "war_widows_pension", ], ) - # A loss counts as nil (DWP HB guidance A5.551). + # The model has profits, not the receipts DWP HB guidance A5.551 + # counts before business expenses. As an approximation, a loss + # counts as nil, so it never reduces other income. losses_as_nil = max_(0, person("self_employment_income", period)) + max_( 0, person("property_income", period) ) From a99ff4e8558141db9a3af87287903dadf6d172e2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:33 -0500 Subject: [PATCH 37/67] Give the English work-gate cases a pension-age applicant Each English case had a sole 30-year-old forced onto the pension-age scheme. It now has a 70-year-old applicant and a separate 30-year-old non-dependant, and the model works out each family's scheme. Expected deductions don't change. Co-Authored-By: Claude Opus 5.5 --- ...x_reduction_non_dep_remunerative_work.yaml | 777 +++++++++++------- 1 file changed, 477 insertions(+), 300 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index 2cd2277ab9..0714fa51e3 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -6,233 +6,330 @@ # 2026/56; Wales 6.70, WSI 2026/13), and 403.85/week of income in work falls # in the band from £279 in England (10.60) and Wales (13.35) and from £286 in # Scotland (10.95). Family awards, which a single non-dependant's gross income -# would count, are held at zero; English cases take the pension-age scheme, -# the only national one in England. +# would count, are held at zero. English cases give the household a +# pension-age applicant, since England's only national scheme is the +# pension-age one, and the model works out each family's scheme. - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 0 - employment_income: 0 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 0 + employment_income: 0 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 0 - council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 0] + council_tax_reduction_non_dep_gross_income: [0, 12000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 780 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 780 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 15 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 15] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 16 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 16] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 1950 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 1950 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 37.5 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 37.5] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (0 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 0 - employment_income: 0 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 0 + employment_income: 0 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 0 - council_tax_reduction_non_dep_gross_income: 12000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 0] + council_tax_reduction_non_dep_gross_income: [0, 12000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (15 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 780 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 780 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 15 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 15] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 16 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 16] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction bands child maintenance only from 16 weekly hours (37.5 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 1950 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 1950 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - weekly_hours: 37.5 - council_tax_reduction_non_dep_gross_income: 21000 - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + weekly_hours: [0, 37.5] + council_tax_reduction_non_dep_gross_income: [0, 21000] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: SCOTLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) period: 2026 @@ -719,31 +816,35 @@ - name: council_tax_reduction charges no new deduction to a non-dependant on Universal Credit without earnings # SI 2012/2885 Sch 1 para 8(8)(c): no deduction for a non-dependant - # entitled to Universal Credit with no earned income. The family is - # pension-age for CTR, so England's national scheme applies. + # entitled to Universal Credit with no earned income. The applicant is of + # pension age, so England's national scheme applies. period: 2026 absolute_error_margin: 0.01 input: people: + applicant: + age: 70 + social_security_income: 0 non_dependant: age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true hours_worked: 0 social_security_income: 0 is_on_income_support: false is_on_income_based_jsa: false is_on_income_related_esa: false benunits: + applicant_unit: + members: [applicant] non_dependant_unit: members: [non_dependant] universal_credit_pre_benefit_cap: 6_000 - council_tax_reduction_pensioner: true households: household: - members: [non_dependant] + members: [applicant, non_dependant] country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: [0] + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (Income Support) # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and @@ -751,26 +852,39 @@ period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: true - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: true + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 0 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-based JSA) # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and @@ -778,26 +892,39 @@ period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: true - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: true + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 0 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (income-related ESA) # The recipient is not in remunerative work (SI 2012/2885 reg 10(6)), and @@ -805,105 +932,155 @@ period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: true + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: true + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 0 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 0] - name: council_tax_reduction excludes each income-related benefit recipient from remunerative work (no excluded benefit) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true - age: 30 - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - iidb: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + iidb: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 832 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 832 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 270.40 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 270.40] - name: ENGLAND council_tax_reduction reads its remunerative work hours parameter (20 weekly hours) period: 2026 absolute_error_margin: 0.01 input: - universal_credit_pre_benefit_cap: 0 - child_tax_credit: 0 - working_tax_credit: 0 - child_benefit: 0 - income_support: 0 - jsa_income: 0 - esa_income: 0 - pension_credit: 0 - council_tax_reduction_pensioner: true gov.local_authorities.england.council_tax_reduction.pensioners.non_dep_deduction.remunerative_work_hours: 20 - age: 30 - country: ENGLAND - council_tax_reduction_individual_non_dep_deduction_eligible: true - hours_worked: 1040 - employment_income: 9000 - maintenance_income: 12000 - social_security_income: 0 - is_on_income_support: false - is_on_income_based_jsa: false - is_on_income_related_esa: false + people: + applicant: + age: 70 + social_security_income: 0 + non_dependant: + age: 30 + hours_worked: 1040 + employment_income: 9000 + maintenance_income: 12000 + social_security_income: 0 + is_on_income_support: false + is_on_income_based_jsa: false + is_on_income_related_esa: false + benunits: + applicant_unit: + members: [applicant] + non_dependant_unit: + members: [non_dependant] + universal_credit_pre_benefit_cap: 0 + child_tax_credit: 0 + working_tax_credit: 0 + child_benefit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + pension_credit: 0 + households: + household: + members: [applicant, non_dependant] + country: ENGLAND output: - council_tax_reduction_individual_non_dep_deduction: 551.20 + council_tax_reduction_pensioner: [true, false] + council_tax_reduction_individual_non_dep_deduction: [0, 551.20] - name: SCOTLAND council_tax_reduction reads its remunerative work hours parameter (16 weekly hours) period: 2026 From c280fb058655b8e3a3b9cd47e7f39c8c516d5868 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:34 -0500 Subject: [PATCH 38/67] Pin a tiny rent share and a partner's PIP A family with a 0.5% share of the rent is a joint occupier, not a non-dependant (HB reg 3(2)(d)), and a partner's PIP daily living component exempts the claimant's family (reg 74(6)). Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 38 +++++++++++++++++++ ...it_non_dep_deductions_claimant_exempt.yaml | 18 +++++++++ 2 files changed, 56 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 40edc163e7..c566a310b0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -129,6 +129,44 @@ share_of_household_rent: [0.5, 0.5] housing_benefit_non_dep_deductions: [0, 0] +- name: A family with any share of the rent is a joint occupier, however small + period: 2025 + absolute_error_margin: 0.001 + input: + people: + person_1: + is_household_head: true + household_benefits_individual_non_dep_deduction: 1 + person_2: + household_benefits_individual_non_dep_deduction: 2 + person_3: + household_benefits_individual_non_dep_deduction: 3 + benunits: + benunit_1: + members: [person_1] + share_of_household_rent: 0.995 + housing_benefit_non_dep_deductions_claimant_exempt: false + benunit_2: + members: [person_2] + # A nil rent leaves the family not liable for any, though it bears a + # share of the payments. + share_of_household_rent: 0.005 + benunit_is_rent_liable: false + housing_benefit_non_dep_deductions_claimant_exempt: false + benunit_3: + members: [person_3] + share_of_household_rent: 0 + benunit_is_rent_liable: false + housing_benefit_non_dep_deductions_claimant_exempt: false + households: + household: + members: [person_1, person_2, person_3] + output: + # HB Regs 2006 reg 3(2)(d): person 2 shares the payments, so is a joint + # occupier, not a non-dependant. Only person 3's 3 is deducted, split by + # the shares: 3 x 0.995 and 3 x 0.005. + housing_benefit_non_dep_deductions: [2.985, 0.015, 0] + # The review case on #2184: an existing claimant in council housing with an # adult child whose only income is £28,912 of other investment income and who # does no paid work. The bands apply only in remunerative work (HB Regs 2006 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml index f4de59feff..ff959df9c7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.yaml @@ -42,6 +42,24 @@ output: housing_benefit_non_dep_deductions_claimant_exempt: true +- name: Partner on the PIP daily living component + period: 2026 + input: + people: + claimant: + age: 40 + partner: + age: 38 + pip_dl_category: STANDARD + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + housing_benefit_non_dep_deductions_claimant_exempt: true + - name: Claimant on Armed Forces Independence Payment period: 2026 input: From e4f5062a450d8aac4ce656487e2c70bdb1ce4a73 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:36 -0500 Subject: [PATCH 39/67] Place the non-dependant property tests by their CI durations Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/tests/shard_durations.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/shard_durations.toml b/policyengine_uk/tests/shard_durations.toml index 2c52eb6701..86e9a1fc10 100644 --- a/policyengine_uk/tests/shard_durations.toml +++ b/policyengine_uk/tests/shard_durations.toml @@ -7,7 +7,10 @@ # all five Python shards of nine runs on 24 October 2026, each test's mean # over the runs: merge queue runs 40050881130, 40059344467, 40063244949, # 40067189795, 40071060766, 40079467297, 40083761750 and 40087883891, and -# pull request run 40095846881. A slow test not here is placed by the hash. +# pull request run 40095846881. The entries for +# test_non_dependant_deduction_properties.py come from pull request run +# 41269333878 alone, below 20 seconds included. A slow test not here is +# placed by the hash. # test_ci_test_shards.py fails if an entry names a test that no longer # exists, so a pull request that renames or removes a listed test should # rename or delete its line here. Regenerate when a shard drifts well above @@ -153,6 +156,7 @@ "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_residence_condition_is_metamorphic" = 101 "policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py::test_lha_category_is_the_claimed_schemes_old_category" = 101 "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_shared_carer_attribution_matches_the_reference_matching" = 101 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_non_dependant_deduction_invariants" = 101 "policyengine_uk/tests/test_dfe_childcare_student_support_properties.py::test_maintenance_loan_household_income_applies_the_age_25_test" = 100 "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_legacy_premium_equals_the_addition_without_young_people" = 100 "policyengine_uk/tests/test_pension_annual_allowance_properties.py::test_extra_contributions_never_raise_tax_by_more_than_the_charge" = 100 @@ -306,6 +310,7 @@ "policyengine_uk/tests/test_carer_overlapping_benefits_properties.py::test_carer_benefit_is_monotonic_in_state_pension" = 60 "policyengine_uk/tests/test_winter_heating_payment_properties.py::test_non_dependants_never_change_anyone_elses_payment" = 60 "policyengine_uk/tests/test_uc_single_claim_properties.py::test_single_standard_allowance_at_the_claimants_own_age" = 60 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_capped_universal_credit_matches_the_model" = 60 "policyengine_uk/tests/test_trading_loss_properties.py::test_zero_profit_trade_loss_cannot_raise_tax_or_reduce_programme_award[universal_credit-2026]" = 59 "policyengine_uk/tests/test_reform_inputs_survive_reset.py::test_a_reform_names_exactly_the_variables_it_leaves_inputs_for" = 59 "policyengine_uk/tests/test_uc_minimum_income_floor_properties.py::test_top_up_never_takes_couple_above_couple_threshold" = 59 @@ -579,3 +584,7 @@ "policyengine_uk/tests/test_disable_simulated_benefits_winter_heating.py::test_a_household_that_moves_is_paid_under_each_years_scheme[LONDON-SCOTLAND]" = 20 "policyengine_uk/tests/test_capital_gains_responses.py::test_revenue_falls_short_of_the_static_estimate" = 20 "policyengine_uk/tests/test_capital_gains_response_default.py::test_employer_cost_branches_hold_the_realisation_response" = 20 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[ENGLAND]" = 14 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[WALES]" = 11 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[SCOTLAND]" = 10 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[HOUSING_BENEFIT]" = 10 From fa93bd179e48d24f61d7b0e61fa5c08689d7665c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 01:52:38 -0500 Subject: [PATCH 40/67] Name who deducts in the notebook; label the exemptions Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/housing-benefit.ipynb | 2 +- .../housing_benefit_non_dep_deduction_exempt.py | 2 +- .../housing_benefit_non_dep_deductions_claimant_exempt.py | 2 +- .../council_tax_reduction_non_dep_deduction_exempt.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 59ae57eb68..64f220a8ba 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -42,7 +42,7 @@ "\n", "1. **Eligible rent**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate for the Housing Benefit category of dwelling (`housing_benefit_LHA_category`: the shared accommodation rate for a young individual with no non-dependant, or a claimant entitled to one bedroom who lacks exclusive use of self-contained accommodation, unless the severe disability premium applies, reg 13D(2)) or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", - "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). Under reg 74 (SI 2006/214 reg 55), a non-dependant aged 18 or over in remunerative work (16 hours a week or more) pays the deduction for the band containing their normal gross weekly income, counting a couple's joint income and including each band's lower edge; one not in remunerative work pays the lowest amount. A couple pays one deduction, the higher. No deduction is made for full-time students, State Pension Credit recipients, or under-25s on Income Support, income-based JSA or Universal Credit without earned income, nor for any non-dependant if the claimant or partner is blind or receives Attendance Allowance, the DLA care component, the PIP daily living component or Armed Forces Independence Payment. The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", + "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). Under reg 74 (SI 2006/214 reg 55), for a non-dependant aged 18 or over in remunerative work (16 hours a week or more) the council deducts the amount for the band containing their normal gross weekly income, counting a couple's joint income and including each band's lower edge; for one not in remunerative work it deducts the lowest amount. It makes one deduction for a couple, the higher. Councils make no deduction for full-time students, State Pension Credit recipients, or under-25s on Income Support, income-based JSA or Universal Credit without earned income, and none for any non-dependant if the claimant or partner is blind or receives Attendance Allowance, the DLA care component, the PIP daily living component or Armed Forces Independence Payment. The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", " If the claimant or partner is on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`housing_benefit_on_passporting_benefit`), the whole of the family's earnings, other income and capital is disregarded ([SI 2006/213 Sch. 4 para. 12](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12), [Sch. 5 para. 4](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4) and [Sch. 6 para. 5](https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5); Northern Ireland: [SR 2006/405 Sch. 5 para. 12](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12), [Sch. 6 para. 4](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4) and [Sch. 7 para. 5](https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5)). `housing_benefit_applicable_income` and `housing_benefit_assessable_capital` are then zero, so the award is that maximum. This applies at any age: a mixed-age couple whose younger member is on one of these benefits is assessed under the working-age regulations ([SI 2006/213 reg. 5(1)(b)](https://www.legislation.gov.uk/uksi/2006/213/regulation/5)). Income Support, income-based Jobseeker's Allowance and income-related Employment and Support Allowance count when a positive amount is paid on the claimant's or partner's award (`in_receipt_of_income_support_jsa_ib_or_esa_ir`, which reads `claimant_or_partner_jsa_income` and `claimant_or_partner_esa_income`). Another member's own award, such as a non-dependant's, is payable to them and does not passport the claimant. A person is on Universal Credit on any day they are entitled to it, whether it is in payment or not ([reg. 2(3B)](https://www.legislation.gov.uk/uksi/2006/213/regulation/2)); PolicyEngine reads that as Universal Credit above nil before the benefit cap and deductions (`is_uc_entitled`), for a family that claims it, so the passport adds no circular dependency through the benefit cap. The universal credit limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). It matters only where Housing Benefit is paid alongside Universal Credit, for specified or temporary accommodation, which the eligibility rules above do not yet model.\n", "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `housing_benefit_benefit_cap_reduction`, the excess of the family's welfare benefits over the Housing Benefit cap (`housing_benefit_benefit_cap`, SI 2006/213 reg 75CA), leaving at least 50 pence a week (reg 75D). The cap does not apply where the claimant or couple is entitled to working tax credit (reg 75E), the claimant or partner receives a benefit listed in reg 75F, or the claim is under the pension-age regulations (reg 5). Universal Credit has its own cap.\n", "\n", diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 3ef4cd0518..9ed6cf1730 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -10,7 +10,7 @@ class housing_benefit_non_dep_deduction_exempt(Variable): value_type = bool entity = Person - label = "Housing Benefit makes no non-dependant deduction for this person" + label = "Exempt from Housing Benefit non-dependant deductions" documentation = ( "Modelled exemptions: a full-time student (SI 2006/213 reg 74(7)(c)-(e); " "SI 2006/214 reg 55(7)(e)); a non-dependant on State Pension Credit " diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 53535fe798..5b612d8181 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -4,7 +4,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): value_type = bool entity = BenUnit - label = "Housing Benefit claimant exempt from all non-dependant deductions" + label = "Exempt from all Housing Benefit non-dependant deductions" documentation = ( "No deduction is made for any non-dependant if the claimant or partner " "is blind or receives Attendance Allowance, the DLA care component, the " diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index 9713b51913..f1914638b8 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -8,7 +8,7 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): value_type = bool entity = Person - label = "national Council Tax Reduction makes no non-dependant deduction for this person" + label = "Exempt from national Council Tax Reduction non-dependant deductions" documentation = ( "Modelled exemptions in the England pensioner, Scottish and Welsh " "schemes: a full-time student; a non-dependant on Income Support, " From 3252a32ef5b019497bc2bc2a5120be410066937c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:27:07 -0500 Subject: [PATCH 41/67] Restate the changelog fragment without a leading bullet Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index bd8311447b..34d291f0da 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -- Apply Housing Benefit and national Council Tax Reduction non-dependant deductions as legislated. Bands include their lower edge. A non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, pays the lowest amount whatever their income. A couple pays one deduction, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and any other adult in the family pays their own. Gross income counts other investment income and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. +The model now makes Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. It makes one deduction for a couple, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and a separate one for any other adult in the family. Gross income counts other investment income, ISA interest and ISA dividends, and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply, and the Universal Credit exemption reads Universal Credit's own calculation of earned income. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. From 65469540da8c53e5bc412d2c27e619d454940e68 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:29:14 -0500 Subject: [PATCH 42/67] Name who exempts ESA(IR) recipients in the CTR exemption documentation Co-Authored-By: Claude Opus 5.5 --- .../council_tax_reduction_non_dep_deduction_exempt.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py index f1914638b8..5c965738bb 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deduction_exempt.py @@ -17,10 +17,10 @@ class council_tax_reduction_non_dep_deduction_exempt(Variable): "partner is on Income Support, income-based JSA or income-related ESA " "and who is not on Universal Credit; an adult for whom someone else is " "entitled to child benefit (LGFA 1992 Sch 1 para 3); and, from the year " - "each scheme added it, " - "one entitled to Universal Credit calculated on no earned income. Wales excludes " - "members of the ESA work-related activity group; with no ESA group " - "input, income-related ESA is treated as exempt. Not modelled: other " + "each scheme added it, one entitled to Universal Credit calculated on " + "no earned income. Welsh councils deduct for members of the ESA " + "work-related activity group; with no ESA group input, the model " + "exempts every income-related ESA recipient. Not modelled: other " "persons disregarded for council tax discounts (LGFA 1992 Sch 1), " "youth training allowances, a normal home elsewhere, and " "absence in hospital or on armed forces operations." From 77b5bc6640c73dc97ef71126d8d2f0073ad39e42 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 02:59:51 -0500 Subject: [PATCH 43/67] Set UC claimant flags for everyone or no one in the property populations An is_uc_claimant input for some people stores False for the rest, so the generated populations now enter it for every person or leave the model's default. The earned-income invariant compares the exemption with Universal Credit's earned income for the claimants the award assesses, and the capped-award invariant sets the protected floor in place on one simulation and checks that the floor never lowers an award. Co-Authored-By: Claude Opus 5.5 --- ...test_non_dependant_deduction_properties.py | 97 +++++++++++-------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 1d43da7e1f..0d4e067f16 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -226,19 +226,21 @@ def households(draw): # Which adult non-dependant reports IS, JSA(IB), ESA(IR) and SPC, if # anyone does. reporter=draw(st.sampled_from([None, 0, 1])), - # Explicit is_uc_claimant flags for the non-dependants, by position, - # or None to leave the claimant and partner as the claimants. A flag - # on the young person alone makes them a single claimant beside the - # couple. - uc_claimants=draw( - st.one_of(st.none(), st.lists(st.booleans(), min_size=3, max_size=3)) - ), + # is_uc_claimant for the non-dependants, by position, where the test + # enters it (explicit_uc_claimants). A flag on the young person alone + # makes them a single claimant beside the couple. + uc_claimants=draw(st.lists(st.booleans(), min_size=3, max_size=3)), claims_child_benefit=draw(st.sampled_from([True, True, False])), non_dependants=non_dependants, ) -def build(population, year, raise_income=False): +def build(population, year, raise_income=False, explicit_uc_claimants=False): + """The generated population as a simulation. With explicit_uc_claimants, + every person's is_uc_claimant is an input: True for each claimant and the + generated flag for each non-dependant. (An input for some people stores + the default, False, for the rest.) Otherwise the model's default applies: + the claimant and partner of each family.""" people, benunits, households_, persons = {}, {}, {}, [] for h, household in enumerate(population): claimant = f"claimant_{h}" @@ -258,6 +260,8 @@ def build(population, year, raise_income=False): if name != "is_blind" }, ) + if explicit_uc_claimants: + people[claimant]["is_uc_claimant"] = True persons.append(dict(household=h, benunit=2 * h, non_dependant=None)) members = [] for n, nd in enumerate(household["non_dependants"]): @@ -291,7 +295,7 @@ def build(population, year, raise_income=False): if benefit != "is_blind" }, ) - if household.get("uc_claimants") is not None: + if explicit_uc_claimants: people[name]["is_uc_claimant"] = household["uc_claimants"][n] members.append(name) persons.append( @@ -381,6 +385,7 @@ def aggregate(individual, persons, couple, benunit_count, each_member): @given( population=st.lists(households(), min_size=1, max_size=12), year=st.integers(2025, 2030), + explicit_uc_claimants=st.booleans(), ) @example( # Rare under random draws: a Welsh working-age applicant's non-dependant @@ -441,9 +446,10 @@ def aggregate(individual, persons, couple, benunit_count, each_member): ], ], year=2026, + explicit_uc_claimants=False, ) -def test_non_dependant_deduction_invariants(population, year): - sim, persons = build(population, year) +def test_non_dependant_deduction_invariants(population, year, explicit_uc_claimants): + sim, persons = build(population, year, explicit_uc_claimants=explicit_uc_claimants) r = calculate(sim, year) p = SYSTEM.parameters(str(year)) hb = p.gov.dwp.housing_benefit.non_dep_deduction @@ -469,8 +475,10 @@ def test_non_dependant_deduction_invariants(population, year): flags = household.get("uc_claimants") def flagged(j): - if flags is None or persons[j]["non_dependant"] is None: + if not explicit_uc_claimants: return bool(couple[j]) + if persons[j]["non_dependant"] is None: + return True return flags[persons[j]["position"]] def age(j): @@ -743,7 +751,12 @@ def band_amount(scale, weekly): ) # 6. Raising non-dependants' incomes never lowers a deduction. - raised, _ = build(population, year, raise_income=True) + raised, _ = build( + population, + year, + raise_income=True, + explicit_uc_claimants=explicit_uc_claimants, + ) s = calculate(raised, year) for name in [ "household_benefits_individual_non_dep_deduction", @@ -943,35 +956,35 @@ def capped_universal_credit(sim, year): ) +PROTECTED_FLOOR = "gov.dwp.universal_credit.deductions.protected_floor" + + @PROPERTY_SETTINGS @given( families=st.lists(CAPPED_FAMILY, min_size=1, max_size=8), year=st.integers(2025, 2030), - protected_floor=st.sampled_from([0.0, 0.85]), ) -@example(families=[ONE_CAPPED_FAMILY], year=2025, protected_floor=0.85) -def test_capped_universal_credit_matches_the_model(families, year, protected_floor): +@example(families=[ONE_CAPPED_FAMILY], year=2025) +def test_capped_universal_credit_matches_the_model(families, year): """Invariant 12: the after-cap Universal Credit counted in a non-dependant family's gross income equals universal_credit for a family - with no Housing Benefit, Maternity Allowance or deductions, with or - without a protected minimum floor.""" - reform = ( - { - "gov.dwp.universal_credit.deductions.protected_floor": { - f"{year}-01-01.{year}-12-31": protected_floor - } - } - if protected_floor - else None - ) - sim = Simulation( - situation=capped_universal_credit_families(families, year), reform=reform - ) + with no Housing Benefit, Maternity Allowance or deductions, under current + law and under an 85% protected minimum floor. The floor is set in place + on the same simulation, which costs far less than a reformed one.""" + sim = Simulation(situation=capped_universal_credit_families(families, year)) + current_law = np.asarray(sim.calculate("universal_credit", year)) np.testing.assert_allclose( - capped_universal_credit(sim, year), - sim.calculate("universal_credit", year), - atol=0.01, + capped_universal_credit(sim, year), current_law, atol=0.01 ) + sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( + period=str(year), value=0.85 + ) + sim.tax_benefit_system.reset_parameter_caches() + sim.reset_calculations() + floored = np.asarray(sim.calculate("universal_credit", year)) + np.testing.assert_allclose(capped_universal_credit(sim, year), floored, atol=0.01) + # The floor only limits the reduction, so no award falls. + assert np.all(floored >= current_law - 0.01) def test_capped_universal_credit_reads_stored_and_replaced_values(): @@ -1141,7 +1154,7 @@ def test_earned_income_exemption_reads_universal_credit(families, year): ) ) earned = np.asarray(sim.calculate("uc_individual_earned_income", year)) > 0 - claimant_ = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) + assessed = np.asarray(sim.calculate("is_uc_assessed_claimant", year)) floor = np.asarray(sim.calculate("uc_mif_applies", year)) hb_exempt = np.asarray( sim.calculate("housing_benefit_non_dep_deduction_exempt", year) @@ -1153,11 +1166,17 @@ def test_earned_income_exemption_reads_universal_credit(families, year): if case is None: continue member, settled = case - assert claimant_[i] - # The predicate is Universal Credit's earned income: the Welsh - # pensioner scheme at any age, Housing Benefit under 25. - assert bool(ctr_exempt[i]) == (not earned[i]), (i, member) - assert bool(hb_exempt[i]) == (member["age"] < 25 and not earned[i]), (i, member) + # The predicate is Universal Credit's earned income, for a claimant + # the award assesses (the model pairs claimant and partner, and may + # leave a second adult outside the couple): the Welsh pensioner + # scheme at any age, Housing Benefit under 25. + on_universal_credit = bool(assessed[i]) + uc_without_earnings = on_universal_credit and not earned[i] + assert bool(ctr_exempt[i]) == uc_without_earnings, (i, member) + assert bool(hb_exempt[i]) == (member["age"] < 25 and uc_without_earnings), ( + i, + member, + ) # Where the inputs settle it, and the floor doesn't apply. if settled is not None and not floor[i]: assert bool(earned[i]) == settled, (i, member) From 0ef6f871b9a0aef3e5f7683d0d862d355fd70514 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 03:13:13 -0500 Subject: [PATCH 44/67] Name who acts in the new documentation and test comments Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_non_dep_deductions.yaml | 6 +-- .../non_dependant_deduction_review_cases.yaml | 38 +++++++++---------- ...x_reduction_non_dep_remunerative_work.yaml | 4 +- ...test_non_dependant_deduction_properties.py | 4 +- .../non_dep_deduction/_non_dependants.py | 24 ++++++------ .../housing_benefit_non_dep_gross_income.py | 2 +- .../benefit_cap/uc_benefit_cap_reduction.py | 4 +- .../uc_benefit_cap_reduction_after_floor.py | 4 +- ...ouncil_tax_reduction_non_dep_deductions.py | 10 ++--- ...ncil_tax_reduction_non_dep_gross_income.py | 2 +- 10 files changed, 50 insertions(+), 48 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index c566a310b0..8b11ac331d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -162,9 +162,9 @@ household: members: [person_1, person_2, person_3] output: - # HB Regs 2006 reg 3(2)(d): person 2 shares the payments, so is a joint - # occupier, not a non-dependant. Only person 3's 3 is deducted, split by - # the shares: 3 x 0.995 and 3 x 0.005. + # HB Regs 2006 reg 3(2)(d): person 2 shares the payments, which makes + # them a joint occupier, not a non-dependant. The council deducts only + # person 3's 3, split by the shares: 3 x 0.995 and 3 x 0.005. housing_benefit_non_dep_deductions: [2.985, 0.015, 0] # The review case on #2184: an existing claimant in council housing with an diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 49605e630c..c11dc6b3b0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1312,8 +1312,8 @@ local_authority: CARDIFF output: # UC Regs 2013 reg 57(2): the trades' profits and losses are added - # together before any nil floor, so the loss cancels the profit. The - # minimum income floor is held off, so only the loss rule decides. + # together before any nil floor, so the loss cancels the profit. The case + # holds the minimum income floor off, so only the loss rule decides. housing_benefit_non_dep_deduction_exempt: [false, true] council_tax_reduction_non_dep_deduction_exempt: [false, true] housing_benefit_non_dep_deductions: [0, 0] @@ -1356,10 +1356,10 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 77(3)(b): the company's income is treated as the - # owner's self-employed earnings, so the exemption doesn't apply. Outside - # remunerative work the non-dependant pays the lowest amounts: HB 20.40 - # and the Welsh pensioner scale's 6.70 a week. + # UC Regs 2013 reg 77(3)(b) treats the company's income as the owner's + # self-employed earnings, so the exemption doesn't apply. Outside + # remunerative work the councils deduct the lowest amounts: HB 20.40 and + # the Welsh pensioner scale's 6.70 a week. housing_benefit_non_dep_deduction_exempt: [false, false] council_tax_reduction_non_dep_deduction_exempt: [false, false] housing_benefit_non_dep_deductions: [1_060.80, 0] @@ -1409,13 +1409,13 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 62(3): a member of a couple is treated as having the - # floor only while the couple's earned income is below the couple + # UC Regs 2013 reg 62(3) treats a member of a couple as having the floor + # only while the couple's earned income falls short of the couple # threshold. The partner's earned income exceeds it, so the non-dependant - # has none and is exempt. The Welsh working-age scheme deducts for each - # member of a Universal Credit couple (Sch 6 para 5(3)): the exempt - # non-dependant's nil and the partner's 6.70 a week, as the partner works - # under 16 hours. + # has none, and the exemption applies. The Welsh working-age scheme + # deducts for each member of a Universal Credit couple (Sch 6 para 5(3)): + # nil for the exempt non-dependant and 6.70 a week for the partner, who + # works under 16 hours. housing_benefit_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_individual_non_dep_deduction: [0, 0, 348.40] @@ -1487,9 +1487,9 @@ output: # WSI 2013/3029 Sch 6 para 5(3): only the couple's own Universal Credit # award takes it out of the one-deduction rule. Here the third adult - # claims alone, so the couple, banded on 15,600 a year (300 a week), pays - # one 13.35 deduction, and the third adult, on Universal Credit with no - # earned income, pays none (para 5(8)(c)). + # claims alone, so the council bands the couple on 15,600 a year (300 a + # week) and makes one 13.35 deduction. It deducts nothing for the third + # adult, on Universal Credit with no earned income (para 5(8)(c)). council_tax_reduction_individual_non_dep_deduction: [0, 694.20, 694.20, 0] council_tax_reduction_non_dep_deductions: [694.20, 0] @@ -1575,8 +1575,8 @@ output: # The pensioner head and the working-age sharer each apply under their # own Welsh scheme and share the deductions equally (Sch 1 para 3(5); Sch - # 6 para 5(5)). The couple, banded on 15,600 + 520 = 16,120 a year (310 - # a week), pays one deduction under either scheme, as the Universal - # Credit is the third adult's; the third adult, banded on 9,100 + 3,000 - # (232.69 a week), pays their own. + # 6 para 5(5)). The council bands the couple on 15,600 + 520 = 16,120 a + # year (310 a week) and makes one deduction under either scheme, as the + # Universal Credit is the third adult's. It bands the third adult on + # 9,100 + 3,000 (232.69 a week) and deducts for them separately. council_tax_reduction_non_dep_deductions: [521.30, 521.30, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index 0714fa51e3..d3bbe37bea 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -7,8 +7,8 @@ # in the band from £279 in England (10.60) and Wales (13.35) and from £286 in # Scotland (10.95). Family awards, which a single non-dependant's gross income # would count, are held at zero. English cases give the household a -# pension-age applicant, since England's only national scheme is the -# pension-age one, and the model works out each family's scheme. +# pension-age applicant, since England's national scheme covers pension-age +# applicants only, and the model works out each family's scheme. - name: ENGLAND council_tax_reduction bands IIDB only from 16 weekly hours (0 weekly hours) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 0d4e067f16..a19429282f 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -969,8 +969,8 @@ def test_capped_universal_credit_matches_the_model(families, year): """Invariant 12: the after-cap Universal Credit counted in a non-dependant family's gross income equals universal_credit for a family with no Housing Benefit, Maternity Allowance or deductions, under current - law and under an 85% protected minimum floor. The floor is set in place - on the same simulation, which costs far less than a reformed one.""" + law and under an 85% protected minimum floor. The test sets the floor in + place on the same simulation, which costs far less than a reformed one.""" sim = Simulation(situation=capped_universal_credit_families(families, year)) current_law = np.asarray(sim.calculate("universal_credit", year)) np.testing.assert_allclose( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 827c86f9a4..6ab90efa2c 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -87,9 +87,10 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco def reads_simulation_value(benunit, period, variable_name, default_formula=None): - """Whether to read the variable rather than recalculate it here: a value - stored as an input for the period, a neutralized variable, or, where - default_formula is given, a formula a reform has replaced.""" + """Whether to read the variable instead of recalculating it here: the + simulation stores an input for the period, a reform neutralized the + variable or, where the caller passes default_formula, replaced its + formula.""" variable = benunit.simulation.tax_benefit_system.get_variable(variable_name) if variable.is_neutralized: return True @@ -102,7 +103,7 @@ def reads_simulation_value(benunit, period, variable_name, default_formula=None) def universal_credit_after_benefit_cap(benunit, period, parameters): """A family's Universal Credit award after the benefit cap (UC Regs 2013 - reg 81), for a family that is not liable for rent. universal_credit and + reg 81), for a family with no liability for rent. universal_credit and uc_benefit_cap_reduction depend on Housing Benefit through the welfare benefits they count, so this applies the cap to the same welfare benefits other than Housing Benefit, which such a family does not receive (UC reg @@ -110,13 +111,14 @@ def universal_credit_after_benefit_cap(benunit, period, parameters): (benefit_cap_couple; reg 79(1)). It shares the award's own calculation of the reduction (benefit_cap_reduction_of) and of the part a protected minimum floor lets it take (benefit_cap_reduction_applied). Deductions - paid from the award to third parties or for debts are still the family's - income, so they are not taken off, and the floor is applied as for a - family with none: the deduction rate depends on the cap reduction, and so - on Housing Benefit. A universal_credit stored as an input or given a new - formula by a reform is read as it is, and so is a stored or neutralized - uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or - benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" + DWP pays from the award to third parties or for debts stay the family's + income, so this keeps them in and applies the floor as for a family with + none: the deduction rate depends on the cap reduction, and so on Housing + Benefit. Where the simulation stores universal_credit as an input, or a + reform replaces its formula, this reads it as it stands. It also reads a + stored or neutralized uc_benefit_cap_reduction_after_floor, + uc_benefit_cap_reduction or benefit_cap_welfare_benefits, the last with + Housing Benefit at nil.""" if reads_simulation_value( benunit, period, "universal_credit", universal_credit_variable.formula ): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py index a43c9be498..e591cca6ab 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py @@ -55,7 +55,7 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest, ISA interest unless a total is supplied. + # Exempt interest: ISA interest unless the inputs supply a total. "tax_free_savings_income", "other_investment_income", "dividend_income", diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index adb72a2912..36defbb6a3 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -4,8 +4,8 @@ def benefit_cap_reduction_of(benunit, period, welfare_benefits): """The regulation 81 reduction for welfare benefits that already count Housing Benefit as nil (reg. 80(2A)): the excess over the Universal Credit - cap, minus the childcare costs element, and nothing where that element is - greater than the excess or the family has no award (reg. 78(1)). The + cap, minus the childcare costs element, and nothing where that element + exceeds the excess or the family has no award (reg. 78(1)). The non-dependant deductions call this too, with their own total of welfare benefits (housing_benefit/non_dep_deduction/_non_dependants.py).""" excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py index dbe7c0b409..615ff5c537 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py @@ -7,8 +7,8 @@ def benefit_cap_reduction_applied( benunit, period, parameters, cap_reduction, deduction_demand ): - """The part of a regulation 81 reduction taken off the award, given the - deductions demanded from it, under any protected minimum floor. The + """The part of a regulation 81 reduction that comes off the award, given + the deductions it must also meet, under any protected minimum floor. The non-dependant deductions call this too (housing_benefit/non_dep_deduction/_non_dependants.py).""" p = parameters(period).gov.dwp.universal_credit.deductions diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py index 99297c3b37..7eeaaab4fa 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.py @@ -38,11 +38,11 @@ class council_tax_reduction_non_dep_deductions(Variable): def formula(benunit, period, parameters): person = benunit.members country = benunit.household("country", period) - # WSI 2013/3029 Sch 6 para 5(3): a working-age applicant's deduction is - # for each member of a non-dependant couple with a Universal Credit - # award. The award is the couple's own: one that assesses the - # claimant or partner. Another member's award, as a single claimant, - # leaves the couple with one deduction. + # WSI 2013/3029 Sch 6 para 5(3): from a working-age applicant's claim + # the council deducts for each member of a non-dependant couple with a + # Universal Credit award. The award must be the couple's own, one that + # assesses the claimant or partner. Another member's award as a single + # claimant leaves the couple one deduction. couple_claims_universal_credit = benunit.any( person("is_uc_assessed_claimant", period) & person("is_claimant_or_partner", period) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py index 86f8ef5a3f..fe3e109e02 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py @@ -59,7 +59,7 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest, ISA interest unless a total is supplied. + # Exempt interest: ISA interest unless the inputs supply a total. "tax_free_savings_income", "other_investment_income", "dividend_income", From 8371842e18d2d462bfb258ed77b33926df5ffc80 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 03:50:11 -0500 Subject: [PATCH 45/67] Calculate the capped award locally when a replaced award cycles A reform that scales main's universal_credit formula still reads the cap reduction, and so Housing Benefit and the non-dependant deduction. The deduction now catches that cycle and calculates the award without Housing Benefit, as for current law, and a test pins the result. Co-Authored-By: Claude Opus 5.5 --- ...test_non_dependant_deduction_properties.py | 64 +++++++++++++++++++ .../non_dep_deduction/_non_dependants.py | 18 ++++-- 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index a19429282f..96d2ad93bd 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -1035,6 +1035,70 @@ def apply(self): ) assert capped_universal_credit(sim, year) == pytest.approx([12_345], abs=0.01) + # A replacement that scales the current-law award still reads the cap + # reduction, and so Housing Benefit and this deduction. The gross income + # then uses the award calculated without Housing Benefit, and nothing + # cycles: the non-dependant's deduction matches current law's. + scaled_award = type( + "universal_credit", + (current_law,), + dict( + formula=lambda benunit, period, parameters: ( + 0.9 * current_law.formula(benunit, period, parameters) + ) + ), + ) + + class ScaledAward(Reform): + def apply(self): + self.update_variable(scaled_award) + + situation = for_years( + dict( + people=dict( + claimant=dict(age=70), + non_dependant=dict( + age=24, + current_education="NOT_IN_EDUCATION", + employment_income=5_000.0, + weekly_hours=20.0, + ), + ), + benunits=dict( + claimant_unit=dict(members=["claimant"], benunit_is_rent_liable=True), + non_dependant_unit=dict( + members=["non_dependant"], + benunit_is_rent_liable=False, + would_claim_uc=True, + ), + ), + households=dict( + household=dict( + members=["claimant", "non_dependant"], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=6_000.0, + tenure_type="RENT_PRIVATELY", + ) + ), + ), + [year], + ) + current = Simulation(situation=situation) + scaled = CoreSimulation( + tax_benefit_system=ScaledAward(SYSTEM), + situation=situation, + default_input_period=year, + default_calculation_period=year, + ) + assert scaled.calculate("universal_credit", year) == pytest.approx( + 0.9 * current.calculate("universal_credit", year), abs=0.01 + ) + for name in ["household_benefits_individual_non_dep_deduction", "housing_benefit"]: + assert scaled.calculate(name, year) == pytest.approx( + current.calculate(name, year), abs=0.01 + ), name + # Earned-income cases for invariant 13. Each sets the person's earnings # inputs; "settled" is what the inputs alone say, where they say it without diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 6ab90efa2c..284b9739c0 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -1,6 +1,8 @@ """Shared definitions for Housing Benefit and Council Tax Reduction non-dependant deductions.""" +from policyengine_core.errors import CycleError + from policyengine_uk.model_api import * from policyengine_uk.utils.benefit_cap import benefit_cap_couple from policyengine_uk.utils.benefit_unit import add_for_members, award_of_couple @@ -115,14 +117,20 @@ def universal_credit_after_benefit_cap(benunit, period, parameters): income, so this keeps them in and applies the floor as for a family with none: the deduction rate depends on the cap reduction, and so on Housing Benefit. Where the simulation stores universal_credit as an input, or a - reform replaces its formula, this reads it as it stands. It also reads a - stored or neutralized uc_benefit_cap_reduction_after_floor, - uc_benefit_cap_reduction or benefit_cap_welfare_benefits, the last with - Housing Benefit at nil.""" + reform replaces its formula, this reads it as it stands, unless the new + formula still reads the cap reduction and so this deduction; then it + calculates the award here. It also reads a stored or neutralized + uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or + benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" if reads_simulation_value( benunit, period, "universal_credit", universal_credit_variable.formula ): - return benunit("universal_credit", period) + try: + return benunit("universal_credit", period) + except CycleError: + # Core unwinds the calculation; the award below doesn't read + # Housing Benefit. + pass if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction_after_floor"): applied = benunit("uc_benefit_cap_reduction_after_floor", period) else: From 97f792542ad0ab21c6016414b7a53378ab197ef4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 03:59:30 -0500 Subject: [PATCH 46/67] Name who acts in the property tests' invariant list Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_deduction_properties.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 96d2ad93bd..a3175307e3 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -53,7 +53,7 @@ reporter or the benefit unit's head, which the test checks. Universal Credit counts for the claimants it assesses: the members the generated is_uc_claimant flags name (by default the claimant and partner), the two - eldest where more are flagged, which the test checks. Earned income is + eldest where the inputs flag more, which the test checks. Earned income is Universal Credit's own calculation (invariant 13). 10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) 11. Banding: a non-dependant in remunerative work who is neither exempt nor @@ -63,10 +63,10 @@ pay, self-employed profit with a loss as nil, and other investment income; for the claimant or partner of the non-dependant's family, the couple's sum plus the family's IS, JSA(IB), ESA(IR) and Pension Credit, and its - Universal Credit where the award assesses either of them. Another member - who claims Universal Credit alone is banded on their own income and that - award. Incomes within a penny a week of an edge are skipped, since the - inputs are stored as float32. + Universal Credit where the award assesses either of them. The council + bands another member who claims Universal Credit alone on their own + income and that award. The test skips incomes within a penny a week of an + edge, since the inputs hold float32 values. 12. Differential: the after-cap Universal Credit used in a non-dependant family's gross income equals the model's universal_credit for a family with no Housing Benefit, no Maternity Allowance and no deductions from From 23de760a627c9989d196244597ba09b4a9a18fc3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 06:46:06 -0500 Subject: [PATCH 47/67] Read universal_credit in the deductions; give UC's cap a basket without HB Universal Credit's cap reduction read benefit_cap_welfare_benefits minus Housing Benefit, so universal_credit depended on Housing Benefit and the non-dependant deductions rebuilt the award with a caught CycleError. uc_benefit_cap_welfare_benefits now holds the Universal Credit cap's benefits with Housing Benefit at nil (UC Regs 2013 reg 80(2A)), from the same lists as benefit_cap_welfare_benefits. For a supplied combined total it subtracts housing_benefit_pre_benefit_cap, as uc_benefit_cap_reduction did. The Housing Benefit cap keeps the combined total. The deductions read universal_credit plus the debt deductions taken from a positive award (WRA 2012 s.8(1); SI 2013/380 Sch 6 para 2(1); SI 2013/383 reg 10; SI 2013/384 regs 10-11). The catch, the reconstruction, reads_simulation_value and the shared cap functions go; the UC cap files return to main's code apart from the basket. With no non-dependant in any household, housing_benefit_non_dep_deductions returns nil before reading income, so a supplied total still works for a single family. Co-Authored-By: Claude Opus 5.5 --- .../non_dependant_deduction_review_cases.yaml | 44 ++- .../uc_benefit_cap_welfare_benefits.yaml | 61 ++++ ...test_non_dependant_deduction_properties.py | 311 ++++++++++++------ policyengine_uk/utils/supplied_inputs.py | 3 - .../non_dep_deduction/_non_dependants.py | 131 +------- .../housing_benefit_non_dep_deductions.py | 7 + .../benefit_cap/uc_benefit_cap_reduction.py | 27 +- .../uc_benefit_cap_reduction_after_floor.py | 28 +- .../uc_benefit_cap_welfare_benefits.py | 75 +++++ 9 files changed, 402 insertions(+), 285 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index c11dc6b3b0..c9cc019a0d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1,6 +1,6 @@ -# Counterexamples from the independent review of PolicyEngine/policyengine-uk#2007, -# end to end for Housing Benefit (HB Regs 2006 reg 74) and the England pensioner -# Council Tax Reduction scheme (SI 2012/2885 Sch 1 para 8), 2026-27 amounts. +# End-to-end cases for the non-dependant deductions of Housing Benefit (HB Regs +# 2006 reg 74) and the England pensioner Council Tax Reduction scheme (SI +# 2012/2885 Sch 1 para 8), at 2026-27 amounts. # The claimant family is the rent-liable family with the household's oldest # member. Family benefits are held at zero except where a case tests them. @@ -166,8 +166,6 @@ housing_benefit_non_dep_deductions: [1_060.80, 0] council_tax_reduction_non_dep_deductions: [270.40, 0] -# Round 2 of the independent review. - - name: Statutory maternity pay is earned income for the Universal Credit exemption period: 2026 absolute_error_margin: 0.01 @@ -426,8 +424,8 @@ housing_benefit_non_dep_deduction_exempt: [false, false, false] housing_benefit_non_dep_deductions: [3_497.00, 0] -# Round 3: partners are the SSCBA s.137 couple (is_claimant_or_partner), not -# whoever is not a child benefit child or qualifying young person. +# Partners are the SSCBA s.137 couple (is_claimant_or_partner), not whoever is +# not a child benefit child or qualifying young person. - name: A blind 16-year-old partner exempts a Welsh 19-year-old applicant period: 2026 @@ -569,9 +567,8 @@ council_tax_reduction_applicant_has_non_dep_exemption: [true, false] merton_council_tax_reduction_non_dep_deductions: [0, 0] -# Round 3 of the independent review: gross and earned income, the Scottish -# working-age scheme, the benefit cap's childcare protection, and child -# benefit entitlement. +# Gross and earned income, the Scottish working-age scheme, the benefit cap's +# childcare protection, and child benefit entitlement. - name: Statutory maternity pay counts towards a non-dependant couple's gross income period: 2026 @@ -1008,9 +1005,7 @@ council_tax_reduction_non_dep_deduction_exempt: [false, false, false] council_tax_reduction_non_dep_deductions: [540.80, 0] -# Round 4 of the independent review. - -- name: Maternity Allowance counts towards the benefit cap on a non-dependant's Universal Credit +- name: A non-dependant family's Universal Credit counts after the benefit cap period: 2026 absolute_error_margin: 0.01 input: @@ -1068,13 +1063,17 @@ country: ENGLAND local_authority: MAIDSTONE output: - # Welfare Reform Act 2012 s.96(10)(i): Maternity Allowance is a capped - # benefit. Capped benefits are 25,129.80, 3,109.80 over the cap, so UC - # is 10,890.20 (UC reg 81). Joint gross income is 9,200 wages + 6,000 MA - # + 10,890.20 UC + 5,129.80 child benefit = 600.38 a week: the HB 485 - # band (119.85) and the CTR 485 band (13.30). - housing_benefit_non_dep_deductions: [6_232.20, 0] - council_tax_reduction_non_dep_deductions: [691.60, 0] + # The Universal Credit cap counts 14,000 UC and 5,129.80 child benefit, + # 19,129.80, under the 22,020 cap, so UC stays 14,000 (UC reg 81). Joint + # gross income is 9,200 wages + 6,000 Maternity Allowance + 14,000 UC + + # 5,129.80 child benefit = 660.19 a week: the top bands, 131.45 HB and + # 15.95 CTR. Welfare Reform Act 2012 s.96(10)(i) makes Maternity + # Allowance a welfare benefit, which the model's cap leaves out (#2784). + # Counting it gives 25,129.80, 3,109.80 over the cap, so UC of 10,890.20, + # 600.38 a week and the 485 bands: 6,232.20 HB and 691.60 CTR. + universal_credit: [0, 14_000] + housing_benefit_non_dep_deductions: [6_835.40, 0] + council_tax_reduction_non_dep_deductions: [829.40, 0] - name: A self-employment loss does not cancel a non-dependant's gross wages period: 2026 @@ -1173,8 +1172,6 @@ council_tax_reduction_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_non_dep_deductions: [288.60, 0] -# Round 6 of the independent review. - - name: A young person's own Maternity Allowance does not cap the family's Universal Credit period: 2026 absolute_error_margin: 0.01 @@ -1227,8 +1224,7 @@ housing_benefit_non_dep_deductions: [6_232.20, 0] council_tax_reduction_non_dep_deductions: [691.60, 0] -# Round 7 of the independent review: the Universal Credit exemption reads -# Universal Credit's own calculation of earned income (uc_individual_earned_income). +# The Universal Credit exemption reads uc_individual_earned_income. - name: Employment expenses that cancel the wages leave no earned income period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml new file mode 100644 index 0000000000..61a1b2cfbb --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/uc_benefit_cap_welfare_benefits.yaml @@ -0,0 +1,61 @@ +# The welfare benefits the Universal Credit cap counts, with Housing Benefit +# at nil (UC Regs 2013 reg 80(2A)). + +- name: The Universal Credit cap leaves out Housing Benefit the family receives + period: 2025 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 30 + benunits: + family: + members: [adult] + universal_credit_pre_benefit_cap: 15_000 + housing_benefit_pre_benefit_cap: 10_000 + child_benefit: 5_000 + child_tax_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + uc_benefit_cap: 14_753 + uc_childcare_element: 0 + uc_deductions: 0 + households: + household: + members: [adult] + country: ENGLAND + output: + # The Housing Benefit cap counts all three awards (HB Regs 2006 reg + # 75C(2)(b)); the Universal Credit cap counts 15,000 + 5,000 = 20,000, + # 5,247 over the cap (reg 81). + benefit_cap_welfare_benefits: 30_000 + uc_benefit_cap_welfare_benefits: 20_000 + uc_benefit_cap_reduction: 5_247 + universal_credit: 9_753 + +- name: A supplied benefit cap total includes Housing Benefit, which the Universal Credit cap takes out + period: 2025 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 30 + benunits: + family: + members: [adult] + universal_credit_pre_benefit_cap: 20_000 + housing_benefit_pre_benefit_cap: 6_000 + benefit_cap_welfare_benefits: 26_000 + uc_benefit_cap: 14_753 + uc_childcare_element: 0 + uc_deductions: 0 + households: + household: + members: [adult] + country: ENGLAND + output: + # 26,000 - 6,000 = 20,000, 5,247 over the cap, so the award is 14,753. + uc_benefit_cap_welfare_benefits: 20_000 + uc_benefit_cap_reduction: 5_247 + universal_credit: 14_753 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index a3175307e3..4c893f62ef 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -81,6 +81,7 @@ without the tax and National Insurance calculation. """ +from functools import lru_cache from pathlib import Path import numpy as np @@ -90,6 +91,7 @@ from hypothesis import strategies as st from policyengine_uk import CountryTaxBenefitSystem, Simulation +from policyengine_uk.model_api import YEAR, BenUnit, Variable PROPERTY_SETTINGS = settings( max_examples=10, @@ -884,8 +886,8 @@ def test_ctr_scales_match_enacted_sums(): def capped_universal_credit_families(families, year, **extra): - """One household per family: the generated awards, no Housing Benefit, - no Maternity Allowance and no deductions from the award.""" + """One household per family: the generated awards and debt deduction + rate, no Housing Benefit and no Maternity Allowance.""" people, benunits, households_ = {}, {}, {} for f, family in enumerate(families): members = [f"adult_{f}_{k}" for k in range(2 if family["couple"] else 1)] @@ -896,6 +898,7 @@ def capped_universal_credit_families(families, year, **extra): esa_contrib=family["esa_contrib"] if k == 0 else 0.0, maternity_allowance=0.0, ) + rate = family.get("uc_deduction_rate", 0.0) benunits[f"family_{f}"] = dict( members=members, **{ @@ -912,8 +915,8 @@ def capped_universal_credit_families(families, year, **extra): jsa_income=0.0, esa_income=0.0, housing_benefit_pre_benefit_cap=0.0, - uc_deduction_rate=0.0, - uc_deductions=0.0, + uc_deduction_rate=rate, + **(dict() if rate else dict(uc_deductions=0.0)), **extra, ) households_[f"household_{f}"] = dict(members=members, country="ENGLAND") @@ -924,12 +927,10 @@ def capped_universal_credit_families(families, year, **extra): def capped_universal_credit(sim, year): from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( - universal_credit_after_benefit_cap, + universal_credit_award, ) - return universal_credit_after_benefit_cap( - sim.populations["benunit"], year, sim.tax_benefit_system.parameters - ) + return universal_credit_award(sim.populations["benunit"], year) CAPPED_FAMILY = st.fixed_dictionaries( @@ -942,6 +943,7 @@ def capped_universal_credit(sim, year): uc_childcare_element=st.sampled_from([0.0, 2_000.0, 12_000.0]), jsa_contrib=st.sampled_from([0.0, 4_000.0]), esa_contrib=st.sampled_from([0.0, 5_000.0]), + uc_deduction_rate=st.sampled_from([0.0, 0.05, 0.15]), ) ) ONE_CAPPED_FAMILY = dict( @@ -964,112 +966,88 @@ def capped_universal_credit(sim, year): families=st.lists(CAPPED_FAMILY, min_size=1, max_size=8), year=st.integers(2025, 2030), ) -@example(families=[ONE_CAPPED_FAMILY], year=2025) +@example( + # Under the 85% floor the 15% debt deduction comes first and the cap + # reduction goes, so the award before deductions is the full 20,000. + families=[ONE_CAPPED_FAMILY, dict(ONE_CAPPED_FAMILY, uc_deduction_rate=0.15)], + year=2025, +) def test_capped_universal_credit_matches_the_model(families, year): - """Invariant 12: the after-cap Universal Credit counted in a - non-dependant family's gross income equals universal_credit for a family - with no Housing Benefit, Maternity Allowance or deductions, under current - law and under an 85% protected minimum floor. The test sets the floor in - place on the same simulation, which costs far less than a reformed one.""" + """Invariant 12: the Universal Credit cap counts the welfare benefits + other than Housing Benefit, and a non-dependant family's gross income + counts its award after the cap and before debt deductions, under current + law and under an 85% protected minimum floor. The test changes the floor + in place on the same simulation.""" sim = Simulation(situation=capped_universal_credit_families(families, year)) - current_law = np.asarray(sim.calculate("universal_credit", year)) - np.testing.assert_allclose( - capped_universal_credit(sim, year), current_law, atol=0.01 - ) + + def check(): + # UC Regs 2013 reg 80(2A), with no Housing Benefit to take out. + np.testing.assert_array_equal( + sim.calculate("uc_benefit_cap_welfare_benefits", year), + sim.calculate("benefit_cap_welfare_benefits", year) + - sim.calculate("housing_benefit_pre_benefit_cap", year), + ) + award = np.asarray(sim.calculate("universal_credit", year)) + np.testing.assert_allclose( + capped_universal_credit(sim, year), + np.maximum( + sim.calculate("universal_credit_pre_benefit_cap", year) + - sim.calculate("uc_benefit_cap_reduction_after_floor", year), + 0, + ), + atol=0.01, + ) + return award + + current_law = check() sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( period=str(year), value=0.85 ) sim.tax_benefit_system.reset_parameter_caches() sim.reset_calculations() - floored = np.asarray(sim.calculate("universal_credit", year)) - np.testing.assert_allclose(capped_universal_credit(sim, year), floored, atol=0.01) - # The floor only limits the reduction, so no award falls. + try: + floored = check() + finally: + sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( + period=str(year), value=0.0 + ) + sim.tax_benefit_system.reset_parameter_caches() + # The floor only limits the reductions, so no award falls. assert np.all(floored >= current_law - 0.01) -def test_capped_universal_credit_reads_stored_and_replaced_values(): - """Invariant 12: a stored award, cap reduction or welfare-benefit total, - and a replaced universal_credit formula, reach the gross income. The - family's Universal Credit before the cap is 20,000 against a 14,753 cap.""" - from policyengine_core.reforms import Reform - from policyengine_core.simulations import Simulation as CoreSimulation - - from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( - universal_credit as current_law, - ) +def test_a_supplied_benefit_cap_total_with_a_non_dependant_cycles(): + """A supplied benefit_cap_welfare_benefits includes Housing Benefit, so + Universal Credit takes out housing_benefit_pre_benefit_cap. Where the + simulation calculates that Housing Benefit for a household with a + non-dependant on Universal Credit, the award and the deduction depend on + each other, and the engine raises.""" + from policyengine_core.errors import CycleError year = 2025 - for stored, expected in [ - (dict(), 14_753), - (dict(universal_credit=15_000.0), 15_000), - (dict(uc_benefit_cap_reduction=0.0), 20_000), - (dict(uc_benefit_cap_reduction_after_floor=1_000.0), 19_000), - (dict(benefit_cap_welfare_benefits=10_000.0), 20_000), - ]: - sim = Simulation( - situation=capped_universal_credit_families( - [ONE_CAPPED_FAMILY], year, **stored - ) - ) - helper = capped_universal_credit(sim, year) - assert helper == pytest.approx([expected], abs=0.01), stored - assert helper == pytest.approx( - sim.calculate("universal_credit", year), abs=0.01 - ), stored - - constant_award = type( - "universal_credit", - (current_law,), - dict(formula=lambda benunit, period, parameters: benunit.filled_array(12_345)), - ) - - class ConstantAward(Reform): - def apply(self): - self.update_variable(constant_award) - - sim = CoreSimulation( - tax_benefit_system=ConstantAward(SYSTEM), - situation=capped_universal_credit_families([ONE_CAPPED_FAMILY], year), - default_input_period=year, - default_calculation_period=year, - ) - assert capped_universal_credit(sim, year) == pytest.approx([12_345], abs=0.01) - - # A replacement that scales the current-law award still reads the cap - # reduction, and so Housing Benefit and this deduction. The gross income - # then uses the award calculated without Housing Benefit, and nothing - # cycles: the non-dependant's deduction matches current law's. - scaled_award = type( - "universal_credit", - (current_law,), - dict( - formula=lambda benunit, period, parameters: ( - 0.9 * current_law.formula(benunit, period, parameters) - ) - ), - ) - - class ScaledAward(Reform): - def apply(self): - self.update_variable(scaled_award) - situation = for_years( dict( people=dict( claimant=dict(age=70), non_dependant=dict( - age=24, + age=40, current_education="NOT_IN_EDUCATION", - employment_income=5_000.0, + employment_income=4_000.0, weekly_hours=20.0, ), ), benunits=dict( - claimant_unit=dict(members=["claimant"], benunit_is_rent_liable=True), + claimant_unit=dict( + members=["claimant"], + benunit_is_rent_liable=True, + benefit_cap_welfare_benefits=0.0, + ), non_dependant_unit=dict( members=["non_dependant"], benunit_is_rent_liable=False, would_claim_uc=True, + universal_credit_pre_benefit_cap=15_000.0, + benefit_cap_welfare_benefits=15_000.0, ), ), households=dict( @@ -1084,20 +1062,157 @@ def apply(self): ), [year], ) - current = Simulation(situation=situation) - scaled = CoreSimulation( - tax_benefit_system=ScaledAward(SYSTEM), - situation=situation, + with pytest.raises(CycleError): + Simulation(situation=situation).calculate("housing_benefit", year) + + +class uc_award_scale(Variable): + value_type = float + entity = BenUnit + label = "Scale on the Universal Credit award, for a test reform" + definition_period = YEAR + default_value = 1.0 + + +@lru_cache(maxsize=None) +def scaled_award_system(): + """Current law, with universal_credit replaced by uc_award_scale times + current law's formula. The replacement still reads the cap reduction.""" + from policyengine_core.reforms import Reform + + from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( + universal_credit as current_law, + ) + + def formula(benunit, period, parameters): + return benunit("uc_award_scale", period) * current_law.formula( + benunit, period, parameters + ) + + class ScaledAward(Reform): + def apply(self): + self.add_variable(uc_award_scale) + self.update_variable( + type("universal_credit", (current_law,), dict(formula=formula)) + ) + + return ScaledAward(SYSTEM) + + +AWARD_YEAR = 2025 +AWARD_HOUSEHOLD = st.fixed_dictionaries( + dict( + employment_income=st.floats(0, 20_000), + universal_credit_pre_benefit_cap=st.floats(0, 25_000), + scale=st.floats(0, 1), + ) +) +# 4,000 of wages plus the 14,753 capped award is 360.63 a week, in the 348 +# band; a tenth less award gives 332.26 a week, in the 266 band. +BAND_CROSSING = dict( + employment_income=4_000.0, universal_credit_pre_benefit_cap=15_000.0, scale=0.9 +) + + +def award_households(households, awards=None): + """Each generated household twice, with the generated scale and then + with none: a 70-year-old claimant paying 6,000 rent, and a 40-year-old + non-dependant working 20 hours who claims Universal Credit under a 14,753 + cap. awards supplies each family's universal_credit.""" + people, benunits, households_ = {}, {}, {} + no_awards = {benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS} + for copy, scaled in enumerate([True, False]): + for h, household in enumerate(households): + claimant, non_dependant = f"claimant_{copy}_{h}", f"non_dep_{copy}_{h}" + people[claimant] = dict(age=70, current_education="NOT_IN_EDUCATION") + people[non_dependant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=household["employment_income"], + weekly_hours=20.0, + ) + benunits[f"claimant_unit_{copy}_{h}"] = dict( + members=[claimant], benunit_is_rent_liable=True, **no_awards + ) + benunits[f"non_dep_unit_{copy}_{h}"] = dict( + members=[non_dependant], + benunit_is_rent_liable=False, + would_claim_uc=True, + uc_benefit_cap=14_753.0, + uc_childcare_element=0.0, + **{ + **no_awards, + "universal_credit_pre_benefit_cap": household[ + "universal_credit_pre_benefit_cap" + ], + }, + ) + if awards is None: + benunits[f"non_dep_unit_{copy}_{h}"]["uc_award_scale"] = ( + household["scale"] if scaled else 1.0 + ) + households_[f"household_{copy}_{h}"] = dict( + members=[claimant, non_dependant], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=6_000.0, + tenure_type="RENT_PRIVATELY", + ) + if awards is not None: + for name, award in zip(benunits, awards): + benunits[name]["universal_credit"] = float(award) + return for_years( + dict(people=people, benunits=benunits, households=households_), [AWARD_YEAR] + ) + + +@PROPERTY_SETTINGS +@given(households=st.lists(AWARD_HOUSEHOLD, min_size=1, max_size=3)) +@example(households=[BAND_CROSSING]) +def test_deduction_follows_a_reformed_award(households): + """Invariant 12: under a reform that scales the Universal Credit award, + each household's Housing Benefit and CTR deductions equal those of the + same household with the reformed award supplied, and the Housing Benefit + deduction is the band of the non-dependant's wages plus that award, so + an award that crosses a band moves the deduction.""" + from policyengine_core.simulations import Simulation as CoreSimulation + + year = AWARD_YEAR + reformed = CoreSimulation( + tax_benefit_system=scaled_award_system(), + situation=award_households(households), default_input_period=year, default_calculation_period=year, ) - assert scaled.calculate("universal_credit", year) == pytest.approx( - 0.9 * current.calculate("universal_credit", year), abs=0.01 + awards = np.asarray(reformed.calculate("universal_credit", year)) + supplied = Simulation(situation=award_households(households, awards)) + for name in [ + "housing_benefit_non_dep_deductions", + "council_tax_reduction_non_dep_deductions", + "housing_benefit", + ]: + np.testing.assert_allclose( + reformed.calculate(name, year), + supplied.calculate(name, year), + atol=0.01, + err_msg=name, + ) + # Reg 74(1)(a), (2): the non-dependant works 20 hours and has no other + # income, so the deduction is the band of wages plus the award. + scale = SYSTEM.parameters(str(year)).gov.dwp.housing_benefit.non_dep_deduction + deductions = np.asarray( + reformed.calculate("housing_benefit_non_dep_deductions", year) ) - for name in ["household_benefits_individual_non_dep_deduction", "housing_benefit"]: - assert scaled.calculate(name, year) == pytest.approx( - current.calculate(name, year), abs=0.01 - ), name + wages = [h["employment_income"] for h in households] * 2 + for i, (wage, award) in enumerate(zip(wages, awards[1::2])): + weekly = (wage + award) / WEEKS + edges = [bracket.threshold for bracket in scale.amount.brackets] + if min(abs(weekly - edge) for edge in edges) < 0.01: + continue + expected = scale.amount.calc(np.array([weekly]))[0] * WEEKS + assert deductions[2 * i] == pytest.approx(expected, abs=0.01), (i, weekly) + if households == [BAND_CROSSING]: + assert deductions[0] < deductions[2], deductions # Earned-income cases for invariant 13. Each sets the person's earnings diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index f6ff5000a6..ca05662268 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -84,12 +84,9 @@ "tenure_type", "targeted_childcare_entitlement", "trading_loss", - "uc_benefit_cap_reduction", - "uc_benefit_cap_reduction_after_floor", "uc_carer_element", "uc_LCWRA_element", "universal_childcare_entitlement", - "universal_credit", } ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 284b9739c0..565dba21cd 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -1,31 +1,7 @@ """Shared definitions for Housing Benefit and Council Tax Reduction non-dependant deductions.""" -from policyengine_core.errors import CycleError - from policyengine_uk.model_api import * -from policyengine_uk.utils.benefit_cap import benefit_cap_couple -from policyengine_uk.utils.benefit_unit import add_for_members, award_of_couple -from policyengine_uk.utils.supplied_inputs import stored_as_input -from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( - CLAIMANT_OR_PARTNER_BENEFITS as CAPPED_CLAIMANT_OR_PARTNER_BENEFITS, - FAMILY_BENEFITS as CAPPED_FAMILY_BENEFITS, -) -from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_reduction import ( - benefit_cap_reduction_of, -) -from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_reduction_after_floor import ( - benefit_cap_reduction_applied, -) -from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit import ( - universal_credit as universal_credit_variable, -) -from policyengine_uk.variables.gov.dwp.esa_income import ( - income_related_esa_award_on_reports, -) -from policyengine_uk.variables.gov.dwp.jsa_income import ( - income_related_jsa_award_on_reports, -) # Family benefits counted in a couple's gross income, besides Universal Credit. # They are awarded to the benefit unit, so the person-level gross income @@ -42,16 +18,6 @@ "pension_credit", ] -# The welfare benefits the benefit cap counts (benefit_cap_welfare_benefits) -# other than Housing Benefit, and Maternity Allowance, a welfare benefit -# (Welfare Reform Act 2012 s.96(10)(i)) that total leaves out. Bereavement -# allowance and the widow's benefits have no input. -CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT = [ - benefit - for benefit in CAPPED_CLAIMANT_OR_PARTNER_BENEFITS + CAPPED_FAMILY_BENEFITS - if benefit != "housing_benefit_pre_benefit_cap" -] + ["maternity_allowance"] - def non_dependant_weekly_gross_income(person, period, parameters, own_gross_income): """Normal weekly gross income for the deduction bands. A claimant or @@ -69,9 +35,7 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco other_awards = benunit.project(add(benunit, period, FAMILY_GROSS_INCOME_BENEFITS)) # Universal Credit is the income of the claimants it assesses, the # claimant and partner unless is_uc_claimant names someone else. - universal_credit = benunit.project( - universal_credit_after_benefit_cap(benunit, period, parameters) - ) + universal_credit = benunit.project(universal_credit_award(benunit, period)) uc_claimant = person("is_uc_assessed_claimant", period) couple_claims_universal_credit = person.benunit.any( uc_claimant & claimant_or_partner @@ -88,88 +52,17 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco return annual / WEEKS_IN_YEAR -def reads_simulation_value(benunit, period, variable_name, default_formula=None): - """Whether to read the variable instead of recalculating it here: the - simulation stores an input for the period, a reform neutralized the - variable or, where the caller passes default_formula, replaced its - formula.""" - variable = benunit.simulation.tax_benefit_system.get_variable(variable_name) - if variable.is_neutralized: - return True - if default_formula is not None: - formula = variable.get_formula(period) - if formula is None or formula.__code__ != default_formula.__code__: - return True - return stored_as_input(benunit, variable_name, period) - - -def universal_credit_after_benefit_cap(benunit, period, parameters): - """A family's Universal Credit award after the benefit cap (UC Regs 2013 - reg 81), for a family with no liability for rent. universal_credit and - uc_benefit_cap_reduction depend on Housing Benefit through the welfare - benefits they count, so this applies the cap to the same welfare benefits - other than Housing Benefit, which such a family does not receive (UC reg - 80(2A) counts it as nil in any case), for the same single person or couple - (benefit_cap_couple; reg 79(1)). It shares the award's own calculation of - the reduction (benefit_cap_reduction_of) and of the part a protected - minimum floor lets it take (benefit_cap_reduction_applied). Deductions - DWP pays from the award to third parties or for debts stay the family's - income, so this keeps them in and applies the floor as for a family with - none: the deduction rate depends on the cap reduction, and so on Housing - Benefit. Where the simulation stores universal_credit as an input, or a - reform replaces its formula, this reads it as it stands, unless the new - formula still reads the cap reduction and so this deduction; then it - calculates the award here. It also reads a stored or neutralized - uc_benefit_cap_reduction_after_floor, uc_benefit_cap_reduction or - benefit_cap_welfare_benefits, the last with Housing Benefit at nil.""" - if reads_simulation_value( - benunit, period, "universal_credit", universal_credit_variable.formula - ): - try: - return benunit("universal_credit", period) - except CycleError: - # Core unwinds the calculation; the award below doesn't read - # Housing Benefit. - pass - if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction_after_floor"): - applied = benunit("uc_benefit_cap_reduction_after_floor", period) - else: - if reads_simulation_value(benunit, period, "uc_benefit_cap_reduction"): - reduction = benunit("uc_benefit_cap_reduction", period) - else: - reduction = benefit_cap_reduction_of( - benunit, period, capped_welfare_benefits(benunit, period) - ) - applied = benefit_cap_reduction_applied( - benunit, period, parameters, reduction, 0 - ) - return max_(benunit("universal_credit_pre_benefit_cap", period) - applied, 0) - - -def capped_welfare_benefits(benunit, period): - """The welfare benefits the Universal Credit cap counts, with Housing - Benefit at nil (reg 80(2A)) and Maternity Allowance in.""" - if reads_simulation_value(benunit, period, "benefit_cap_welfare_benefits"): - return benunit("benefit_cap_welfare_benefits", period) - couple = benefit_cap_couple(benunit, period) - income_based = award_of_couple( - benunit, - period, - "jsa_income", - "jsa_income_reported", - income_related_jsa_award_on_reports, - couple, - ) + award_of_couple( - benunit, - period, - "esa_income", - "esa_income_reported", - income_related_esa_award_on_reports, - couple, - ) - return income_based + add_for_members( - benunit, period, CAPPED_BENEFITS_EXCEPT_HOUSING_BENEFIT, couple - ) +def universal_credit_award(benunit, period): + """Return a family's Universal Credit award after the benefit cap (UC + Regs 2013 reg 81) and before debt deductions. Welfare Reform Act 2012 + s.8(1) sets the award as the maximum amount less the amounts for + income. DWP then takes advances (SI 2013/383 reg 10), overpayments (SI + 2013/384 regs 10-11) and third-party payments (SI 2013/380 Sch 6 para + 2(1)) from the sum payable, so those deductions stay the family's + income. A nil award carries none.""" + universal_credit = benunit("universal_credit", period) + deductions = benunit("uc_deductions", period) + return universal_credit + where(universal_credit > 0, deductions, 0) def has_earned_income(person, period): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 40710ed628..42054b797b 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -41,6 +41,13 @@ def formula(benunit, period, parameters): non_dependant = ( person("is_non_dependant_of_household_head", period) & ~joint_occupier ) + # With no non-dependant there is nothing to deduct, so the formula + # reads no income. Universal Credit reads Housing Benefit where the + # inputs supply benefit_cap_welfare_benefits + # (uc_benefit_cap_welfare_benefits); a household of one family can + # then still calculate both. + if not non_dependant.any(): + return benunit.empty_array() deductions = ( person("household_benefits_individual_non_dep_deduction", period) * non_dependant diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index 36defbb6a3..a6fde7766a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -1,19 +1,6 @@ from policyengine_uk.model_api import * -def benefit_cap_reduction_of(benunit, period, welfare_benefits): - """The regulation 81 reduction for welfare benefits that already count - Housing Benefit as nil (reg. 80(2A)): the excess over the Universal Credit - cap, minus the childcare costs element, and nothing where that element - exceeds the excess or the family has no award (reg. 78(1)). The - non-dependant deductions call this too, with their own total of welfare - benefits (housing_benefit/non_dep_deduction/_non_dependants.py).""" - excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) - reduction = max_(excess - benunit("uc_childcare_element", period), 0) - has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 - return where(has_award, reduction, 0) - - class uc_benefit_cap_reduction(Variable): value_type = float entity = BenUnit @@ -42,9 +29,11 @@ class uc_benefit_cap_reduction(Variable): def formula(benunit, period, parameters): # Reg 80(2A): where the welfare benefit is Housing Benefit, "the # amount to be used is nil". benefit_cap_welfare_benefits counts it - # for the Housing Benefit cap (HB Regs 2006 reg 75C(2)(b)), so it is - # taken out here; a family paid both is capped on its other benefits. - welfare_benefits = benunit("benefit_cap_welfare_benefits", period) - benunit( - "housing_benefit_pre_benefit_cap", period - ) - return benefit_cap_reduction_of(benunit, period, welfare_benefits) + # for the Housing Benefit cap (HB Regs 2006 reg 75C(2)(b)); + # uc_benefit_cap_welfare_benefits leaves it out, so a family paid + # both is capped on its other benefits. + welfare_benefits = benunit("uc_benefit_cap_welfare_benefits", period) + excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) + reduction = max_(excess - benunit("uc_childcare_element", period), 0) + has_award = benunit("universal_credit_pre_benefit_cap", period) > 0 + return where(has_award, reduction, 0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py index 615ff5c537..b40442dc06 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction_after_floor.py @@ -4,24 +4,6 @@ ) -def benefit_cap_reduction_applied( - benunit, period, parameters, cap_reduction, deduction_demand -): - """The part of a regulation 81 reduction that comes off the award, given - the deductions it must also meet, under any protected minimum floor. The - non-dependant deductions call this too - (housing_benefit/non_dep_deduction/_non_dependants.py).""" - p = parameters(period).gov.dwp.universal_credit.deductions - _, applied = allocate_uc_reductions( - benunit("universal_credit_pre_benefit_cap", period), - cap_reduction, - deduction_demand, - benunit("uc_maximum_reduction", period), - p.minimum_payable * MONTHS_IN_YEAR, - ) - return applied - - class uc_benefit_cap_reduction_after_floor(Variable): label = "UC benefit cap reduction applied" documentation = ( @@ -42,16 +24,18 @@ class uc_benefit_cap_reduction_after_floor(Variable): defined_for = "claims_universal_credit" def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.universal_credit.deductions # JRF's September 2026 example (standard allowance £98 a week, a £15 # Fair Repayment Rate deduction, an £80 cap reduction, an 85% floor) # limits the total to £15. Meeting the deduction first, as here, # removes the whole cap reduction. - return benefit_cap_reduction_applied( - benunit, - period, - parameters, + _, cap_reduction = allocate_uc_reductions( + benunit("universal_credit_pre_benefit_cap", period), benunit("uc_benefit_cap_reduction", period), benunit("uc_deduction_rate", period) * benunit("uc_standard_allowance", period) * benunit("uc_award_carries_deductions", period), + benunit("uc_maximum_reduction", period), + p.minimum_payable * MONTHS_IN_YEAR, ) + return cap_reduction diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py new file mode 100644 index 0000000000..e2025325d9 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -0,0 +1,75 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.benefit_cap import benefit_cap_couple +from policyengine_uk.utils.benefit_unit import award_of_couple +from policyengine_uk.utils.supplied_inputs import stored_as_input +from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( + CLAIMANT_OR_PARTNER_BENEFITS, + FAMILY_BENEFITS, +) +from policyengine_uk.variables.gov.dwp.esa_income import ( + income_related_esa_award_on_reports, +) +from policyengine_uk.variables.gov.dwp.jsa_income import ( + income_related_jsa_award_on_reports, +) + +# UC Regs 2013 reg 80(2A): where the welfare benefit is Housing Benefit, "the +# amount to be used is nil". The Universal Credit cap therefore counts the +# Housing Benefit cap's benefits other than Housing Benefit. +UC_FAMILY_BENEFITS = [ + benefit + for benefit in FAMILY_BENEFITS + if benefit != "housing_benefit_pre_benefit_cap" +] + + +class uc_benefit_cap_welfare_benefits(Variable): + value_type = float + entity = BenUnit + label = "Welfare benefits counted by the Universal Credit benefit cap" + documentation = ( + "The welfare benefits the Universal Credit cap counts: those in " + "benefit_cap_welfare_benefits, for the same single person or couple, " + "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Universal " + "Credit therefore reads no Housing Benefit, and the Housing Benefit " + "non-dependant deductions can read universal_credit. Where the " + "inputs supply benefit_cap_welfare_benefits, whose total includes " + "Housing Benefit, this subtracts housing_benefit_pre_benefit_cap " + "from it." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/96", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/79", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/80", + ) + + def formula(benunit, period, parameters): + if stored_as_input(benunit, "benefit_cap_welfare_benefits", period): + return benunit("benefit_cap_welfare_benefits", period) - benunit( + "housing_benefit_pre_benefit_cap", period + ) + # The rest follows benefit_cap_welfare_benefits, in the same order. + couple = benefit_cap_couple(benunit, period) + income_based = award_of_couple( + benunit, + period, + "jsa_income", + "jsa_income_reported", + income_related_jsa_award_on_reports, + couple, + ) + award_of_couple( + benunit, + period, + "esa_income", + "esa_income_reported", + income_related_esa_award_on_reports, + couple, + ) + return income_based + add_for_members( + benunit, + period, + CLAIMANT_OR_PARTNER_BENEFITS + UC_FAMILY_BENEFITS, + couple, + ) From 270d4eff595a312202d8634a4a9b9dcc702dec69 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 07:09:11 -0500 Subject: [PATCH 48/67] Test the award read, the cap basket and a reform crossing a band Invariant 12 now checks that the Universal Credit cap's basket equals the combined total less Housing Benefit, that the award counted in gross income is universal_credit plus debt deductions (the award before the cap less the reduction applied) under current law and an 85% floor, and that Universal Credit's trace reads no Housing Benefit. A Hypothesis property scales the award by a reform and checks each household's deductions against the same household with the reformed award supplied, and against the band of wages plus that award; the explicit example crosses from the 348 band to the 266 band. A supplied total with a calculated Housing Benefit and a non-dependant on Universal Credit raises CycleError. The tests build core simulations on the module's system, so the rewritten invariant 12 tests take about 45 seconds locally. The shard table takes the six entries and the earned-income test from run 41294689067. Comments on the cap total, the cap couple and the passporting rule describe the new basket, and the review cases carry no review history. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/tests/shard_durations.toml | 13 +- ...test_non_dependant_deduction_properties.py | 162 ++++++++---------- policyengine_uk/utils/benefit_cap.py | 11 +- .../gov/dwp/benefit_cap_welfare_benefits.py | 8 +- .../housing_benefit_on_passporting_benefit.py | 10 +- 5 files changed, 97 insertions(+), 107 deletions(-) diff --git a/policyengine_uk/tests/shard_durations.toml b/policyengine_uk/tests/shard_durations.toml index 86e9a1fc10..a5d21cd607 100644 --- a/policyengine_uk/tests/shard_durations.toml +++ b/policyengine_uk/tests/shard_durations.toml @@ -9,7 +9,7 @@ # 40067189795, 40071060766, 40079467297, 40083761750 and 40087883891, and # pull request run 40095846881. The entries for # test_non_dependant_deduction_properties.py come from pull request run -# 41269333878 alone, below 20 seconds included. A slow test not here is +# 41294689067 alone, below 20 seconds included. A slow test not here is # placed by the hash. # test_ci_test_shards.py fails if an entry names a test that no longer # exists, so a pull request that renames or removes a listed test should @@ -156,7 +156,6 @@ "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_residence_condition_is_metamorphic" = 101 "policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py::test_lha_category_is_the_claimed_schemes_old_category" = 101 "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_shared_carer_attribution_matches_the_reference_matching" = 101 -"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_non_dependant_deduction_invariants" = 101 "policyengine_uk/tests/test_dfe_childcare_student_support_properties.py::test_maintenance_loan_household_income_applies_the_age_25_test" = 100 "policyengine_uk/tests/test_severe_disability_addition_properties.py::test_legacy_premium_equals_the_addition_without_young_people" = 100 "policyengine_uk/tests/test_pension_annual_allowance_properties.py::test_extra_contributions_never_raise_tax_by_more_than_the_charge" = 100 @@ -171,6 +170,7 @@ "policyengine_uk/tests/test_uc_state_pension_properties.py::test_uc_falls_pound_for_pound_in_state_pension" = 96 "policyengine_uk/tests/test_uc_work_related_groups_properties.py::test_qualifying_age_route_moves_only_claimants_past_it" = 96 "policyengine_uk/tests/test_uc_earned_income_before_work_allowance_properties.py::test_more_pay_never_turns_the_childcare_criterion_on" = 96 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_non_dependant_deduction_invariants" = 96 "policyengine_uk/tests/test_legacy_award_work_properties.py::test_screens_match_a_family_by_family_reading" = 95 "policyengine_uk/tests/test_dated_reform_routes.py::test_a_schedule_of_two_changes_matches_the_oracle" = 95 "policyengine_uk/tests/test_uc_state_pension_properties.py::test_state_pension_counts_like_private_pension" = 95 @@ -266,6 +266,7 @@ "policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py::test_no_op_without_sharers_boarders_or_lodgers" = 72 "policyengine_uk/tests/test_state_pension_age.py::test_status_matches_statute_for_every_birth_date[2017]" = 72 "policyengine_uk/tests/test_pawhp_household_income_properties.py::test_switching_pawhp_off_lowers_household_income_by_pawhp" = 72 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_capped_universal_credit_matches_the_model" = 72 "policyengine_uk/tests/test_process_time_reforms.py::test_a_triple_lock_rate_set_by_reform_uprates_the_state_pension[via_parameter_changes-generated-bare-year]" = 71 "policyengine_uk/tests/test_state_pension_age.py::test_status_matches_statute_for_every_birth_date[2019]" = 71 "policyengine_uk/tests/test_labour_supply_response_baseline.py::test_basic_rate_rise_gives_responses_of_the_implied_sign" = 71 @@ -310,7 +311,6 @@ "policyengine_uk/tests/test_carer_overlapping_benefits_properties.py::test_carer_benefit_is_monotonic_in_state_pension" = 60 "policyengine_uk/tests/test_winter_heating_payment_properties.py::test_non_dependants_never_change_anyone_elses_payment" = 60 "policyengine_uk/tests/test_uc_single_claim_properties.py::test_single_standard_allowance_at_the_claimants_own_age" = 60 -"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_capped_universal_credit_matches_the_model" = 60 "policyengine_uk/tests/test_trading_loss_properties.py::test_zero_profit_trade_loss_cannot_raise_tax_or_reduce_programme_award[universal_credit-2026]" = 59 "policyengine_uk/tests/test_reform_inputs_survive_reset.py::test_a_reform_names_exactly_the_variables_it_leaves_inputs_for" = 59 "policyengine_uk/tests/test_uc_minimum_income_floor_properties.py::test_top_up_never_takes_couple_above_couple_threshold" = 59 @@ -377,6 +377,7 @@ "policyengine_uk/tests/test_uc_statutory_pay_properties.py::test_uc_does_not_increase_as_statutory_paternity_pay_rises" = 48 "policyengine_uk/tests/test_salary_sacrifice_ni_only.py::test_explicit_actual_return_matches_actual_pay_and_member_inputs[personal_allowance]" = 48 "policyengine_uk/tests/test_income_support_eligibility_properties.py::test_more_work_or_jsa_never_makes_a_family_eligible" = 48 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_earned_income_exemption_reads_universal_credit" = 48 "policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py::test_disregard_is_non_decreasing_in_earnings" = 47 "policyengine_uk/tests/test_benefit_cap_uc_and_hb_properties.py::test_a_family_with_both_awards_is_capped_once_through_universal_credit" = 47 "policyengine_uk/tests/test_legacy_award_readers_properties.py::test_other_members_esa_never_changes_non_dependant_exemption" = 47 @@ -584,7 +585,7 @@ "policyengine_uk/tests/test_disable_simulated_benefits_winter_heating.py::test_a_household_that_moves_is_paid_under_each_years_scheme[LONDON-SCOTLAND]" = 20 "policyengine_uk/tests/test_capital_gains_responses.py::test_revenue_falls_short_of_the_static_estimate" = 20 "policyengine_uk/tests/test_capital_gains_response_default.py::test_employer_cost_branches_hold_the_realisation_response" = 20 -"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[ENGLAND]" = 14 -"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[WALES]" = 11 -"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[SCOTLAND]" = 10 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[WALES]" = 15 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[SCOTLAND]" = 12 "policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[HOUSING_BENEFIT]" = 10 +"policyengine_uk/tests/test_non_dependant_deduction_properties.py::test_band_edges_are_inclusive[ENGLAND]" = 9 diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 4c893f62ef..c819ec3632 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -67,12 +67,20 @@ bands another member who claims Universal Credit alone on their own income and that award. The test skips incomes within a penny a week of an edge, since the inputs hold float32 values. -12. Differential: the after-cap Universal Credit used in a non-dependant - family's gross income equals the model's universal_credit for a family - with no Housing Benefit, no Maternity Allowance and no deductions from - its award, under current law and under an 85% protected minimum floor; - and it reads a stored universal_credit, cap reduction or welfare-benefit - total, and a universal_credit formula a reform replaces. +12. The Universal Credit cap counts the Housing Benefit cap's welfare + benefits other than Housing Benefit (UC Regs 2013 reg 80(2A)), so + universal_credit reads no Housing Benefit unless the inputs supply + benefit_cap_welfare_benefits. A non-dependant family's gross income + counts its award after the cap and before debt deductions: universal_credit + plus uc_deductions, which equals the award before the cap less the cap + reduction applied, under current law and under an 85% protected minimum + floor; the floor never lowers an award. Under a reform that scales + universal_credit, each household's deductions equal those of the same + household with the reformed award supplied, and the Housing Benefit + deduction is the band of wages plus that award, so it moves when the + award crosses a band. With a supplied total, a calculated Housing + Benefit and a non-dependant on Universal Credit, the engine raises + CycleError. 13. Differential: the Universal Credit exemption's earned-income test agrees with Universal Credit's own calculation (uc_individual_earned_income) across employment expenses, trading losses, company income attributed to @@ -933,6 +941,19 @@ def capped_universal_credit(sim, year): return universal_credit_award(sim.populations["benunit"], year) +def core_simulation(situation, year, system=SYSTEM, trace=False): + """A simulation on a tax-benefit system built once for the module.""" + from policyengine_core.simulations import Simulation as CoreSimulation + + return CoreSimulation( + tax_benefit_system=system, + situation=situation, + default_input_period=year, + default_calculation_period=year, + trace=trace, + ) + + CAPPED_FAMILY = st.fixed_dictionaries( dict( couple=st.booleans(), @@ -976,18 +997,17 @@ def test_capped_universal_credit_matches_the_model(families, year): """Invariant 12: the Universal Credit cap counts the welfare benefits other than Housing Benefit, and a non-dependant family's gross income counts its award after the cap and before debt deductions, under current - law and under an 85% protected minimum floor. The test changes the floor - in place on the same simulation.""" - sim = Simulation(situation=capped_universal_credit_families(families, year)) + law and under an 85% protected minimum floor, which never lowers an + award.""" def check(): + sim = core_simulation(capped_universal_credit_families(families, year), year) # UC Regs 2013 reg 80(2A), with no Housing Benefit to take out. np.testing.assert_array_equal( sim.calculate("uc_benefit_cap_welfare_benefits", year), sim.calculate("benefit_cap_welfare_benefits", year) - sim.calculate("housing_benefit_pre_benefit_cap", year), ) - award = np.asarray(sim.calculate("universal_credit", year)) np.testing.assert_allclose( capped_universal_credit(sim, year), np.maximum( @@ -997,75 +1017,20 @@ def check(): ), atol=0.01, ) - return award + return np.asarray(sim.calculate("universal_credit", year)) current_law = check() - sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( - period=str(year), value=0.85 - ) - sim.tax_benefit_system.reset_parameter_caches() - sim.reset_calculations() + floor = SYSTEM.parameters.get_child(PROTECTED_FLOOR) + floor.update(period=str(year), value=0.85) + SYSTEM.reset_parameter_caches() try: floored = check() finally: - sim.tax_benefit_system.parameters.get_child(PROTECTED_FLOOR).update( - period=str(year), value=0.0 - ) - sim.tax_benefit_system.reset_parameter_caches() - # The floor only limits the reductions, so no award falls. + floor.update(period=str(year), value=0.0) + SYSTEM.reset_parameter_caches() assert np.all(floored >= current_law - 0.01) -def test_a_supplied_benefit_cap_total_with_a_non_dependant_cycles(): - """A supplied benefit_cap_welfare_benefits includes Housing Benefit, so - Universal Credit takes out housing_benefit_pre_benefit_cap. Where the - simulation calculates that Housing Benefit for a household with a - non-dependant on Universal Credit, the award and the deduction depend on - each other, and the engine raises.""" - from policyengine_core.errors import CycleError - - year = 2025 - situation = for_years( - dict( - people=dict( - claimant=dict(age=70), - non_dependant=dict( - age=40, - current_education="NOT_IN_EDUCATION", - employment_income=4_000.0, - weekly_hours=20.0, - ), - ), - benunits=dict( - claimant_unit=dict( - members=["claimant"], - benunit_is_rent_liable=True, - benefit_cap_welfare_benefits=0.0, - ), - non_dependant_unit=dict( - members=["non_dependant"], - benunit_is_rent_liable=False, - would_claim_uc=True, - universal_credit_pre_benefit_cap=15_000.0, - benefit_cap_welfare_benefits=15_000.0, - ), - ), - households=dict( - household=dict( - members=["claimant", "non_dependant"], - country="ENGLAND", - local_authority="MAIDSTONE", - rent=6_000.0, - tenure_type="RENT_PRIVATELY", - ) - ), - ), - [year], - ) - with pytest.raises(CycleError): - Simulation(situation=situation).calculate("housing_benefit", year) - - class uc_award_scale(Variable): value_type = float entity = BenUnit @@ -1114,14 +1079,15 @@ def apply(self): ) -def award_households(households, awards=None): +def award_households(households, awards=None, scaled=True): """Each generated household twice, with the generated scale and then with none: a 70-year-old claimant paying 6,000 rent, and a 40-year-old non-dependant working 20 hours who claims Universal Credit under a 14,753 - cap. awards supplies each family's universal_credit.""" + cap. awards supplies each family's universal_credit; scaled=False leaves + out the scale, for current law.""" people, benunits, households_ = {}, {}, {} no_awards = {benefit: 0.0 for benefit in BENEFITS + OTHER_FAMILY_BENEFITS} - for copy, scaled in enumerate([True, False]): + for copy, with_scale in enumerate([True, False]): for h, household in enumerate(households): claimant, non_dependant = f"claimant_{copy}_{h}", f"non_dep_{copy}_{h}" people[claimant] = dict(age=70, current_education="NOT_IN_EDUCATION") @@ -1147,9 +1113,9 @@ def award_households(households, awards=None): ], }, ) - if awards is None: + if awards is None and scaled: benunits[f"non_dep_unit_{copy}_{h}"]["uc_award_scale"] = ( - household["scale"] if scaled else 1.0 + household["scale"] if with_scale else 1.0 ) households_[f"household_{copy}_{h}"] = dict( members=[claimant, non_dependant], @@ -1166,6 +1132,36 @@ def award_households(households, awards=None): ) +def test_universal_credit_reads_housing_benefit_only_in_a_supplied_total(): + """Invariant 12: Universal Credit's calculation reads no Housing Benefit, + so the deductions can read universal_credit. A supplied + benefit_cap_welfare_benefits includes Housing Benefit, so Universal + Credit takes out housing_benefit_pre_benefit_cap. Where the simulation + calculates that Housing Benefit for a household with a non-dependant on + Universal Credit, the award and the deduction depend on each other, and + the engine raises.""" + from policyengine_core.errors import CycleError + + year = 2025 + households = [BAND_CROSSING] + situation = award_households(households, scaled=False) + sim = core_simulation(situation, year, trace=True) + sim.calculate("universal_credit", year) + read = {key.split("<")[0] for key in sim.tracer.get_serialized_flat_trace()} + assert "universal_credit" in read + assert not [ + name + for name in read + if name.startswith("housing_benefit") and name != "housing_benefit_reported" + ], read + for unit in situation["benunits"].values(): + unit["benefit_cap_welfare_benefits"] = { + str(year): unit["universal_credit_pre_benefit_cap"][str(year)] + } + with pytest.raises(CycleError): + core_simulation(situation, year).calculate("housing_benefit", year) + + @PROPERTY_SETTINGS @given(households=st.lists(AWARD_HOUSEHOLD, min_size=1, max_size=3)) @example(households=[BAND_CROSSING]) @@ -1175,17 +1171,12 @@ def test_deduction_follows_a_reformed_award(households): same household with the reformed award supplied, and the Housing Benefit deduction is the band of the non-dependant's wages plus that award, so an award that crosses a band moves the deduction.""" - from policyengine_core.simulations import Simulation as CoreSimulation - year = AWARD_YEAR - reformed = CoreSimulation( - tax_benefit_system=scaled_award_system(), - situation=award_households(households), - default_input_period=year, - default_calculation_period=year, + reformed = core_simulation( + award_households(households), year, system=scaled_award_system() ) awards = np.asarray(reformed.calculate("universal_credit", year)) - supplied = Simulation(situation=award_households(households, awards)) + supplied = core_simulation(award_households(households, awards), year) for name in [ "housing_benefit_non_dep_deductions", "council_tax_reduction_non_dep_deductions", @@ -1206,8 +1197,7 @@ def test_deduction_follows_a_reformed_award(households): wages = [h["employment_income"] for h in households] * 2 for i, (wage, award) in enumerate(zip(wages, awards[1::2])): weekly = (wage + award) / WEEKS - edges = [bracket.threshold for bracket in scale.amount.brackets] - if min(abs(weekly - edge) for edge in edges) < 0.01: + if min(abs(weekly - edge) for edge in scale.amount.thresholds) < 0.01: continue expected = scale.amount.calc(np.array([weekly]))[0] * WEEKS assert deductions[2 * i] == pytest.approx(expected, abs=0.01), (i, weekly) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index 9a334e3ddf..82e28af299 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -39,11 +39,12 @@ def benefit_cap_couple(benunit, period): (is_uc_assessed_claimant): a single claimant or joint claimants, and the other member of a couple where one member claims as a single person (UC Regs 2013 reg 78(2)). Otherwise it is Housing Benefit's claimant and - partner (is_claimant_or_partner; HB Regs 2006 regs 2(1), 75A). One total - of welfare benefits (benefit_cap_welfare_benefits) serves both caps: a - family receiving both awards is capped only through Universal Credit (UC - Regs 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so its total is the - Universal Credit couple's. The two couples are the same unless + partner (is_claimant_or_partner; HB Regs 2006 regs 2(1), 75A). Both caps + count this couple's welfare benefits (benefit_cap_welfare_benefits, and + uc_benefit_cap_welfare_benefits without Housing Benefit): a family + receiving both awards is capped only through Universal Credit (UC Regs + 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so its couple is the + Universal Credit couple. The two couples are the same unless is_uc_claimant is entered. """ person = benunit.members diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index dc0af1081b..3471ff1c1e 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -16,10 +16,10 @@ # contributory or income-related benefit; it still counts in household income. # Housing Benefit counts its own amount before the cap (HB Regs 2006 # reg. 75C(2)(b)), so this total includes it. Universal Credit counts Housing -# Benefit as nil (UC Regs 2013 reg. 80(2A)): uc_benefit_cap_reduction takes it -# out. Universal Credit before the cap is in the total too; a family receiving -# it is outside the Housing Benefit cap (HB Regs 2006 reg. 75F(1)(g)). With -# those two adjustments one total serves both schemes. Reg. 75C(2)(a) counts +# Benefit as nil (UC Regs 2013 reg. 80(2A)), so its cap reads +# uc_benefit_cap_welfare_benefits, these benefits without Housing Benefit. +# Universal Credit before the cap is in the total too; HB reg. 75F(1)(g) puts a +# family receiving it outside the Housing Benefit cap. Reg. 75C(2)(a) counts # Housing Benefit for specified accommodation as nil; the model's input # in_specified_or_temporary_accommodation does not separate specified from # temporary accommodation, so that Housing Benefit is counted at its amount diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 780d17bc56..50ad361384 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -54,12 +54,10 @@ def formula(benunit, period, parameters): p.northern_ireland, p.great_britain, ) - # Universal Credit before the benefit cap and deductions: Universal - # Credit depends on Housing Benefit through the benefit cap - # (uc_benefit_cap_reduction, whose total benefit_cap_welfare_benefits - # counts Housing Benefit), so reading the amount paid - # (universal_credit) here would be circular. Before the cap is also - # the law's reading: entitlement counts whether or not it is paid. + # Universal Credit before the benefit cap and deductions: entitlement + # counts whether or not it is paid. The amount paid + # (universal_credit) reads Housing Benefit where the inputs supply + # benefit_cap_welfare_benefits (uc_benefit_cap_welfare_benefits). on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb on_legacy_benefit = benunit( "in_receipt_of_income_support_jsa_ib_or_esa_ir", period From 764d57b19433f98a0e7750089f6a85243e49d48d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 08:24:34 -0500 Subject: [PATCH 49/67] Register the cap total beside adjusted_net_income The registration sat where #2765 adds self_employment_income, so the two conflicted. The set's order means nothing; this place merges cleanly with #1969, #2661, #2765 and #2776. Co-Authored-By: Claude Opus 5.5 --- policyengine_uk/utils/supplied_inputs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index ca05662268..5dec9471f1 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -46,6 +46,7 @@ SUPPLIED_INPUT_VARIABLES = frozenset( { "adjusted_net_income", + "benefit_cap_welfare_benefits", "carers_allowance", "carers_allowance_pre_couple_limit", "carers_allowance_pre_overlap", @@ -55,7 +56,6 @@ "receives_carer_benefit", "receives_carers_allowance", "receives_carer_support_payment", - "benefit_cap_welfare_benefits", "bus_fare_spending", "bus_in_london_trips", "current_education", From dfab37d348bb5d37e21dde3125f1929536837362 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 08:59:07 -0500 Subject: [PATCH 50/67] Name who acts in the gross-income docs, test comments and changelog DWP, the council and the model take the verbs the comments gave to guidance paragraphs, regulations and cases; the gross-income documentation drops its closing disclaimer, and the changelog qualifies the flat charge and the couple rule and adds the debt deductions and the Universal Credit cap's basket. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- .../housing_benefit_non_dep_gross_income.yaml | 9 ++-- .../non_dependant_deduction_review_cases.yaml | 22 +++++----- ...il_tax_reduction_non_dep_gross_income.yaml | 9 ++-- ...x_reduction_non_dep_remunerative_work.yaml | 4 +- ...test_non_dependant_deduction_properties.py | 13 +++--- .../non_dep_deduction/_non_dependants.py | 18 ++++---- .../housing_benefit_non_dep_gross_income.py | 41 +++++++++---------- .../uc_benefit_cap_welfare_benefits.py | 4 +- ...ncil_tax_reduction_non_dep_gross_income.py | 41 +++++++++---------- 10 files changed, 80 insertions(+), 83 deletions(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 34d291f0da..13ce85cfd4 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -The model now makes Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-dependant outside remunerative work, or on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. It makes one deduction for a couple, banded on the couple's gross income plus the family's awards (Universal Credit after the benefit cap, tax credits, child benefit and the income-related benefits), and a separate one for any other adult in the family. Gross income counts other investment income, ISA interest and ISA dividends, and treats a self-employment or property loss as nil. The claimant and non-dependant exemptions apply, and the Universal Credit exemption reads Universal Credit's own calculation of earned income. The England pensioner, Scottish and Welsh Council Tax Reduction schemes use their own scales from 2013-14, and each jointly liable applicant's deduction follows its own scheme. +The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml index a7b9638754..1c8bfa4f8d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.yaml @@ -603,7 +603,8 @@ housing_benefit_non_dep_gross_income: 1_000 - name: housing_benefit_non_dep_gross_income counts other investment income - # Gross income is earnings and any other income (DWP HB guidance A5.542). + # DWP tells councils to count earnings and any other income (HB guidance + # A5.542). period: 2025 input: social_security_income: 0 @@ -626,9 +627,9 @@ housing_benefit_non_dep_gross_income: 5_000 - name: housing_benefit_non_dep_gross_income counts ISA interest and dividends - # Gross income is earnings and any other income (DWP HB guidance A5.542), - # savings interest included (A5.570). The supplied exempt-interest total - # includes the ISA interest, which counts once. + # DWP tells councils to count earnings and other income (HB guidance + # A5.542), including savings interest (A5.570). This case supplies an + # exempt-interest total, so the model counts that total once. period: 2025 input: social_security_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index c9cc019a0d..4e51f02aab 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -1262,9 +1262,9 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 55(3)(a): allowable employment expenses come off - # employed earnings, leaving none (HB reg 74(8)(b), (11); WSI 2013/3029 - # Sch 1 para 3(8)(c), (10)). + # DWP subtracts allowable employment expenses from employed earnings + # (UC Regs 2013 reg 55(3)(a)), leaving none (HB reg 74(8)(b), (11); WSI + # 2013/3029 Sch 1 para 3(8)(c), (10)). housing_benefit_non_dep_deduction_exempt: [false, true] council_tax_reduction_non_dep_deduction_exempt: [false, true] housing_benefit_non_dep_deductions: [0, 0] @@ -1307,9 +1307,9 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 57(2): the trades' profits and losses are added - # together before any nil floor, so the loss cancels the profit. The case - # holds the minimum income floor off, so only the loss rule decides. + # DWP adds the trades' profits and losses before applying the nil floor + # (UC Regs 2013 reg 57(2)), so the loss cancels the profit. The inputs + # turn off the minimum income floor. housing_benefit_non_dep_deduction_exempt: [false, true] council_tax_reduction_non_dep_deduction_exempt: [false, true] housing_benefit_non_dep_deductions: [0, 0] @@ -1352,8 +1352,8 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 77(3)(b) treats the company's income as the owner's - # self-employed earnings, so the exemption doesn't apply. Outside + # DWP attributes the company's income to its owner as self-employed + # earnings (UC Regs 2013 reg 77(3)(b)), so the exemption doesn't apply. Outside # remunerative work the councils deduct the lowest amounts: HB 20.40 and # the Welsh pensioner scale's 6.70 a week. housing_benefit_non_dep_deduction_exempt: [false, false] @@ -1405,9 +1405,9 @@ country: WALES local_authority: CARDIFF output: - # UC Regs 2013 reg 62(3) treats a member of a couple as having the floor - # only while the couple's earned income falls short of the couple - # threshold. The partner's earned income exceeds it, so the non-dependant + # DWP applies the minimum income floor to a member of a couple only while + # the couple's earnings fall below the couple threshold (UC Regs 2013 reg + # 62(3)). The partner's earned income exceeds it, so the non-dependant # has none, and the exemption applies. The Welsh working-age scheme # deducts for each member of a Universal Credit couple (Sch 6 para 5(3)): # nil for the exempt non-dependant and 6.70 a week for the partner, who diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index ec89611965..b9a056f778 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -723,7 +723,8 @@ council_tax_reduction_non_dep_gross_income: 1_000 - name: council_tax_reduction_non_dep_gross_income counts other investment income - # Gross income is earnings and any other income (DWP HB guidance A5.542). + # DWP tells councils to count earnings and any other income (HB guidance + # A5.542). period: 2025 input: social_security_income: 0 @@ -746,9 +747,9 @@ council_tax_reduction_non_dep_gross_income: 5_000 - name: council_tax_reduction_non_dep_gross_income counts ISA interest and dividends - # Gross income is earnings and any other income (DWP HB guidance A5.542), - # savings interest included (A5.570). The supplied exempt-interest total - # includes the ISA interest, which counts once. + # DWP tells councils to count earnings and other income (HB guidance + # A5.542), including savings interest (A5.570). This case supplies an + # exempt-interest total, so the model counts that total once. period: 2025 input: social_security_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index d3bbe37bea..b660578932 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -5,8 +5,8 @@ # pays the lowest amount (England 5.20, SI 2026/27; Scotland 5.55, SSI # 2026/56; Wales 6.70, WSI 2026/13), and 403.85/week of income in work falls # in the band from £279 in England (10.60) and Wales (13.35) and from £286 in -# Scotland (10.95). Family awards, which a single non-dependant's gross income -# would count, are held at zero. English cases give the household a +# Scotland (10.95). The cases hold the family's awards, which a single +# non-dependant's gross income would count, at zero. English cases give the household a # pension-age applicant, since England's national scheme covers pension-age # applicants only, and the model works out each family's scheme. diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index c819ec3632..556283beb2 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -247,8 +247,8 @@ def households(draw): def build(population, year, raise_income=False, explicit_uc_claimants=False): """The generated population as a simulation. With explicit_uc_claimants, - every person's is_uc_claimant is an input: True for each claimant and the - generated flag for each non-dependant. (An input for some people stores + the inputs name each family's UC claimants: is_uc_claimant is True for + each claimant and the generated flag for each non-dependant. (An input for some people stores the default, False, for the rest.) Otherwise the model's default applies: the claimant and partner of each family.""" people, benunits, households_, persons = {}, {}, {}, [] @@ -1335,10 +1335,11 @@ def test_earned_income_exemption_reads_universal_credit(families, year): if case is None: continue member, settled = case - # The predicate is Universal Credit's earned income, for a claimant - # the award assesses (the model pairs claimant and partner, and may - # leave a second adult outside the couple): the Welsh pensioner - # scheme at any age, Housing Benefit under 25. + # For each claimant the award assesses, compare the exemption with + # Universal Credit's earned-income calculation (the model pairs + # claimant and partner, and may leave a second adult outside the + # couple): the Welsh pensioner scheme at any age, Housing Benefit + # under 25. on_universal_credit = bool(assessed[i]) uc_without_earnings = on_universal_credit and not earned[i] assert bool(ctr_exempt[i]) == uc_without_earnings, (i, member) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 565dba21cd..565738d075 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -24,7 +24,8 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_inco partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 para 8(4)): each member's own gross income plus the family's tax credits, child benefit, Income Support, income-based JSA, income-related ESA and - Pension Credit, and its Universal Credit where either member claims it. + Pension Credit, and its Universal Credit award (universal_credit_award) + where either member claims it. Anyone else in the benefit unit is a separate non-dependant, banded on their own income, with the Universal Credit where they alone claim it. own_gross_income names the programme's person-level gross income @@ -66,14 +67,13 @@ def universal_credit_award(benunit, period): def has_earned_income(person, period): - """Whether the person has earned income as Universal Credit calculates it - (HB Regs 2006 reg 74(8)(b), (11) and the CTR schemes' equivalents give - "earned income" the meaning in UC Regs 2013 reg 52): - uc_individual_earned_income, which takes off allowable expenses, tax, - National Insurance and relievable pension contributions (reg 55(3), (5)), - counts a company's income attributed to its owner and sets trading losses - against profits (regs 57(2), 77(3)), and applies the minimum income floor - only where reg 62 does, a couple's floor included.""" + """Read whether Universal Credit counts positive earned income for the + person (uc_individual_earned_income). HB Regs 2006 reg 74(8)(b), (11) and + the CTR schemes' equivalents give "earned income" the meaning in UC Regs + 2013 reg 52. UC deducts allowable expenses, tax, National Insurance and + relievable pension contributions (reg 55(3), (5)); attributes company + income to its owner (reg 77(3)); offsets trading losses (reg 57(2)); and + applies the floor under reg 62, a couple's floor included.""" return person("uc_individual_earned_income", period) > 0 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py index e591cca6ab..0faeda2b2a 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_gross_income.py @@ -18,31 +18,27 @@ class housing_benefit_non_dep_gross_income(Variable): documentation = ( "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "pension contributions. DWP HB guidance A5.551 counts a " - "self-employed non-dependant's total income with no deduction for " - "business expenses. The model holds self-employment and property " - "profits, not receipts, so this approximates gross income by adding " - "each profit and counting a loss as nil, which stops a loss reducing " + "pension contributions. DWP tells councils to count earnings and any " + "other income (HB guidance A5.542), to count a self-employed " + "non-dependant's total income before business expenses (A5.551) and " + "to add savings interest to earnings (A5.570). The model holds " + "self-employment and property profits rather than receipts, so it " + "adds each profit and counts a loss as nil, and a loss never reduces " "other income. The claimant war-pension and supplementary-pension " "disregards do not apply to these bands. The total includes " "statutory sick, maternity and paternity pay once, through " "employment_benefits; child and spousal maintenance; the tax-purpose " - "social security income sources; and the non-taxable periodic " - "benefits the model represents separately. It leaves out Attendance " - "Allowance, DLA and PIP under the disability-payment disregards; the " - "other disregarded disability, compensation and trust payments have " - "no model inputs. BSP and AFCS inputs do not separate periodic income " - "from capital payments, so the total leaves them out. Gross income " - "includes earnings and any other income (A5.542), and A5.570 adds " - "savings interest to earnings, so other investment income, ISA " - "interest (tax_free_savings_income) and ISA dividends count. This is " - "the person's own income. Benefit-unit awards, such as Child Benefit, " - "tax credits and Universal Credit, stay out of it: the deduction " - "formula adds them for the claimant and partner of the " - "non-dependant's family, whom it bands on their joint income, and " - "applies the remunerative-work test. This variable decides neither " - "whether someone is in remunerative work nor whether a deduction is " - "due." + "social security income sources; the non-taxable periodic benefits " + "the model represents separately; and other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends. It leaves out " + "Attendance Allowance, DLA and PIP under the disability-payment " + "disregards; the other disregarded disability, compensation and " + "trust payments have no model inputs. BSP and AFCS inputs do not " + "separate periodic income from capital payments, so the total leaves " + "them out. Benefit-unit awards, such as Child Benefit, tax credits " + "and Universal Credit, stay out of it. The deduction formula adds " + "family awards to the claimant and partner's joint income and applies " + "the remunerative-work test." ) def formula(person, period, parameters): @@ -55,7 +51,8 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest: ISA interest unless the inputs supply a total. + # Count ISA interest unless the inputs supply an exempt-interest + # total. "tax_free_savings_income", "other_investment_income", "dividend_income", diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py index e2025325d9..bb60e1a70f 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -31,11 +31,11 @@ class uc_benefit_cap_welfare_benefits(Variable): "The welfare benefits the Universal Credit cap counts: those in " "benefit_cap_welfare_benefits, for the same single person or couple, " "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Universal " - "Credit therefore reads no Housing Benefit, and the Housing Benefit " + "Credit then reads no Housing Benefit, so the Housing Benefit " "non-dependant deductions can read universal_credit. Where the " "inputs supply benefit_cap_welfare_benefits, whose total includes " "Housing Benefit, this subtracts housing_benefit_pre_benefit_cap " - "from it." + "from it, and Universal Credit reads Housing Benefit." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py index fe3e109e02..01e88a693e 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.py @@ -22,31 +22,27 @@ class council_tax_reduction_non_dep_gross_income(Variable): documentation = ( "Annual gross income the model represents for the non-dependant " "bands, before tax, National Insurance, employment deductions and " - "pension contributions. DWP HB guidance A5.551 counts a " - "self-employed non-dependant's total income with no deduction for " - "business expenses. The model holds self-employment and property " - "profits, not receipts, so this approximates gross income by adding " - "each profit and counting a loss as nil, which stops a loss reducing " + "pension contributions. DWP tells councils to count earnings and any " + "other income (HB guidance A5.542), to count a self-employed " + "non-dependant's total income before business expenses (A5.551) and " + "to add savings interest to earnings (A5.570). The model holds " + "self-employment and property profits rather than receipts, so it " + "adds each profit and counts a loss as nil, and a loss never reduces " "other income. The claimant war-pension and supplementary-pension " "disregards do not apply to these bands. The total includes " "statutory sick, maternity and paternity pay once, through " "employment_benefits; child and spousal maintenance; the tax-purpose " - "social security income sources; and the non-taxable periodic " - "benefits the model represents separately. It leaves out Attendance " - "Allowance, DLA and PIP under the disability-payment disregards; the " - "other disregarded disability, compensation and trust payments have " - "no model inputs. BSP and AFCS inputs do not separate periodic income " - "from capital payments, so the total leaves them out. Gross income " - "includes earnings and any other income (A5.542), and A5.570 adds " - "savings interest to earnings, so other investment income, ISA " - "interest (tax_free_savings_income) and ISA dividends count. This is " - "the person's own income. Benefit-unit awards, such as Child Benefit, " - "tax credits and Universal Credit, stay out of it: the deduction " - "formula adds them for the claimant and partner of the " - "non-dependant's family, whom it bands on their joint income, and " - "applies the remunerative-work test. This variable decides neither " - "whether someone is in remunerative work nor whether a deduction is " - "due." + "social security income sources; the non-taxable periodic benefits " + "the model represents separately; and other investment income, ISA " + "interest (tax_free_savings_income) and ISA dividends. It leaves out " + "Attendance Allowance, DLA and PIP under the disability-payment " + "disregards; the other disregarded disability, compensation and " + "trust payments have no model inputs. BSP and AFCS inputs do not " + "separate periodic income from capital payments, so the total leaves " + "them out. Benefit-unit awards, such as Child Benefit, tax credits " + "and Universal Credit, stay out of it. The deduction formula adds " + "family awards to the claimant and partner's joint income and applies " + "the remunerative-work test." ) def formula(person, period, parameters): @@ -59,7 +55,8 @@ def formula(person, period, parameters): "private_pension_income", "social_security_income", "savings_interest_income", - # Exempt interest: ISA interest unless the inputs supply a total. + # Count ISA interest unless the inputs supply an exempt-interest + # total. "tax_free_savings_income", "other_investment_income", "dividend_income", From 02cc0896f5c3ca3efed822ba265f96216be3c453 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 10:56:26 -0500 Subject: [PATCH 51/67] Give the cap and passport comments their actors Comment-only: the council, DWP and Universal Credit take the verbs that be-verbs and passives held in the comments this round touched. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_deduction_properties.py | 4 ++-- policyengine_uk/utils/benefit_cap.py | 7 +++---- .../housing_benefit_on_passporting_benefit.py | 4 ++-- .../housing_benefit_non_dep_deductions.py | 2 +- .../benefit_cap/uc_benefit_cap_reduction.py | 4 ++-- 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 556283beb2..7d2acc30fd 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -1072,8 +1072,8 @@ def apply(self): scale=st.floats(0, 1), ) ) -# 4,000 of wages plus the 14,753 capped award is 360.63 a week, in the 348 -# band; a tenth less award gives 332.26 a week, in the 266 band. +# 4,000 of wages plus the 14,753 capped award come to 360.63 a week, in the +# 348 band; a tenth less award gives 332.26 a week, in the 266 band. BAND_CROSSING = dict( employment_income=4_000.0, universal_credit_pre_benefit_cap=15_000.0, scale=0.9 ) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index 82e28af299..21de4a9efc 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -41,10 +41,9 @@ def benefit_cap_couple(benunit, period): (UC Regs 2013 reg 78(2)). Otherwise it is Housing Benefit's claimant and partner (is_claimant_or_partner; HB Regs 2006 regs 2(1), 75A). Both caps count this couple's welfare benefits (benefit_cap_welfare_benefits, and - uc_benefit_cap_welfare_benefits without Housing Benefit): a family - receiving both awards is capped only through Universal Credit (UC Regs - 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so its couple is the - Universal Credit couple. The two couples are the same unless + uc_benefit_cap_welfare_benefits without Housing Benefit). Universal Credit + alone caps a family receiving both awards (UC Regs 2013 reg 80(2A); HB + Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two couples are the same unless is_uc_claimant is entered. """ person = benunit.members diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 50ad361384..9c390ed462 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -54,8 +54,8 @@ def formula(benunit, period, parameters): p.northern_ireland, p.great_britain, ) - # Universal Credit before the benefit cap and deductions: entitlement - # counts whether or not it is paid. The amount paid + # Read Universal Credit before the benefit cap and deductions: + # entitlement counts whether or not DWP pays it. The amount paid # (universal_credit) reads Housing Benefit where the inputs supply # benefit_cap_welfare_benefits (uc_benefit_cap_welfare_benefits). on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 42054b797b..4061946eaa 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -41,7 +41,7 @@ def formula(benunit, period, parameters): non_dependant = ( person("is_non_dependant_of_household_head", period) & ~joint_occupier ) - # With no non-dependant there is nothing to deduct, so the formula + # With no non-dependant the council deducts nothing, so the formula # reads no income. Universal Credit reads Housing Benefit where the # inputs supply benefit_cap_welfare_benefits # (uc_benefit_cap_welfare_benefits); a household of one family can diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py index a6fde7766a..3571e6fb82 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_reduction.py @@ -30,8 +30,8 @@ def formula(benunit, period, parameters): # Reg 80(2A): where the welfare benefit is Housing Benefit, "the # amount to be used is nil". benefit_cap_welfare_benefits counts it # for the Housing Benefit cap (HB Regs 2006 reg 75C(2)(b)); - # uc_benefit_cap_welfare_benefits leaves it out, so a family paid - # both is capped on its other benefits. + # uc_benefit_cap_welfare_benefits leaves it out, so Universal Credit + # caps a family paid both on its other benefits. welfare_benefits = benunit("uc_benefit_cap_welfare_benefits", period) excess = max_(welfare_benefits - benunit("uc_benefit_cap", period), 0) reduction = max_(excess - benunit("uc_childcare_element", period), 0) From f025c0d50b89b3df6f1aefab62b2611958fb6343 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 15:43:29 -0500 Subject: [PATCH 52/67] Require Housing Benefit beside a supplied cap total uc_benefit_cap_welfare_benefits subtracts housing_benefit_pre_benefit_cap from a supplied benefit_cap_welfare_benefits only when the inputs supply it too, and raises ValueError otherwise. Universal Credit then calculates no Housing Benefit on any baseline route, so housing_benefit_non_dep_deductions loses its early return. A model value (set_model_value) supplies neither. Tests: the mixed-population contract, the population-consistent totals, the cap-list uniqueness check, and the ninth supplied-total YAML case now supplies the Housing Benefit it calculated (nil). Co-Authored-By: Claude Opus 5.5 --- .../test_benefit_cap_lists_unique.py | 27 + .../gov/dwp/benefit_cap_by_scheme.yaml | 1 + .../test_non_dependant_cycle_contracts.py | 548 ++++++++++++++++++ ...test_non_dependant_deduction_properties.py | 49 +- ...n_dependant_population_consistent_total.py | 104 ++++ policyengine_uk/utils/benefit_cap.py | 4 +- policyengine_uk/utils/supplied_inputs.py | 1 + .../gov/dwp/benefit_cap_welfare_benefits.py | 18 +- .../housing_benefit_on_passporting_benefit.py | 4 +- .../housing_benefit_non_dep_deductions.py | 7 - .../uc_benefit_cap_welfare_benefits.py | 38 +- 11 files changed, 757 insertions(+), 44 deletions(-) create mode 100644 policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py create mode 100644 policyengine_uk/tests/test_non_dependant_cycle_contracts.py create mode 100644 policyengine_uk/tests/test_non_dependant_population_consistent_total.py diff --git a/policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py b/policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py new file mode 100644 index 0000000000..c4bb47b45b --- /dev/null +++ b/policyengine_uk/tests/code_health/test_benefit_cap_lists_unique.py @@ -0,0 +1,27 @@ +from collections import Counter + +import pytest + +from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( + CLAIMANT_OR_PARTNER_BENEFITS, + FAMILY_BENEFITS, +) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_welfare_benefits import ( + UC_FAMILY_BENEFITS, +) +from policyengine_uk.variables.gov.dwp.housing_benefit.non_dep_deduction._non_dependants import ( + FAMILY_GROSS_INCOME_BENEFITS, +) + + +@pytest.mark.parametrize( + "names", + [ + CLAIMANT_OR_PARTNER_BENEFITS + FAMILY_BENEFITS, + CLAIMANT_OR_PARTNER_BENEFITS + UC_FAMILY_BENEFITS, + FAMILY_GROSS_INCOME_BENEFITS, + ], + ids=["housing-benefit-cap", "universal-credit-cap", "non-dependant-family-income"], +) +def test_each_name_occurs_once(names): + assert not {name: n for name, n in Counter(names).items() if n != 1} diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml index 53e746bdf2..09bbd8efd9 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap_by_scheme.yaml @@ -320,6 +320,7 @@ uc_benefit_cap: 22_020 uc_childcare_element: 9_000 universal_credit_pre_benefit_cap: 15_000 + housing_benefit_pre_benefit_cap: 0 output: # Excess 7,980 < childcare 9,000. uc_benefit_cap_reduction: 0 diff --git a/policyengine_uk/tests/test_non_dependant_cycle_contracts.py b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py new file mode 100644 index 0000000000..5e6418db00 --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py @@ -0,0 +1,548 @@ +"""Universal Credit and Housing Benefit when the inputs supply cap totals. + +Housing Benefit's non-dependant deductions read universal_credit, so no +baseline formula on Universal Credit's route may calculate Housing Benefit. +The Universal Credit cap counts Housing Benefit as nil (UC Regs 2013 reg +80(2A)): its basket, uc_benefit_cap_welfare_benefits, adds up the other +welfare benefits itself. Inputs that supply benefit_cap_welfare_benefits, a +total that includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, +which the basket subtracts, or supply the basket itself. + +Contract (an invariant for every population): either the inputs leave +benefit_cap_welfare_benefits to the model, or they supply +housing_benefit_pre_benefit_cap or uc_benefit_cap_welfare_benefits beside it; +otherwise the basket raises ValueError, whether or not any household holds a +non-dependant. A value a scenario stores as the model's own (set_model_value) +supplies neither input. Under the contract, each household's Universal Credit +and Housing Benefit equal its results alone. A reform that makes Universal +Credit read calculated Housing Benefit adds a cycle, and the engine raises +CycleError. + +Two independent households: a single Universal Credit claimant with £20,000 +before a £14,753 cap and no rent; and a 70-year-old paying £6,000 rent with a +40-year-old non-dependant who earns £9,000 at 20 hours and claims no +Universal Credit. Alone, the first gets £14,753 of Universal Credit, and the +council deducts £19.65 x 52 = £1,021.80 (HB Regs 2006 reg 74(2)(a), 2025-26) +from the second's rent, leaving £4,978.20 of Housing Benefit. +""" + +import copy + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.errors import CycleError +from policyengine_core.reforms import Reform +from policyengine_core.simulations import Simulation + +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.utils.supplied_inputs import set_model_value +from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( + benefit_cap_welfare_benefits as CombinedTotal, +) +from policyengine_uk.variables.gov.dwp.universal_credit.benefit_cap.uc_benefit_cap_welfare_benefits import ( + uc_benefit_cap_welfare_benefits as Basket, +) +from policyengine_uk.variables.gov.dwp.universal_credit.universal_credit_pre_benefit_cap import ( + universal_credit_pre_benefit_cap as PreCap, +) + +YEAR = 2025 +UC_MIXED = [14_753.0, 0.0, 0.0] +HB_MIXED = [0.0, 4_978.20, 0.0] +FIRST_REQUESTS = [ + "universal_credit", + "housing_benefit", + "housing_benefit_non_dep_deductions", +] +NO_AWARDS = dict( + child_benefit=0.0, + child_tax_credit=0.0, + working_tax_credit=0.0, + income_support=0.0, + jsa_income=0.0, + esa_income=0.0, + pension_credit=0.0, + uc_deductions=0.0, + uc_deduction_rate=0.0, + uc_childcare_element=0.0, +) + + +@pytest.fixture(scope="module") +def system(): + return CountryTaxBenefitSystem() + + +def situation(which="mixed", year=YEAR, full_uc=False): + """The claimant's household ("single"), the non-dependant's + ("nondep"), or both ("mixed"), with no cap total supplied.""" + people, units, households = {}, {}, {} + if which in ("single", "mixed"): + people["uc"] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=1_000.0, + weekly_hours=20.0, + ) + units["uc"] = dict( + members=["uc"], + **NO_AWARDS, + would_claim_uc=True, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=False, + ) + if not full_uc: + units["uc"]["universal_credit_pre_benefit_cap"] = 20_000.0 + households["single"] = dict( + members=["uc"], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=0.0, + tenure_type="RENT_PRIVATELY", + ) + if which in ("nondep", "mixed"): + people["applicant"] = dict(age=70, current_education="NOT_IN_EDUCATION") + people["nondep"] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=9_000.0, + weekly_hours=20.0, + ) + for name in ["applicant", "nondep"]: + units[name] = dict( + members=[name], + **NO_AWARDS, + would_claim_uc=False, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=name == "applicant", + ) + if not full_uc: + units[name]["universal_credit_pre_benefit_cap"] = 0.0 + households["nondep"] = dict( + members=["applicant", "nondep"], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=6_000.0, + tenure_type="RENT_PRIVATELY", + ) + return keyed(dict(people=people, benunits=units, households=households), year) + + +def keyed(data, year): + """Key every input by year.""" + for entities in data.values(): + for entity in entities.values(): + for name, value in entity.items(): + if name != "members" and not isinstance(value, dict): + entity[name] = {year: value} + return data + + +def supply(data, variable, values, year=YEAR): + """Supply ``variable`` for every benefit unit, from ``values`` by unit + name (nil for a unit it leaves out).""" + for name, unit in data["benunits"].items(): + unit[variable] = {year: float(values.get(name, 0.0))} + return data + + +def make(system, data, year=YEAR, trace=False): + return Simulation( + tax_benefit_system=system, + situation=copy.deepcopy(data), + default_input_period=year, + default_calculation_period=year, + trace=trace, + ) + + +def with_totals(which, **inputs): + """The population with the combined total its Universal Credit and + Housing Benefit imply supplied, and any other inputs.""" + data = situation(which) + supply(data, "benefit_cap_welfare_benefits", {"uc": 20_000.0}) + for variable, values in inputs.items(): + supply(data, variable, values) + return data + + +def traced_names(simulation): + return {key.split("<")[0] for key in simulation.tracer.get_serialized_flat_trace()} + + +@pytest.mark.parametrize("first", FIRST_REQUESTS) +def test_a_supplied_universal_credit_basket_keeps_households_independent(system, first): + """With the combined total and Universal Credit's basket supplied, each + household gets its results alone, whatever the first request.""" + basket = {"uc": 20_000.0} + alone = [ + make(system, with_totals(which, uc_benefit_cap_welfare_benefits=basket)) + for which in ["single", "nondep"] + ] + mixed = make(system, with_totals("mixed", uc_benefit_cap_welfare_benefits=basket)) + mixed.calculate(first, YEAR) + for name, expected in [ + ("universal_credit", UC_MIXED), + ("housing_benefit", HB_MIXED), + ]: + result = mixed.calculate(name, YEAR) + np.testing.assert_allclose(result, expected, atol=0.01, err_msg=name) + np.testing.assert_allclose( + result, + np.concatenate([sim.calculate(name, YEAR) for sim in alone]), + atol=0.01, + err_msg=name, + ) + np.testing.assert_allclose( + mixed.calculate("housing_benefit_non_dep_deductions", YEAR), + [0.0, 1_021.80, 0.0], + atol=0.01, + ) + + +@pytest.mark.parametrize("first", FIRST_REQUESTS) +def test_a_supplied_housing_benefit_gives_its_arithmetic(system, first): + """With the combined total and Housing Benefit supplied, the basket is + their difference, and Universal Credit and Housing Benefit follow from + it. The claimant's total of £26,000 includes £6,000 of Housing Benefit + (as in specified accommodation): the basket counts £20,000, £5,247 over + the cap, so the award comes to £14,753, and HB reg 75F(1)(g) leaves the + Housing Benefit uncapped. The applicant's total is their £4,978.20 of + Housing Benefit, and pension age puts them outside the cap.""" + data = situation("mixed") + supply( + data, + "benefit_cap_welfare_benefits", + {"uc": 26_000.0, "applicant": 4_978.20}, + ) + supply( + data, + "housing_benefit_pre_benefit_cap", + {"uc": 6_000.0, "applicant": 4_978.20}, + ) + sim = make(system, data) + sim.calculate(first, YEAR) + np.testing.assert_allclose( + sim.calculate("uc_benefit_cap_welfare_benefits", YEAR), + [20_000.0, 0.0, 0.0], + atol=0.01, + ) + np.testing.assert_allclose( + sim.calculate("universal_credit", YEAR), UC_MIXED, atol=0.01 + ) + np.testing.assert_allclose( + sim.calculate("housing_benefit", YEAR), [6_000.0, 4_978.20, 0.0], atol=0.01 + ) + # The deduction still reads the non-dependant's wages and Universal + # Credit; the supplied Housing Benefit does not read the deduction. + np.testing.assert_allclose( + sim.calculate("housing_benefit_non_dep_deductions", YEAR), + [0.0, 1_021.80, 0.0], + atol=0.01, + ) + + +@pytest.mark.parametrize("which", ["mixed", "single"]) +def test_a_supplied_total_alone_raises(system, which): + """A supplied combined total without housing_benefit_pre_benefit_cap or + uc_benefit_cap_welfare_benefits raises ValueError, with or without a + non-dependant in the population, and the message names both inputs.""" + sim = make(system, with_totals(which)) + with pytest.raises(ValueError) as error: + sim.calculate("universal_credit", YEAR) + message = str(error.value) + assert "housing_benefit_pre_benefit_cap" in message + assert "uc_benefit_cap_welfare_benefits" in message + assert "benefit_cap_welfare_benefits" in message + + +def test_a_reformed_total_leaves_universal_credit_on_its_basket(system): + """A reform that gives benefit_cap_welfare_benefits a new formula changes + the Housing Benefit cap's total, and Universal Credit's basket still adds + up the benefits itself.""" + + def formula(benunit, period, parameters): + return CombinedTotal.formula(benunit, period, parameters) + 1_000 + + class ReformedTotal(Reform): + def apply(self): + self.update_variable( + type( + "benefit_cap_welfare_benefits", + (CombinedTotal,), + dict(formula=formula), + ) + ) + + baseline = make(system, situation("mixed")) + reformed = make(ReformedTotal(system), situation("mixed")) + np.testing.assert_allclose( + reformed.calculate("benefit_cap_welfare_benefits", YEAR), + baseline.calculate("benefit_cap_welfare_benefits", YEAR) + 1_000, + atol=0.01, + ) + for name in ["uc_benefit_cap_welfare_benefits", "universal_credit"]: + np.testing.assert_allclose( + reformed.calculate(name, YEAR), + baseline.calculate(name, YEAR), + atol=0.01, + err_msg=name, + ) + np.testing.assert_allclose( + reformed.calculate("universal_credit", YEAR), UC_MIXED, atol=0.01 + ) + + +def test_model_values_supply_neither_input(system): + """set_model_value stores a value as the model's own, so it supplies + neither the combined total nor Housing Benefit. A model value of the + total leaves Universal Credit's basket adding up the benefits itself; a + supplied total with a model value of Housing Benefit still raises.""" + baseline = make(system, situation("mixed")) + basket = baseline.calculate("uc_benefit_cap_welfare_benefits", YEAR) + stored_total = make(system, situation("mixed")) + set_model_value( + stored_total, + "benefit_cap_welfare_benefits", + YEAR, + np.array([30_000.0, 0.0, 0.0]), + ) + np.testing.assert_allclose( + stored_total.calculate("uc_benefit_cap_welfare_benefits", YEAR), + basket, + atol=0.01, + ) + np.testing.assert_allclose( + stored_total.calculate("universal_credit", YEAR), UC_MIXED, atol=0.01 + ) + stored_housing_benefit = make(system, with_totals("mixed")) + set_model_value( + stored_housing_benefit, + "housing_benefit_pre_benefit_cap", + YEAR, + np.array([0.0, 4_978.20, 0.0]), + ) + with pytest.raises(ValueError, match="housing_benefit_pre_benefit_cap"): + stored_housing_benefit.calculate("universal_credit", YEAR) + + +UC_HOUSEHOLD = st.fixed_dictionaries( + dict( + kind=st.just("uc"), + universal_credit_pre_benefit_cap=st.floats(0, 40_000), + uc_benefit_cap=st.sampled_from([14_753.0, 22_020.0]), + child_benefit=st.sampled_from([0.0, 1_500.0]), + employment_income=st.floats(0, 20_000), + ) +) +NON_DEPENDANT_HOUSEHOLD = st.fixed_dictionaries( + dict( + kind=st.just("nondep"), + rent=st.sampled_from([3_000.0, 6_000.0]), + employment_income=st.floats(0, 30_000), + weekly_hours=st.sampled_from([0.0, 20.0, 35.0]), + universal_credit_pre_benefit_cap=st.sampled_from([0.0, 5_000.0, 15_000.0]), + ) +) + + +def generated_population(households): + """Each generated household, with its Universal Credit basket and the + combined total it implies supplied for every benefit unit.""" + people, units, homes, basket = {}, {}, {}, {} + for h, household in enumerate(households): + if household["kind"] == "uc": + claimant = f"uc_{h}" + people[claimant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=household["employment_income"], + weekly_hours=20.0, + ) + units[claimant] = dict( + members=[claimant], + **{**NO_AWARDS, "child_benefit": household["child_benefit"]}, + would_claim_uc=True, + uc_benefit_cap=household["uc_benefit_cap"], + benunit_is_rent_liable=False, + universal_credit_pre_benefit_cap=household[ + "universal_credit_pre_benefit_cap" + ], + ) + basket[claimant] = ( + household["universal_credit_pre_benefit_cap"] + + household["child_benefit"] + ) + homes[f"home_{h}"] = dict( + members=[claimant], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=0.0, + tenure_type="RENT_PRIVATELY", + ) + else: + applicant, non_dependant = f"applicant_{h}", f"nondep_{h}" + on_uc = household["universal_credit_pre_benefit_cap"] > 0 + people[applicant] = dict(age=70, current_education="NOT_IN_EDUCATION") + people[non_dependant] = dict( + age=40, + current_education="NOT_IN_EDUCATION", + employment_income=household["employment_income"], + weekly_hours=household["weekly_hours"], + ) + units[applicant] = dict( + members=[applicant], + **NO_AWARDS, + would_claim_uc=False, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=True, + universal_credit_pre_benefit_cap=0.0, + ) + units[non_dependant] = dict( + members=[non_dependant], + **NO_AWARDS, + would_claim_uc=on_uc, + uc_benefit_cap=14_753.0, + benunit_is_rent_liable=False, + universal_credit_pre_benefit_cap=household[ + "universal_credit_pre_benefit_cap" + ], + ) + basket[non_dependant] = household["universal_credit_pre_benefit_cap"] + homes[f"home_{h}"] = dict( + members=[applicant, non_dependant], + country="ENGLAND", + local_authority="MAIDSTONE", + rent=household["rent"], + tenure_type="RENT_PRIVATELY", + ) + data = keyed(dict(people=people, benunits=units, households=homes), YEAR) + # The supplied units hold no Housing Benefit in the cap: the claimants + # pay no rent, and the applicants have reached pension age. + supply(data, "uc_benefit_cap_welfare_benefits", basket) + supply(data, "benefit_cap_welfare_benefits", basket) + return data + + +@settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + households=st.lists( + st.one_of(UC_HOUSEHOLD, NON_DEPENDANT_HOUSEHOLD), min_size=1, max_size=3 + ) +) +def test_each_household_gets_its_results_alone(system, households): + """For any mix of independent households, with Universal Credit's basket + supplied, each household's Universal Credit, Housing Benefit and + non-dependant deductions equal its results alone.""" + mixed = make(system, generated_population(households)) + alone = [ + make(system, generated_population([household])) for household in households + ] + for name in [ + "universal_credit", + "housing_benefit", + "housing_benefit_non_dep_deductions", + ]: + np.testing.assert_allclose( + mixed.calculate(name, YEAR), + np.concatenate([sim.calculate(name, YEAR) for sim in alone]), + atol=0.01, + err_msg=name, + ) + + +def test_computed_universal_credit_and_local_schemes(system): + """With nothing supplied, Universal Credit's traced calculation reads no + Housing Benefit variable but the reported award, and the national and + local Council Tax Reduction schemes then read Universal Credit.""" + # Each local scheme's parameters start in 2026-27. + year = 2026 + template = situation("mixed", year=year, full_uc=True) + template["people"]["uc"]["housing_benefit_reported"] = {year: 6_000.0} + data = {group: {} for group in template} + for authority in [ + "MERTON", + "KINGSTON_UPON_THAMES", + "NEWHAM", + "WESTMINSTER", + "OXFORD", + ]: + prefix = authority.lower() + "_" + for group, entities in template.items(): + for name, entity in entities.items(): + copied = copy.deepcopy(entity) + if "members" in copied: + copied["members"] = [ + prefix + member for member in copied["members"] + ] + if group == "benunits": + # A reported Housing Benefit award otherwise makes + # claims_all_entitled_benefits false, so this family + # would not claim CTR by default. + copied["would_claim_council_tax_reduction"] = {year: True} + if group == "households": + copied["local_authority"] = {year: authority} + copied["council_tax"] = {year: 2_000.0} + data[group][prefix + name] = copied + sim = make(system, data, year=year, trace=True) + universal_credit = sim.calculate("universal_credit", year) + names = traced_names(sim) + assert universal_credit[0] > 0 + assert "uc_maximum_amount" in names + assert "housing_benefit_reported" in names + assert not { + name + for name in names + if name.startswith("housing_benefit") and name != "housing_benefit_reported" + } + for name in [ + "housing_benefit", + "council_tax_reduction", + "merton_council_tax_reduction", + "kingston_upon_thames_council_tax_reduction", + "newham_council_tax_reduction", + "westminster_council_tax_reduction", + "oxford_council_tax_reduction", + ]: + values = sim.calculate(name, year) + if name.endswith("_council_tax_reduction"): + assert values.max() > 0, (name, values) + + +@pytest.mark.parametrize( + "variable,parent", + [ + ("uc_benefit_cap_welfare_benefits", Basket), + ("universal_credit_pre_benefit_cap", PreCap), + ], +) +def test_a_reform_reading_calculated_housing_benefit_raises_cycle_error( + system, variable, parent +): + """A reform that adds calculated Housing Benefit to Universal Credit's + basket or award makes Universal Credit and the deductions depend on each + other, and the engine raises CycleError.""" + + def formula(benunit, period, parameters): + return parent.formula(benunit, period, parameters) + benunit( + "housing_benefit", period + ) + + replacement = type(variable, (parent,), dict(formula=formula)) + + class CyclicReform(Reform): + def apply(self): + self.update_variable(replacement) + + data = situation("mixed") + for unit in data["benunits"].values(): + unit.pop(variable, None) + with pytest.raises(CycleError): + make(CyclicReform(system), data).calculate("housing_benefit", YEAR) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 7d2acc30fd..be63ac1f6e 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -68,9 +68,12 @@ income and that award. The test skips incomes within a penny a week of an edge, since the inputs hold float32 values. 12. The Universal Credit cap counts the Housing Benefit cap's welfare - benefits other than Housing Benefit (UC Regs 2013 reg 80(2A)), so - universal_credit reads no Housing Benefit unless the inputs supply - benefit_cap_welfare_benefits. A non-dependant family's gross income + benefits other than Housing Benefit (UC Regs 2013 reg 80(2A)), and no + baseline formula on universal_credit's route calculates Housing Benefit: + its traced calculation reads no Housing Benefit variable but the reported + award. Inputs that supply benefit_cap_welfare_benefits, a total that + includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, + or the basket raises ValueError. A non-dependant family's gross income counts its award after the cap and before debt deductions: universal_credit plus uc_deductions, which equals the award before the cap less the cap reduction applied, under current law and under an 85% protected minimum @@ -78,9 +81,9 @@ universal_credit, each household's deductions equal those of the same household with the reformed award supplied, and the Housing Benefit deduction is the band of wages plus that award, so it moves when the - award crosses a band. With a supplied total, a calculated Housing - Benefit and a non-dependant on Universal Credit, the engine raises - CycleError. + award crosses a band. (test_non_dependant_cycle_contracts.py checks the + supplied-total contract over mixed populations, and that a reform making + Universal Credit read calculated Housing Benefit raises CycleError.) 13. Differential: the Universal Credit exemption's earned-income test agrees with Universal Credit's own calculation (uc_individual_earned_income) across employment expenses, trading losses, company income attributed to @@ -1132,19 +1135,16 @@ def award_households(households, awards=None, scaled=True): ) -def test_universal_credit_reads_housing_benefit_only_in_a_supplied_total(): - """Invariant 12: Universal Credit's calculation reads no Housing Benefit, - so the deductions can read universal_credit. A supplied - benefit_cap_welfare_benefits includes Housing Benefit, so Universal - Credit takes out housing_benefit_pre_benefit_cap. Where the simulation - calculates that Housing Benefit for a household with a non-dependant on - Universal Credit, the award and the deduction depend on each other, and - the engine raises.""" - from policyengine_core.errors import CycleError - +def test_universal_credit_reads_no_calculated_housing_benefit(): + """Invariant 12: Universal Credit's traced calculation reads no Housing + Benefit variable but the reported award, so the deductions can read + universal_credit. Inputs that supply benefit_cap_welfare_benefits, a + total that includes Housing Benefit, also supply + housing_benefit_pre_benefit_cap, and Universal Credit reads that input + without calculating Housing Benefit. A supplied total without it raises + ValueError.""" year = 2025 - households = [BAND_CROSSING] - situation = award_households(households, scaled=False) + situation = award_households([BAND_CROSSING], scaled=False) sim = core_simulation(situation, year, trace=True) sim.calculate("universal_credit", year) read = {key.split("<")[0] for key in sim.tracer.get_serialized_flat_trace()} @@ -1158,8 +1158,19 @@ def test_universal_credit_reads_housing_benefit_only_in_a_supplied_total(): unit["benefit_cap_welfare_benefits"] = { str(year): unit["universal_credit_pre_benefit_cap"][str(year)] } - with pytest.raises(CycleError): + with pytest.raises(ValueError, match="housing_benefit_pre_benefit_cap"): core_simulation(situation, year).calculate("housing_benefit", year) + for unit in situation["benunits"].values(): + unit["housing_benefit_pre_benefit_cap"] = {str(year): 0.0} + sim = core_simulation(situation, year, trace=True) + sim.calculate("universal_credit", year) + read = {key.split("<")[0] for key in sim.tracer.get_serialized_flat_trace()} + assert not [ + name + for name in read + if name.startswith("housing_benefit") + and name not in ("housing_benefit_reported", "housing_benefit_pre_benefit_cap") + ], read @PROPERTY_SETTINGS diff --git a/policyengine_uk/tests/test_non_dependant_population_consistent_total.py b/policyengine_uk/tests/test_non_dependant_population_consistent_total.py new file mode 100644 index 0000000000..074d84ad24 --- /dev/null +++ b/policyengine_uk/tests/test_non_dependant_population_consistent_total.py @@ -0,0 +1,104 @@ +"""Supplied cap totals that agree with each household's own calculation. + +Each household's combined total and Housing Benefit come from its own +simulation with nothing supplied. Supplying both for a population of those +households gives every household the results it gets alone, whatever the +first request asks for. Supplying the totals without the Housing Benefit +raises ValueError (test_non_dependant_cycle_contracts.py states the +contract). +""" + +import numpy as np +import pytest + +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.tests.test_non_dependant_cycle_contracts import ( + FIRST_REQUESTS, + YEAR, + make, + situation, + supply, +) + +HOUSEHOLDS = ["single", "nondep"] + + +@pytest.fixture(scope="module") +def system(): + return CountryTaxBenefitSystem() + + +@pytest.fixture(scope="module") +def standalone(system): + """Each household's own totals and results, with nothing supplied.""" + results = {} + for which in HOUSEHOLDS: + sim = make(system, situation(which)) + units = list(situation(which)["benunits"]) + results[which] = { + name: dict(zip(units, sim.calculate(name, YEAR).tolist())) + for name in [ + "benefit_cap_welfare_benefits", + "housing_benefit_pre_benefit_cap", + "universal_credit", + "housing_benefit", + ] + } + return results + + +def consistent(standalone, *variables): + """The mixed population with ``variables`` supplied at each household's + own values.""" + data = situation("mixed") + for variable in variables: + values = {} + for which in HOUSEHOLDS: + values.update(standalone[which][variable]) + supply(data, variable, values) + return data + + +def expected(standalone, name): + return [value for which in HOUSEHOLDS for value in standalone[which][name].values()] + + +def test_each_household_alone(standalone): + # The claimant pays no rent, so the total is the £20,000 of Universal + # Credit; the applicant's total is their own Housing Benefit, before a + # cap that pension age keeps away. + assert expected(standalone, "benefit_cap_welfare_benefits") == pytest.approx( + [20_000.0, 4_978.20, 0.0], abs=0.01 + ) + assert expected(standalone, "housing_benefit_pre_benefit_cap") == pytest.approx( + [0.0, 4_978.20, 0.0], abs=0.01 + ) + assert expected(standalone, "universal_credit") == pytest.approx( + [14_753.0, 0.0, 0.0], abs=0.01 + ) + + +@pytest.mark.parametrize("first", FIRST_REQUESTS) +def test_consistent_totals_keep_households_independent(system, standalone, first): + sim = make( + system, + consistent( + standalone, + "benefit_cap_welfare_benefits", + "housing_benefit_pre_benefit_cap", + ), + ) + sim.calculate(first, YEAR) + for name in ["universal_credit", "housing_benefit"]: + np.testing.assert_allclose( + sim.calculate(name, YEAR), + expected(standalone, name), + atol=0.01, + err_msg=name, + ) + + +def test_consistent_totals_without_housing_benefit_raise(system, standalone): + sim = make(system, consistent(standalone, "benefit_cap_welfare_benefits")) + with pytest.raises(ValueError, match="housing_benefit_pre_benefit_cap"): + sim.calculate("housing_benefit", YEAR) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index 21de4a9efc..b2a2b8f49a 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -43,8 +43,8 @@ def benefit_cap_couple(benunit, period): count this couple's welfare benefits (benefit_cap_welfare_benefits, and uc_benefit_cap_welfare_benefits without Housing Benefit). Universal Credit alone caps a family receiving both awards (UC Regs 2013 reg 80(2A); HB - Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two couples are the same unless - is_uc_claimant is entered. + Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two + couples are the same unless is_uc_claimant is entered. """ person = benunit.members on_universal_credit = benunit("universal_credit_pre_benefit_cap", period) > 0 diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index 5dec9471f1..a6b71589fd 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -68,6 +68,7 @@ "housing_benefit_applicable_income_childcare_element", "housing_benefit_applicable_income_disregard", "housing_benefit_childcare_earnings_limit", + "housing_benefit_pre_benefit_cap", "housing_benefit_specified_or_temporary_accommodation_disregard", "in_HE", "other_local_bus_trips", diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index 3471ff1c1e..8e27281a25 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -57,12 +57,18 @@ class benefit_cap_welfare_benefits(Variable): "another member of the benefit unit claims in their own right do not " "count. The total includes Universal Credit and Housing Benefit " "before the cap (universal_credit_pre_benefit_cap, " - "housing_benefit_pre_benefit_cap). Universal Credit counts Housing " - "Benefit as nil (UC Regs 2013 reg. 80(2A)), so " - "uc_benefit_cap_reduction subtracts it; an entered total should " - "include it. Widowed parent's allowance counts; maternity allowance " - "and the other bereavement benefits, also welfare benefits, are not " - "modelled here." + "housing_benefit_pre_benefit_cap). Universal Credit's cap reads " + "uc_benefit_cap_welfare_benefits, which totals the same benefits " + "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Inputs that " + "supply this total include Housing Benefit in it, and also supply " + "housing_benefit_pre_benefit_cap, which " + "uc_benefit_cap_welfare_benefits subtracts, or supply " + "uc_benefit_cap_welfare_benefits itself; otherwise Universal Credit's " + "cap raises ValueError. A reform of this variable changes the Housing " + "Benefit cap. To change the Universal Credit cap's basket, replace " + "uc_benefit_cap_welfare_benefits. Widowed parent's allowance counts; " + "maternity allowance and the other bereavement benefits, also welfare " + "benefits, are not modelled here." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 9c390ed462..e6480cfb82 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -55,9 +55,7 @@ def formula(benunit, period, parameters): p.great_britain, ) # Read Universal Credit before the benefit cap and deductions: - # entitlement counts whether or not DWP pays it. The amount paid - # (universal_credit) reads Housing Benefit where the inputs supply - # benefit_cap_welfare_benefits (uc_benefit_cap_welfare_benefits). + # entitlement counts whether or not DWP pays it. on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb on_legacy_benefit = benunit( "in_receipt_of_income_support_jsa_ib_or_esa_ir", period diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 4061946eaa..40710ed628 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -41,13 +41,6 @@ def formula(benunit, period, parameters): non_dependant = ( person("is_non_dependant_of_household_head", period) & ~joint_occupier ) - # With no non-dependant the council deducts nothing, so the formula - # reads no income. Universal Credit reads Housing Benefit where the - # inputs supply benefit_cap_welfare_benefits - # (uc_benefit_cap_welfare_benefits); a household of one family can - # then still calculate both. - if not non_dependant.any(): - return benunit.empty_array() deductions = ( person("household_benefits_individual_non_dep_deduction", period) * non_dependant diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py index bb60e1a70f..821e08e13d 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -1,7 +1,7 @@ from policyengine_uk.model_api import * from policyengine_uk.utils.benefit_cap import benefit_cap_couple from policyengine_uk.utils.benefit_unit import award_of_couple -from policyengine_uk.utils.supplied_inputs import stored_as_input +from policyengine_uk.utils.supplied_inputs import supplied_input from policyengine_uk.variables.gov.dwp.benefit_cap_welfare_benefits import ( CLAIMANT_OR_PARTNER_BENEFITS, FAMILY_BENEFITS, @@ -23,6 +23,18 @@ ] +def supplied_total_without_housing_benefit(period) -> str: + return ( + f"The inputs supply benefit_cap_welfare_benefits for {period}, a total " + "that includes Housing Benefit, which the Universal Credit cap counts " + "as nil (UC Regs 2013 reg. 80(2A)). Supply " + "housing_benefit_pre_benefit_cap for the same period, for the cap to " + "subtract, or supply uc_benefit_cap_welfare_benefits. The model doesn't " + "calculate that Housing Benefit for Universal Credit, because the " + "Housing Benefit non-dependant deductions read universal_credit." + ) + + class uc_benefit_cap_welfare_benefits(Variable): value_type = float entity = BenUnit @@ -30,12 +42,14 @@ class uc_benefit_cap_welfare_benefits(Variable): documentation = ( "The welfare benefits the Universal Credit cap counts: those in " "benefit_cap_welfare_benefits, for the same single person or couple, " - "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Universal " - "Credit then reads no Housing Benefit, so the Housing Benefit " - "non-dependant deductions can read universal_credit. Where the " + "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Where the " "inputs supply benefit_cap_welfare_benefits, whose total includes " - "Housing Benefit, this subtracts housing_benefit_pre_benefit_cap " - "from it, and Universal Credit reads Housing Benefit." + "Housing Benefit, this subtracts the supplied " + "housing_benefit_pre_benefit_cap. Inputs that supply the combined " + "total also supply housing_benefit_pre_benefit_cap or this variable; " + "otherwise the formula raises ValueError. No baseline formula on " + "Universal Credit's route calculates Housing Benefit, so the Housing " + "Benefit non-dependant deductions can read universal_credit." ) definition_period = YEAR unit = GBP @@ -46,7 +60,17 @@ class uc_benefit_cap_welfare_benefits(Variable): ) def formula(benunit, period, parameters): - if stored_as_input(benunit, "benefit_cap_welfare_benefits", period): + # A supplied combined total includes Housing Benefit, and the inputs + # supply that Housing Benefit too. Calculating it here would make + # Universal Credit depend on Housing Benefit, whose non-dependant + # deductions read universal_credit. A value a scenario stores as the + # model's own (set_model_value) supplies neither. + if supplied_input(benunit, "benefit_cap_welfare_benefits", period) is not None: + if ( + supplied_input(benunit, "housing_benefit_pre_benefit_cap", period) + is None + ): + raise ValueError(supplied_total_without_housing_benefit(period)) return benunit("benefit_cap_welfare_benefits", period) - benunit( "housing_benefit_pre_benefit_cap", period ) From 1bbd87d7ebf71b929c2b89ae42a8de2c4461cd6d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 15:57:52 -0500 Subject: [PATCH 53/67] Say who deducts in the non-dependant prose The council deducts the amounts from the claimant's award; the non-dependant pays nothing. Comments, documentation strings and test docstrings now name the council. Each YAML file parses to the same cases. Co-Authored-By: Claude Opus 5.5 --- ...benefits_individual_non_dep_deduction.yaml | 12 +++++----- ...using_benefit_non_dep_deduction_chain.yaml | 4 ++-- .../housing_benefit_non_dep_deductions.yaml | 8 +++---- ...ing_benefit_non_dep_remunerative_work.yaml | 2 +- .../non_dependant_deduction_review_cases.yaml | 22 ++++++++--------- .../claimant_family_scheme.yaml | 4 ++-- ...eduction_individual_non_dep_deduction.yaml | 4 ++-- ...ncil_tax_reduction_non_dep_deductions.yaml | 6 ++--- ...tion_non_dep_deductions_mixed_schemes.yaml | 2 +- ...il_tax_reduction_non_dep_gross_income.yaml | 4 ++-- ...x_reduction_non_dep_remunerative_work.yaml | 4 ++-- .../council_tax_reduction_pension_credit.yaml | 4 ++-- ...test_non_dependant_deduction_properties.py | 24 +++++++++---------- ...sharers_boarders_and_lodgers_properties.py | 4 ++-- .../non_dep_deduction/_non_dependants.py | 16 ++++++------- ...d_benefits_individual_non_dep_deduction.py | 12 +++++----- ...ousing_benefit_non_dep_deduction_exempt.py | 6 ++--- .../housing_benefit_non_dep_deductions.py | 8 +++---- ...efit_non_dep_deductions_claimant_exempt.py | 2 +- ...duction_applicant_has_non_dep_exemption.py | 2 +- ..._reduction_individual_non_dep_deduction.py | 10 ++++---- 21 files changed, 80 insertions(+), 80 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml index 6ecfaa2b49..863c91d88a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.yaml @@ -216,8 +216,8 @@ household: members: [worker, partner] output: - # reg 74(4): joint income 400 a week is in the 365 band; the partner, not - # in remunerative work, would pay 20.40 (reg 74(1)(b)). + # reg 74(4): joint income 400 a week is in the 365 band; the council would + # deduct 20.40 for the partner outside remunerative work (reg 74(1)(b)). household_benefits_individual_non_dep_deduction: [5_470.40, 1_060.80] - name: A dependent child's income does not count towards the couple's @@ -277,8 +277,8 @@ working_tax_credit: 0 child_benefit: 0 output: - # reg 6(6): a person on income-related ESA is treated as not in - # remunerative work, so pays the reg 74(1)(b) amount. + # reg 6(6): the council treats a person on income-related ESA as outside + # remunerative work and deducts the reg 74(1)(b) amount. household_benefits_individual_non_dep_deduction: 20.40 * 52 - name: A dependent young person in the family is banded on their own income, not the parent's @@ -310,8 +310,8 @@ members: [parent, young_person] output: # reg 74(4) joins only a couple's income. The parent's 180 a week is - # under the 192 band (20.40); the young person, under 16 hours, pays 20.40 - # if not exempt (eligibility is an input here). + # under the 192 band (20.40); for the young person, under 16 hours, the council deducts 20.40 + # unless they are exempt (eligibility is an input here). household_benefits_individual_non_dep_deduction: [1_060.80, 1_060.80] # 2025-26 bands and deductions (HB Regs 2006 reg 74 as up-rated by SI 2025/295 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml index 0b510e985c..f3f5835c1d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_chain.yaml @@ -3,8 +3,8 @@ # award is the eligible rent less the non-dependant deductions. - name: A zero-income full-time student does not reduce the claimant's HB - # The counterexample from the PolicyEngine/policyengine-uk#1925 review: - # HB Regs 2006 reg 74(7)(c)-(d). + # A full-time student lives with the claimant; HB Regs 2006 + # reg 74(7)(c)-(d) exempts the student from a deduction. period: 2025 absolute_error_margin: 0.01 input: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml index 8b11ac331d..88b1a2db3e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.yaml @@ -170,8 +170,8 @@ # The review case on #2184: an existing claimant in council housing with an # adult child whose only income is £28,912 of other investment income and who # does no paid work. The bands apply only in remunerative work (HB Regs 2006 -# reg 74(2)), so the adult child pays the reg 74(1)(b) amount, as one with no -# income would. +# reg 74(2)), so the council deducts the reg 74(1)(b) amount for the adult child, +# as it would for one with no income. - name: A non-dependant's investment income does not raise the Housing Benefit deduction period: 2025 absolute_error_margin: 0.01 @@ -250,7 +250,7 @@ # (is_uc_claimant), so it counts in his gross income, not his parents' # (HB Regs 2006 reg 74(2), (4)). The parents' joint income is the father's # 180 a week, under the 192 band: 20.40, one deduction for the couple - # (reg 74(3)). The son works 2 hours, so he pays 20.40 (reg 74(1)(b)). + # (reg 74(3)). The son works 2 hours, so the council deducts 20.40 for him (reg 74(1)(b)). # (20.40 + 20.40) * 52 = 2,121.60. Counting the son's 100 a week of UC in # the parents' income would give 280 a week and 64.35. period: 2026 @@ -305,7 +305,7 @@ # The adult son claims income-related ESA in his own right, so it is his # income, not his parents' (HB Regs 2006 reg 74(4)). The parents' joint # income is the father's 180 a week, under the 192 band: 20.40, one - # deduction for the couple. The son does no work and pays 20.40 (reg + # deduction for the couple. The son does no work, so the council deducts 20.40 for him (reg # 74(1)(b)). (20.40 + 20.40) * 52 = 2,121.60. Counting his 100 a week of # ESA in the parents' income would give 280 a week and 64.35. period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml index 7d0dde3004..f6bde9e1e8 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/housing_benefit_non_dep_remunerative_work.yaml @@ -1,6 +1,6 @@ # HB Regs 2006 reg 74(1)(b), (2) and reg 6(1), (6); pension-age # HB Regs 2006 reg 55(1)(b), (2) and reg 6(1), (6). -# A non-worker or 15-hour worker pays 20.40 * 52 = 1,060.80 in 2026-27, +# For a non-worker or 15-hour worker, the council deducts 20.40 * 52 = 1,060.80 in 2026-27, # whatever their income. The exemptions in reg 74(7), (8) and (10) and reg # 55(7)-(9) act through eligibility, which most cases here take as an input. # Family awards, which a single non-dependant's gross income would count, are diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml index 4e51f02aab..633c7c7dfb 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/non_dep_deduction/non_dependant_deduction_review_cases.yaml @@ -240,8 +240,8 @@ country: ENGLAND local_authority: MAIDSTONE output: - # UC Regs 2013 reg 55(5)(a): relievable pension contributions are deducted - # from employed earnings, leaving none (HB reg 74(8)(b); CTR para 8(8)(c)). + # DWP deducts relievable pension contributions from employed earnings + # (UC Regs 2013 reg 55(5)(a)), leaving none (HB reg 74(8)(b); CTR para 8(8)(c)). housing_benefit_non_dep_deduction_exempt: [false, true] council_tax_reduction_non_dep_deduction_exempt: [false, true] housing_benefit_non_dep_deductions: [0, 0] @@ -282,8 +282,8 @@ country: ENGLAND local_authority: MAIDSTONE output: - # UC Regs 2013 reg 62(2): a person to whom the floor applies is treated as - # having earned income equal to it. + # DWP treats a person to whom the floor applies as having earned income + # equal to it (UC Regs 2013 reg 62(2)). housing_benefit_non_dep_deduction_exempt: [false, false] council_tax_reduction_non_dep_deduction_exempt: [false, false] housing_benefit_non_dep_deductions: [1_060.80, 0] @@ -704,8 +704,8 @@ output: # UC Regs 2013 reg 55(4)(c): statutory paternity pay is employed earnings, # so the award is not calculated on no earned income and neither - # exemption applies (HB reg 74(8)(b); CTR para 8(8)(c)). Out of - # remunerative work, the lodger pays the lowest amounts, 20.40 and 5.20. + # exemption applies (HB reg 74(8)(b); CTR para 8(8)(c)). The lodger does no + # remunerative work, so the council deducts the lowest amounts, 20.40 and 5.20. housing_benefit_non_dep_deduction_exempt: [false, false] council_tax_reduction_non_dep_deduction_exempt: [false, false] housing_benefit_non_dep_deductions: [1_060.80, 0] @@ -807,7 +807,7 @@ # is" on Income Support and is not on Universal Credit, so both are exempt # from CTR. Housing Benefit has no such rule (reg 74(8)(a) is for the # person on IS, and only under 25): both are out of remunerative work, and - # the couple pays one lowest amount, 20.40 a week. + # the council deducts one lowest amount for the couple, 20.40 a week. council_tax_reduction_non_dep_deduction_exempt: [false, true, true] council_tax_reduction_non_dep_deductions: [0, 0] housing_benefit_non_dep_deductions: [1_060.80, 0] @@ -858,7 +858,7 @@ local_authority: GLASGOW_CITY output: # SSI 2012/303 reg 67(8)(a): only a non-dependant "who is on income - # support" is exempt. The partner, out of remunerative work, pays the + # support" is exempt. For the partner outside remunerative work, the council deducts the # lowest 2021-22 amount, 4.30 a week. council_tax_reduction_non_dep_deduction_exempt: [false, true, false] council_tax_reduction_non_dep_deductions: [223.60, 0] @@ -999,9 +999,9 @@ output: # LGFA 1992 Sch 1 para 3 needs another person to be "entitled to child # benefit" for the young person, and entitlement needs a claim (SSAA 1992 - # s.1(1)). With no claim, both are charged: the parent's 200 a week is - # under the CTR 279 band (5.20), and the young person, out of remunerative - # work, pays the lowest amount (5.20). + # s.1(1)). With no claim, the council deducts for both: the parent's 200 a week is + # under the CTR 279 band (5.20), and for the young person outside remunerative + # work, the council deducts the lowest amount (5.20). council_tax_reduction_non_dep_deduction_exempt: [false, false, false] council_tax_reduction_non_dep_deductions: [540.80, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml index cece32c759..7dc97e3c1e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/claimant_family_scheme.yaml @@ -251,8 +251,8 @@ # The adult child's own scheme (Maidstone's working-age one) is not # simulated, but the household's bill is: the head's pensioner claim # covers all of it, less the non-dependant deduction for the adult child, - # who is not in remunerative work, not a student and has no award, so - # pays the lowest amount (SI 2012/2885 Sch 1 para 8(1)(b): 5.20 * 52 = + # who is not in remunerative work, not a student and has no award. The + # council deducts the lowest amount (SI 2012/2885 Sch 1 para 8(1)(b): 5.20 * 52 = # 270.40). The child's report cannot be added on top. council_tax_reduction_scheme_supported: [true, false] council_tax_reduction_household_has_simulated_claim: true diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml index e013c7b3ac..d96e582366 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.yaml @@ -1,7 +1,7 @@ # The national Council Tax Reduction non-dependant deduction bands normal # gross weekly income, including investment income, but only for a -# non-dependant in remunerative work (SI 2012/2885 Sch 1 para 8(1)-(2)); one -# not in remunerative work pays the lowest amount whatever their income. The +# non-dependant in remunerative work (SI 2012/2885 Sch 1 para 8(1)-(2)); the council +# deducts the lowest amount for one outside remunerative work whatever their income. The # England pensioner scheme's 2025-26 scale (SI 2025/39): under £266 a week # 5.00; £266 10.20; £463 12.80; £577 and above 15.35. diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml index abf7d73ac4..71c342a701 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions.yaml @@ -5,7 +5,7 @@ # England: under 279 5.20; 279 10.60; 485 13.30; 605 and above 15.95. # Wales: under 279 6.70; 279 13.35; 485 16.80; 605 and above 20.05. # Scotland: under 286 5.55; 286 10.95; 497 13.90; 614 and above 16.55. -# A non-dependant not in remunerative work (under 16 hours) pays the lowest. +# The council deducts the lowest amount for a non-dependant outside remunerative work (under 16 hours). # The applicant is the benefit unit with the household's oldest member, so # working-age applicants here are 60. @@ -1161,8 +1161,8 @@ council_tax: 2_000 savings: 0 output: - # para 8(3)-(4): joint 300 a week is in the 279 band (10.60); the partner - # not in remunerative work would pay 5.20; only the higher is deducted. + # para 8(3)-(4): joint 300 a week is in the 279 band (10.60); for the partner + # outside remunerative work the council would deduct 5.20; it deducts only the higher. council_tax_reduction_individual_non_dep_deduction: [0, 551.20, 270.40] council_tax_reduction_non_dep_deductions: [551.20, 0] diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml index 171fac905c..f2f1382d1d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_deductions_mixed_schemes.yaml @@ -4,7 +4,7 @@ # equally between them (Sch 1 para 8(5); WSI 2013/3029 Sch 1 para 3(5) and # Sch 6 para 5(5); SSI 2021/249 reg 90(5)). 2026-27 scales: England # pensioners 279 band 10.60; Wales 279 band 13.35; Scotland lowest 5.55, 286 -# band 10.95. Cases from the round-6 review of #2007. +# band 10.95. - name: England, working-age head and pension-age sharer, deduction from the sharer's pensioner claim period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml index b9a056f778..064cdf59f2 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_gross_income.yaml @@ -5,8 +5,8 @@ # maintenance; the listed disability payments are disregarded. # Hand arithmetic: wages 9,000 + statutory pay 520 = 9,520, just above # 183/week. Each national scheme bands it on its own 2025-26 scale, below -# the first band's lower edge, so the non-dependant pays the scheme's lowest -# amount: England 5.00 (SI 2025/39), Scotland 5.35 (SSI 2025/24) and Wales +# the first band's lower edge, so the council deducts the scheme's lowest +# amount for the non-dependant: England 5.00 (SI 2025/39), Scotland 5.35 (SSI 2025/24) and Wales # 6.25 (WSI 2025/59) a week. # Annual wages and statutory pay can relate to different parts of the year; # leave days themselves are not remunerative work (HB reg 6(7)). diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml index b660578932..c3dd3a9753 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_non_dep_remunerative_work.yaml @@ -1,8 +1,8 @@ # England SI 2012/2885 Sch 1 para 8 and reg 10; Scotland SSI 2021/249 # reg 90 and reg 10, SSI 2012/319 reg 48 and reg 6; Wales WSI 2013/3029 # Sch 1 para 3, Sch 6 para 5 and reg 10: income banding requires remunerative -# work. Each scheme's 2026-27 scale: a non-dependant outside remunerative work -# pays the lowest amount (England 5.20, SI 2026/27; Scotland 5.55, SSI +# work. Under each scheme's 2026-27 scale, the council deducts the lowest amount +# for a non-dependant outside remunerative work (England 5.20, SI 2026/27; Scotland 5.55, SSI # 2026/56; Wales 6.70, WSI 2026/13), and 403.85/week of income in work falls # in the band from £279 in England (10.60) and Wales (13.35) and from £286 in # Scotland (10.95). The cases hold the family's awards, which a single diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml index 336f49fa0d..1a4f081d36 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml @@ -251,8 +251,8 @@ guarantee_credit: [3_376, 0] in_receipt_of_guarantee_credit: [true, false] council_tax_reduction_applicable_income: [0, 5_000] - # SI 2012/2885 Sch 1 para 8(1)(b): the son has no hours, so he is not in - # remunerative work and pays the 2026-27 lowest amount, 5.20 a week. + # SI 2012/2885 Sch 1 para 8(1)(b): the son has no hours, so the council deducts + # the 2026-27 lowest amount, 5.20 a week, for him outside remunerative work. council_tax_reduction_non_dep_deductions: [270.40, 0] # 2,000 - 270.40 simulated_council_tax_reduction_benunit: [1_729.60, 0] diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index be63ac1f6e..b2c29e3787 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -13,12 +13,12 @@ 1. An exempt non-dependant's individual deduction is 0, for Housing Benefit and for Council Tax Reduction. -2. Bounds: an eligible non-dependant who is not exempt pays between the - scale's lowest and highest weekly amounts. In English working-age - households (local schemes) the national CTR deduction is 0. -3. A non-dependant not in remunerative work (under 16 hours or, for Housing - Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)) pays the lowest amount, - whatever their income. +2. Bounds: for an eligible non-dependant who is not exempt, the council + deducts between the scale's lowest and highest weekly amounts. In English + working-age households (local schemes) the national CTR deduction is 0. +3. For a non-dependant outside remunerative work (under 16 hours or, for Housing + Benefit, on IS, JSA(IB) or ESA(IR): HB reg 6(6)), the council deducts the lowest + amount whatever their income. 4. If the claimant or partner is blind or receives a qualifying disability benefit (read from the generated inputs, and checked against the model's flags), the claimant family's deductions are 0. @@ -56,10 +56,10 @@ eldest where the inputs flag more, which the test checks. Earned income is Universal Credit's own calculation (invariant 13). 10. (In test_rent_of_sharers_boarders_and_lodgers_properties.py.) -11. Banding: a non-dependant in remunerative work who is neither exempt nor - on IS, JSA(IB) or ESA(IR) pays the amount of the band whose lower edge is - the highest not above their normal gross weekly income, computed here from - the generated inputs: earnings, statutory sick, maternity and paternity +11. Banding: for a non-dependant in remunerative work who is neither exempt nor + on IS, JSA(IB) or ESA(IR), the council deducts the amount of the band whose lower + edge is the highest not above their normal gross weekly income, computed here + from the generated inputs: earnings, statutory sick, maternity and paternity pay, self-employed profit with a loss as nil, and other investment income; for the claimant or partner of the non-dependant's family, the couple's sum plus the family's IS, JSA(IB), ESA(IR) and Pension Credit, and its @@ -1291,8 +1291,8 @@ def test_deduction_follows_a_reformed_award(households): year=2026, ) def test_earned_income_exemption_reads_universal_credit(families, year): - """Invariant 13: the Universal Credit exemption's earned-income test is - Universal Credit's own calculation (HB Regs 2006 reg 74(8)(b), (11); WSI + """Invariant 13: compare the Universal Credit exemption's earned-income test + with Universal Credit's own calculation (HB Regs 2006 reg 74(8)(b), (11); WSI 2013/3029 Sch 1 para 3(8)(c), (10)).""" people, benunits, households_, cases = {}, {}, {}, [] for f, family in enumerate(families): diff --git a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py index 0162003f6b..56150a0845 100644 --- a/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py +++ b/policyengine_uk/tests/test_rent_of_sharers_boarders_and_lodgers_properties.py @@ -290,8 +290,8 @@ def test_non_dependants(population): ) hb_total = sim.map_result(hb_individual, "person", "household") assert hb_total.max() > 0 - # Each non-dependant family pays one deduction for its couple, the higher - # (HB Regs 2006 reg 74(3)), and one for each other member. The pool is + # The council deducts once for each non-dependant couple, the higher amount + # (HB Regs 2006 reg 74(3)), and once for each other member. The pool is # apportioned over the families liable for the rent, and none falls on a # family whose claimant or partner is exempt (reg 74(6)). families = sim.populations["benunit"] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py index 565738d075..c50885abd1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/_non_dependants.py @@ -20,16 +20,16 @@ def non_dependant_weekly_gross_income(person, period, parameters, own_gross_income): - """Normal weekly gross income for the deduction bands. A claimant or - partner is banded on the couple's joint income (HB reg 74(4); CTR Sch 1 - para 8(4)): each member's own gross income plus the family's tax credits, + """Calculate normal weekly gross income for the deduction bands. The council + bands a claimant or partner on the couple's joint income (HB reg 74(4); CTR + Sch 1 para 8(4)): each member's own gross income plus the family's tax credits, child benefit, Income Support, income-based JSA, income-related ESA and Pension Credit, and its Universal Credit award (universal_credit_award) - where either member claims it. - Anyone else in the benefit unit is a separate non-dependant, banded on - their own income, with the Universal Credit where they alone claim it. - own_gross_income names the programme's person-level gross income - variable.""" + where either member claims it. The council bands each other member of the + benefit unit separately on their own income, adding Universal Credit + where that member claims it alone. own_gross_income names the programme's + person-level gross income variable. + """ claimant_or_partner = person("is_claimant_or_partner", period) own_income = person(own_gross_income, period) benunit = person.simulation.populations["benunit"] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py index d9389129d4..162816d04f 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/household_benefits_individual_non_dep_deduction.py @@ -10,12 +10,12 @@ class household_benefits_individual_non_dep_deduction(Variable): entity = Person label = "Housing Benefit individual non-dependent deduction" documentation = ( - "A non-dependant in remunerative work pays the deduction for the band " + "For a non-dependant in remunerative work, the council deducts the amount for the band " "containing their normal gross weekly income (a couple's joint income " "for a claimant or partner of the non-dependant's family); bands " - "include their lower edge. A non-dependant not in remunerative work, " + "include their lower edge. For a non-dependant outside remunerative work, " "including one on Income Support, income-based JSA or income-related " - "ESA (reg 6(6)), pays the lowest amount." + "ESA (reg 6(6)), the council deducts the lowest amount." ) definition_period = YEAR unit = GBP @@ -38,9 +38,9 @@ def formula(person, period, parameters): person("weekly_hours", period) >= p.remunerative_work_hours ) & ~is_on_legacy_income_related_benefit(person, period) # Reg 74(2), or pension-age reg 55(2): each band runs from "not less - # than" its lower edge. Reg 74(1)(b), or reg 55(1)(b): a non-dependant - # outside remunerative work pays the scale's lowest amount, whatever - # their income. + # than" its lower edge. Reg 74(1)(b), or reg 55(1)(b): for a non-dependant + # outside remunerative work the council deducts the scale's lowest + # amount whatever their income. banded = p.amount.calc(weekly_income) not_in_remunerative_work = p.amount.calc(np.zeros_like(weekly_income)) weekly = where(in_remunerative_work, banded, not_in_remunerative_work) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py index 9ed6cf1730..f653095d4a 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deduction_exempt.py @@ -37,9 +37,9 @@ def formula(person, period, parameters): on_legacy_income_related_benefit = person( "is_on_income_support", period ) | person("is_on_income_based_jsa", period) - # Entitlement before the benefit cap: universal_credit itself depends on - # Housing Benefit through the cap. The award is the claimants' it - # assesses. + # Read entitlement before the cap and debt deductions for the claimants + # the award assesses. DWP can reduce the payment to nil while + # entitlement still qualifies for the exemption. entitled_to_universal_credit = person("is_uc_assessed_claimant", period) & ( person.benunit("universal_credit_pre_benefit_cap", period) > 0 ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py index 40710ed628..85002fb197 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions.py @@ -25,12 +25,12 @@ class housing_benefit_non_dep_deductions(Variable): ) def formula(benunit, period, parameters): - # Deductions are made for non-dependants residing with the claimant + # The council deducts for non-dependants residing with the claimant # (HB Regs 2006 reg 74; HB (SPC) Regs 2006 reg 55). Joint occupiers, # boarders, lodgers and the landlord's household are not - # non-dependants (reg 3(2)(d)-(e), 3(4)). A couple pays one deduction, - # the higher (reg 74(3)). A non-dependant of more than one joint - # occupier is apportioned between them by their shares of the + # non-dependants (reg 3(2)(d)-(e), 3(4)). For a couple the council deducts once, + # the higher amount (reg 74(3)). For a non-dependant of more than one joint + # occupier, the council apportions the deduction by their shares of the # payments (reg 74(5); SPC reg 55(5)), so each family liable for the # household's rent bears its share; a boarder or lodger bears none. # A family bearing a share is a joint occupier, never a diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py index 5b612d8181..b8ed5eb93f 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/non_dep_deduction/housing_benefit_non_dep_deductions_claimant_exempt.py @@ -6,7 +6,7 @@ class housing_benefit_non_dep_deductions_claimant_exempt(Variable): entity = BenUnit label = "Exempt from all Housing Benefit non-dependant deductions" documentation = ( - "No deduction is made for any non-dependant if the claimant or partner " + "The council makes no deduction for any non-dependant if the claimant or partner " "is blind or receives Attendance Allowance, the DLA care component, the " "PIP daily living component or Armed Forces Independence Payment (SI " "2006/213 reg 74(6); SI 2006/214 reg 55(6)). Scottish disability " diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py index dde5d3fc01..f2483e0bbf 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicant_has_non_dep_exemption.py @@ -6,7 +6,7 @@ class council_tax_reduction_applicant_has_non_dep_exemption(Variable): entity = BenUnit label = "No non-dependant deductions from this family's Council Tax Reduction" documentation = ( - "Whether no deduction is made for any non-dependant from this family's " + "Whether the council makes no deduction for any non-dependant from this family's " "Council Tax Reduction because the applicant or partner is blind, or " "receives in respect of themselves Attendance Allowance, the care " "component of Disability Living Allowance, the daily living component " diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py index 90e46a9208..0956c62437 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_individual_non_dep_deduction.py @@ -11,14 +11,14 @@ class council_tax_reduction_individual_non_dep_deduction(Variable): label = "CTR individual non-dependent deduction" documentation = ( "The England pensioner, Scottish and Welsh schemes' own weekly scales. " - "A non-dependant in remunerative work pays the deduction for the band " + "For a non-dependant in remunerative work, the council deducts the amount for the band " "containing their normal gross weekly income (a couple's joint income " "for a claimant or partner of the non-dependant's family); bands " - "include their lower edge. A non-dependant not in remunerative work, " + "include their lower edge. For a non-dependant outside remunerative work, " "including one on Income Support, income-based JSA or income-related " - "ESA, pays the lowest amount; an exempt non-dependant pays nothing. English working-age local schemes have their own " - "variables. Reported under the scheme of the household head's family; " - "each applicant's deductions use its own scheme." + "ESA, the council deducts the lowest amount; for an exempt non-dependant it deducts nothing. English working-age local schemes have their own " + "variables. The person-level variable reports the scheme of the household head's family; " + "each applicant's deductions follow their own scheme." ) definition_period = YEAR unit = GBP From 922d0bd09236df494c34755d7d6251425826552c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 16:23:39 -0500 Subject: [PATCH 54/67] Name the supplied-total contract in the changelog Co-Authored-By: Claude Opus 5.5 --- changelog.d/2004.fixed.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 13ce85cfd4..47f0f18c41 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). +The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). Inputs that supply the combined total, `benefit_cap_welfare_benefits`, also supply `housing_benefit_pre_benefit_cap` or `uc_benefit_cap_welfare_benefits`; otherwise the Universal Credit cap raises `ValueError`. From 95fbf301495e4d39d4a6cd4991f060dbf6b17fa3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 17:39:21 -0500 Subject: [PATCH 55/67] Name the actors in the supplied-total docstrings Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_cycle_contracts.py | 15 ++++++++------- .../gov/dwp/benefit_cap_welfare_benefits.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_cycle_contracts.py b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py index 5e6418db00..7be5feccd5 100644 --- a/policyengine_uk/tests/test_non_dependant_cycle_contracts.py +++ b/policyengine_uk/tests/test_non_dependant_cycle_contracts.py @@ -8,7 +8,7 @@ total that includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, which the basket subtracts, or supply the basket itself. -Contract (an invariant for every population): either the inputs leave +The contract holds for every population: either the inputs leave benefit_cap_welfare_benefits to the model, or they supply housing_benefit_pre_benefit_cap or uc_benefit_cap_welfare_benefits beside it; otherwise the basket raises ValueError, whether or not any household holds a @@ -18,12 +18,13 @@ Credit read calculated Housing Benefit adds a cycle, and the engine raises CycleError. -Two independent households: a single Universal Credit claimant with £20,000 -before a £14,753 cap and no rent; and a 70-year-old paying £6,000 rent with a -40-year-old non-dependant who earns £9,000 at 20 hours and claims no -Universal Credit. Alone, the first gets £14,753 of Universal Credit, and the -council deducts £19.65 x 52 = £1,021.80 (HB Regs 2006 reg 74(2)(a), 2025-26) -from the second's rent, leaving £4,978.20 of Housing Benefit. +The tests use two independent households: a single Universal Credit +claimant with £20,000 before a £14,753 cap and no rent; and a 70-year-old +paying £6,000 rent with a 40-year-old non-dependant who earns £9,000 at 20 +hours and claims no Universal Credit. Alone, the first gets £14,753 of +Universal Credit. For the second's non-dependant the council deducts +£19.65 x 52 = £1,021.80 (HB Regs 2006 regs 70 and 74(2)(a), 2025-26), +leaving £4,978.20 of Housing Benefit. """ import copy diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index 8e27281a25..44ffe44b1e 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -60,7 +60,7 @@ class benefit_cap_welfare_benefits(Variable): "housing_benefit_pre_benefit_cap). Universal Credit's cap reads " "uc_benefit_cap_welfare_benefits, which totals the same benefits " "with Housing Benefit at nil (UC Regs 2013 reg. 80(2A)). Inputs that " - "supply this total include Housing Benefit in it, and also supply " + "supply this total include Housing Benefit in it. They also supply " "housing_benefit_pre_benefit_cap, which " "uc_benefit_cap_welfare_benefits subtracts, or supply " "uc_benefit_cap_welfare_benefits itself; otherwise Universal Credit's " From 2667976b828e2b622ebc4360971892609d0bca3a Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 03:31:38 -0500 Subject: [PATCH 56/67] Release the scaled award system's cache after the module test_test_module_caches_released fails on a module-level cache that no module-scoped fixture clears. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_non_dependant_deduction_properties.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index b2c29e3787..754f213b26 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -1067,6 +1067,14 @@ def apply(self): return ScaledAward(SYSTEM) +@pytest.fixture(scope="module", autouse=True) +def release_scaled_award_system(): + # The system also keeps its last simulation alive. Drop it once this + # module's tests are done, instead of for the rest of the test process. + yield + scaled_award_system.cache_clear() + + AWARD_YEAR = 2025 AWARD_HOUSEHOLD = st.fixed_dictionaries( dict( From 06a9df62ff9a3d87e21c8e74d930c0c89eac7937 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 13:44:58 +0000 Subject: [PATCH 57/67] Expect a Universal Credit bedroom for the benefit-cap example's young person The lone parent's 19-year-old in full-time education is past UC's terminal date, so is not a qualifying young person: they are a non-dependant with a bedroom (UC Regs 2013 Sch 4 paras 9(2) and 10(1)(c)), as Housing Benefit gives them one (reg. 13D(3)(b)). Assert two bedrooms under each scheme, and say in invariant 3 where the two counts still differ. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6FQikQp1mW8zi8m88r --- ...benefit_cap_lha_scheme_split_properties.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py index 5976d26204..157c97cbd0 100644 --- a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -38,10 +38,11 @@ schemes' size criteria to give the same number of bedrooms. The old category read the Universal Credit count (``LHA_allowed_bedrooms``); #2006 gave Housing Benefit its own (HB Regs 2006 reg. 13D(3), - ``housing_benefit_LHA_allowed_bedrooms``), which counts a young person - in the family whom the Universal Credit count leaves out, such as one - who is not a qualifying young person for Universal Credit. This change - leaves both counts as main has them. + ``housing_benefit_LHA_allowed_bedrooms``), which can differ from the + Universal Credit count: it gives a bedroom to a qualifying young person + a local authority looks after, whom UC Regs 2013 Sch 4 para 9(2)(g) + leaves out, and pools the children of other occupiers' families. This + change leaves both counts as main has them. 4. Reductions. 0 <= each scheme's reduction <= the excess of the welfare benefits over that scheme's cap; the UC reduction is the excess, with Housing Benefit counted as nil (reg. 80(2A)), less the childcare costs @@ -874,13 +875,12 @@ def test_examples_reach_the_cases(): assert v["is_uc_benefit_cap_single_claimant_rate"][lone_parent] assert not v["is_housing_benefit_benefit_cap_single_claimant_rate"][lone_parent] assert not v["reference_is_benefit_cap_single_claimant_rate"][lone_parent] - # HB's size criteria give the young person a bedroom (reg. 13D(3)(b)), - # so the LHA comparisons leave this family out (invariant 3). + # Both schemes' size criteria give the young person a bedroom: HB as a + # person who is not a child (reg. 13D(3)(b)), UC as a non-dependant who + # is not a child (Sch 4 paras 9(2) and 10(1)(c)). assert v["housing_benefit_LHA_category"][lone_parent] == "C" - assert ( - v["LHA_allowed_bedrooms"][lone_parent] - != v["housing_benefit_LHA_allowed_bedrooms"][lone_parent] - ) + assert v["LHA_allowed_bedrooms"][lone_parent] == 2 + assert v["housing_benefit_LHA_allowed_bedrooms"][lone_parent] == 2 # AFIP: excepted from both shared rates, and lifts both caps, which the # old lists did not do. assert v["LHA_category"][afip] == v["housing_benefit_LHA_category"][afip] == "B" From c2c55086e27507567190a664d2e0e84e42d24981 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 14:50:59 +0000 Subject: [PATCH 58/67] State the per-benefit-unit contract for a supplied cap total Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JPH2P5XFEgwSLLAb1 --- changelog.d/2004.fixed.md | 2 +- ...test_non_dependant_deduction_properties.py | 16 +++++++------- policyengine_uk/utils/benefit_cap.py | 2 +- .../gov/dwp/benefit_cap_welfare_benefits.py | 16 +++++++------- .../uc_benefit_cap_welfare_benefits.py | 21 ++++++++++++------- 5 files changed, 33 insertions(+), 24 deletions(-) diff --git a/changelog.d/2004.fixed.md b/changelog.d/2004.fixed.md index 47f0f18c41..2087926885 100644 --- a/changelog.d/2004.fixed.md +++ b/changelog.d/2004.fixed.md @@ -1 +1 @@ -The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). Inputs that supply the combined total, `benefit_cap_welfare_benefits`, also supply `housing_benefit_pre_benefit_cap` or `uc_benefit_cap_welfare_benefits`; otherwise the Universal Credit cap raises `ValueError`. +The model now calculates Housing Benefit and national Council Tax Reduction non-dependant deductions under each scheme's rules. Bands include their lower edge. For a non-exempt non-dependant outside remunerative work, including someone on Income Support, income-based JSA or income-related ESA, the council deducts the lowest amount whatever their income. The council bands a couple on its joint gross income plus the family's awards, counting Universal Credit after the benefit cap and before debt deductions, and normally makes one deduction, the higher. Under Wales's working-age scheme, it deducts both members' amounts when their own Universal Credit award assesses either member. It deducts separately for each other adult. The model counts other investment income, ISA interest and ISA dividends in gross income, and treats a self-employment or property loss as nil. Councils apply the claimant and non-dependant exemptions, and for the Universal Credit exemption the model reads Universal Credit's own calculation of earned income. The model gives the England pensioner, Scottish and Welsh Council Tax Reduction schemes their own scales from 2013-14, and follows each jointly liable applicant's own scheme. For the Universal Credit benefit cap, the model now totals welfare benefits without Housing Benefit (`uc_benefit_cap_welfare_benefits`). A caller who supplies the combined total, `benefit_cap_welfare_benefits`, must also supply `housing_benefit_pre_benefit_cap` or `uc_benefit_cap_welfare_benefits` for every benefit unit; otherwise the model raises `ValueError` when it calculates the Universal Credit cap. diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 754f213b26..0be4a4ea89 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -72,8 +72,8 @@ baseline formula on universal_credit's route calculates Housing Benefit: its traced calculation reads no Housing Benefit variable but the reported award. Inputs that supply benefit_cap_welfare_benefits, a total that - includes Housing Benefit, also supply housing_benefit_pre_benefit_cap, - or the basket raises ValueError. A non-dependant family's gross income + includes Housing Benefit, also supply housing_benefit_pre_benefit_cap or + uc_benefit_cap_welfare_benefits, or the basket raises ValueError. A non-dependant family's gross income counts its award after the cap and before debt deductions: universal_credit plus uc_deductions, which equals the award before the cap less the cap reduction applied, under current law and under an 85% protected minimum @@ -1069,8 +1069,9 @@ def apply(self): @pytest.fixture(scope="module", autouse=True) def release_scaled_award_system(): - # The system also keeps its last simulation alive. Drop it once this - # module's tests are done, instead of for the rest of the test process. + # The system also keeps its last simulation alive. Clear the cache after + # this module's last test, so the system doesn't stay in memory for the + # rest of the test process. yield scaled_award_system.cache_clear() @@ -1148,9 +1149,10 @@ def test_universal_credit_reads_no_calculated_housing_benefit(): Benefit variable but the reported award, so the deductions can read universal_credit. Inputs that supply benefit_cap_welfare_benefits, a total that includes Housing Benefit, also supply - housing_benefit_pre_benefit_cap, and Universal Credit reads that input - without calculating Housing Benefit. A supplied total without it raises - ValueError.""" + housing_benefit_pre_benefit_cap or uc_benefit_cap_welfare_benefits, or + the basket raises ValueError. Where they supply + housing_benefit_pre_benefit_cap, Universal Credit reads that input + without calculating Housing Benefit.""" year = 2025 situation = award_households([BAND_CROSSING], scaled=False) sim = core_simulation(situation, year, trace=True) diff --git a/policyengine_uk/utils/benefit_cap.py b/policyengine_uk/utils/benefit_cap.py index b2a2b8f49a..e91e2fa09c 100644 --- a/policyengine_uk/utils/benefit_cap.py +++ b/policyengine_uk/utils/benefit_cap.py @@ -44,7 +44,7 @@ def benefit_cap_couple(benunit, period): uc_benefit_cap_welfare_benefits without Housing Benefit). Universal Credit alone caps a family receiving both awards (UC Regs 2013 reg 80(2A); HB Regs 2006 reg 75F(1)(g)), so the Universal Credit couple counts. The two - couples are the same unless is_uc_claimant is entered. + couples match unless the inputs set is_uc_claimant. """ person = benunit.members on_universal_credit = benunit("universal_credit_pre_benefit_cap", period) > 0 diff --git a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py index 44ffe44b1e..3533f15608 100644 --- a/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/benefit_cap_welfare_benefits.py @@ -53,7 +53,7 @@ class benefit_cap_welfare_benefits(Variable): "is its claimants (is_uc_assessed_claimant), including the other " "member of a couple where one member claims as a single person; " "otherwise it is the claimant and partner (is_claimant_or_partner). " - "The two are the same unless is_uc_claimant is entered. Benefits " + "The two couples match unless the inputs set is_uc_claimant. Benefits " "another member of the benefit unit claims in their own right do not " "count. The total includes Universal Credit and Housing Benefit " "before the cap (universal_credit_pre_benefit_cap, " @@ -63,12 +63,14 @@ class benefit_cap_welfare_benefits(Variable): "supply this total include Housing Benefit in it. They also supply " "housing_benefit_pre_benefit_cap, which " "uc_benefit_cap_welfare_benefits subtracts, or supply " - "uc_benefit_cap_welfare_benefits itself; otherwise Universal Credit's " - "cap raises ValueError. A reform of this variable changes the Housing " - "Benefit cap. To change the Universal Credit cap's basket, replace " - "uc_benefit_cap_welfare_benefits. Widowed parent's allowance counts; " - "maternity allowance and the other bereavement benefits, also welfare " - "benefits, are not modelled here." + "uc_benefit_cap_welfare_benefits itself, with a value for every " + "benefit unit; otherwise the model raises ValueError when it " + "calculates Universal Credit's cap. A reform of this variable changes " + "the Housing Benefit cap. To change the Universal Credit cap's basket, " + "replace uc_benefit_cap_welfare_benefits. Widowed parent's allowance " + "counts. The model has no variable for maternity allowance or the " + "other bereavement benefits, which Parliament also lists as welfare " + "benefits (Welfare Reform Act 2012 s. 96(10))." ) definition_period = YEAR unit = GBP diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py index 821e08e13d..d373a2387c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/uc_benefit_cap_welfare_benefits.py @@ -26,12 +26,16 @@ def supplied_total_without_housing_benefit(period) -> str: return ( f"The inputs supply benefit_cap_welfare_benefits for {period}, a total " - "that includes Housing Benefit, which the Universal Credit cap counts " - "as nil (UC Regs 2013 reg. 80(2A)). Supply " - "housing_benefit_pre_benefit_cap for the same period, for the cap to " - "subtract, or supply uc_benefit_cap_welfare_benefits. The model doesn't " - "calculate that Housing Benefit for Universal Credit, because the " - "Housing Benefit non-dependant deductions read universal_credit." + "that includes Housing Benefit, which DWP counts as nil for the " + "Universal Credit cap (UC Regs 2013 reg. 80(2A)). Supply " + "housing_benefit_pre_benefit_cap for the same period, for the model to " + "subtract, or supply uc_benefit_cap_welfare_benefits, with a value for " + "every benefit unit: a situation that sets either for some benefit " + "units stores nil for the rest. The model doesn't calculate that " + "Housing Benefit for Universal Credit, because the Housing Benefit " + "non-dependant deductions read universal_credit. A reform that " + "neutralizes housing_benefit_pre_benefit_cap drops a supplied value; " + "supply uc_benefit_cap_welfare_benefits instead." ) @@ -46,8 +50,9 @@ class uc_benefit_cap_welfare_benefits(Variable): "inputs supply benefit_cap_welfare_benefits, whose total includes " "Housing Benefit, this subtracts the supplied " "housing_benefit_pre_benefit_cap. Inputs that supply the combined " - "total also supply housing_benefit_pre_benefit_cap or this variable; " - "otherwise the formula raises ValueError. No baseline formula on " + "total also supply housing_benefit_pre_benefit_cap or this variable, " + "with a value for every benefit unit; otherwise the formula raises " + "ValueError. No baseline formula on " "Universal Credit's route calculates Housing Benefit, so the Housing " "Benefit non-dependant deductions can read universal_credit." ) From 4ec0693f7a01dc4347c249b9946db7a68be1e3a9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 15:02:06 +0000 Subject: [PATCH 59/67] Rewrap the invariant 12 docstring paragraph Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JPH2P5XFEgwSLLAb1 --- ...test_non_dependant_deduction_properties.py | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/test_non_dependant_deduction_properties.py b/policyengine_uk/tests/test_non_dependant_deduction_properties.py index 0be4a4ea89..5a172b2bad 100644 --- a/policyengine_uk/tests/test_non_dependant_deduction_properties.py +++ b/policyengine_uk/tests/test_non_dependant_deduction_properties.py @@ -73,17 +73,18 @@ its traced calculation reads no Housing Benefit variable but the reported award. Inputs that supply benefit_cap_welfare_benefits, a total that includes Housing Benefit, also supply housing_benefit_pre_benefit_cap or - uc_benefit_cap_welfare_benefits, or the basket raises ValueError. A non-dependant family's gross income - counts its award after the cap and before debt deductions: universal_credit - plus uc_deductions, which equals the award before the cap less the cap - reduction applied, under current law and under an 85% protected minimum - floor; the floor never lowers an award. Under a reform that scales - universal_credit, each household's deductions equal those of the same - household with the reformed award supplied, and the Housing Benefit - deduction is the band of wages plus that award, so it moves when the - award crosses a band. (test_non_dependant_cycle_contracts.py checks the - supplied-total contract over mixed populations, and that a reform making - Universal Credit read calculated Housing Benefit raises CycleError.) + uc_benefit_cap_welfare_benefits, or the basket raises ValueError. A + non-dependant family's gross income counts its award after the cap and + before debt deductions: universal_credit plus uc_deductions, which equals + the award before the cap less the cap reduction applied, under current law + and under an 85% protected minimum floor; the floor never lowers an award. + Under a reform that scales universal_credit, each household's deductions + equal those of the same household with the reformed award supplied, and + the Housing Benefit deduction is the band of wages plus that award, so it + moves when the award crosses a band. + (test_non_dependant_cycle_contracts.py checks the supplied-total contract + over mixed populations, and that a reform making Universal Credit read + calculated Housing Benefit raises CycleError.) 13. Differential: the Universal Credit exemption's earned-income test agrees with Universal Credit's own calculation (uc_individual_earned_income) across employment expenses, trading losses, company income attributed to From ece6b0427b25d26640b7b52b735bf74bfda0d2d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 16:03:59 +0000 Subject: [PATCH 60/67] Apply review of the LHA young-person bedroom change Exclude the household head from the property test's in-unit non-dependants, as is_benefit_unit_non_dependant_for_universal_credit does; add an example of a looked-after qualifying young person to the exact-effect property; say in the formula that it gives a member who heads the household a bedroom although para 9(2)(d) stops them being a non-dependant; quote the model's Maidstone rates; drop a stale docstring sentence. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6HNeYLCMijqe3L1fEk --- .../family/LHA_young_person_bedroom.yaml | 7 ++-- ...benefit_cap_lha_scheme_split_properties.py | 3 +- ...est_lha_bedroom_young_people_properties.py | 41 ++++++++++++++----- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 6 ++- 4 files changed, 40 insertions(+), 17 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml index 11bca1d504..3c7a5c1058 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -11,10 +11,9 @@ # Housing Benefit: HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b), "a # person who is not a child", where a child is a person under 16 (reg 2(1)). # -# Rents of £200 a week (£10,400 a year) sit between the published Maidstone -# one-bedroom (B) and two-bedroom (C) rates (VOA, LHA April 2024, held for -# 2025-26: B £172.60, C £208.27 a week), so the bedroom decides whether the -# rent is met in full. +# Rents of £200 a week (£10,400 a year) sit between the model's Maidstone +# one-bedroom (B, £173.08 a week) and two-bedroom (C, £208.85 a week) rates, +# so the bedroom decides whether the rent is met in full. - name: A qualifying young person has their own bedroom period: 2026 diff --git a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py index 157c97cbd0..b171878534 100644 --- a/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_lha_scheme_split_properties.py @@ -41,8 +41,7 @@ ``housing_benefit_LHA_allowed_bedrooms``), which can differ from the Universal Credit count: it gives a bedroom to a qualifying young person a local authority looks after, whom UC Regs 2013 Sch 4 para 9(2)(g) - leaves out, and pools the children of other occupiers' families. This - change leaves both counts as main has them. + leaves out, and pools the children of other occupiers' families. 4. Reductions. 0 <= each scheme's reduction <= the excess of the welfare benefits over that scheme's cap; the UC reduction is the excess, with Housing Benefit counted as nil (reg. 80(2A)), less the childcare costs diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 935bd39e46..7e0fc46782 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -37,11 +37,13 @@ count other families' children (a separate change). 4. In-unit non-dependants: every member of a benefit unit aged 16 or over who is neither the claimant nor partner nor a Universal Credit qualifying - young person is a non-dependant (para 9(2)) with a bedroom (para - 10(1)(c)). So their family has one more Universal Credit bedroom with - them than without them, and making them qualifying young persons the - renter is responsible for (para 10(1)(b)) leaves every family's - Universal Credit bedrooms unchanged. This ties the bedroom to the same + young person nor the household head (the householder in whose name the + accommodation is rented, whom para 9(2)(d) excludes as liable for the + rent) is a non-dependant (para 9(2)) with a bedroom (para 10(1)(c)). So + their family has one more Universal Credit bedroom with them than + without them, and making them qualifying young persons the renter is + responsible for (para 10(1)(b)) leaves every family's Universal Credit + bedrooms unchanged. This ties the bedroom to the same definition that marks a benefit-unit member as a non-dependant for the non-dependant deduction. """ @@ -49,7 +51,7 @@ from itertools import combinations import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -226,6 +228,23 @@ def test_adding_a_person_aged_16_to_19_never_lowers_bedrooms(case, supply_roles) @PROPERTY_SETTINGS @given(scenario()) +# A qualifying young person a local authority looks after, added to the +# head's family: no Universal Credit bedroom (para 9(2)(g)). +@example( + ( + [[dict(role="head", adults=[40], children=[])]], + (0, 0), + dict( + age=17, + is_male=True, + current_education="UPPER_SECONDARY", + age_started_or_accepted_current_education_or_training=16, + is_before_universal_credit_qualifying_young_person_terminal_date=True, + is_looked_after_by_local_authority=True, + receives_benefits_in_own_right=False, + ), + ) +) def test_exact_effect_of_adding_a_person_aged_16_to_19(case): population, target, person = case before, keys = build(population) @@ -293,15 +312,17 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): def in_unit_non_dependants(sim): - """Each person: aged 16 or over, neither the claimant nor partner, and - not a Universal Credit qualifying young person (UC Regs 2013 Sch 4 para - 9(2)).""" + """Each person: aged 16 or over, neither the claimant nor partner, not a + Universal Credit qualifying young person, and not the household head + (UC Regs 2013 Sch 4 para 9(2), with (d) for the head, the householder + in whose name the accommodation is rented).""" age = np.asarray(sim.calculate("age", YEAR)) claimant_or_partner = np.asarray(sim.calculate("is_claimant_or_partner", YEAR)) qualifying = np.asarray( sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) ) - return (age >= 16) & ~claimant_or_partner & ~qualifying + household_head = np.asarray(sim.calculate("is_resolved_household_head", YEAR)) + return (age >= 16) & ~claimant_or_partner & ~qualifying & ~household_head @PROPERTY_SETTINGS diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 37b20d5f9a..3090676b8f 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -80,7 +80,11 @@ def formula(benunit, period, parameters): # responsible (regs 4 and 5). qualifying_young_person = family_member & responsible # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a - # non-dependant. + # non-dependant with a bedroom. The formula does not read + # is_resolved_household_head, so it also gives this bedroom to a + # member who heads the household, although para 9(2)(d) stops a + # person liable to make payments on a commercial basis for their + # occupation being a non-dependant. family_non_dependant = family_member & ~qualifying family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) # Para 10(1)(c): a non-dependant from another family who is not a From 76e4489a907523d968d02668a398caaaf39aa5e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 17:12:56 +0000 Subject: [PATCH 61/67] Give a non-claiming household head no LHA non-dependant bedroom The model treats the resolved household head as the householder liable for the rent, so under UC Regs 2013 Sch 4 paras 1(2), 9(1) and 9(2)(d) a head who does not claim is neither a renter nor a non-dependant and has no para 10(1)(c) bedroom. Add a YAML case for that household, let the property generator make the added person the household head, and add an example of a looked-after qualifying young person in a non-dependant's family. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01N2tyHSGaU6D6pRC1pYhwv3 --- changelog.d/2008.fixed.md | 2 +- .../family/LHA_young_person_bedroom.yaml | 37 +++++++- ...est_lha_bedroom_young_people_properties.py | 85 ++++++++++++++----- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 20 +++-- 4 files changed, 116 insertions(+), 28 deletions(-) diff --git a/changelog.d/2008.fixed.md b/changelog.d/2008.fixed.md index c9219d3f0f..99a3cadd6c 100644 --- a/changelog.d/2008.fixed.md +++ b/changelog.d/2008.fixed.md @@ -1 +1 @@ -Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit LHA size criteria, as a qualifying young person (UC Regs 2013 Sch 4 para 10(1)(b)) or as a non-dependant (para 10(1)(c)). A qualifying young person no one is responsible for, such as one looked after by a local authority, is not a non-dependant and adds no bedroom in any family (para 9(2)(g)). +Give each member of the benefit unit aged 16 or over who is not the claimant, partner or household head a bedroom in the Universal Credit LHA size criteria, as a qualifying young person (UC Regs 2013 Sch 4 para 10(1)(b)) or as a non-dependant (para 10(1)(c)). A qualifying young person no one is responsible for, such as one looked after by a local authority, adds no bedroom in any family (para 9(2)(g)), and nor does a household head who does not claim, whom the model treats as liable for the rent (para 9(2)(d)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml index 3c7a5c1058..6c902a55f5 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -5,7 +5,9 @@ # person for whom the renter or either joint renter is responsible"; anyone # else aged 16 or over living with the renter is a non-dependant (para 9(2)) # with a bedroom under para 10(1)(c), "a non-dependant who is not a child", -# except a qualifying young person no one is responsible for (para 9(2)(g)). +# except a qualifying young person no one is responsible for (para 9(2)(g)) +# and a member who heads the household, whom the model treats as liable for +# the rent (para 9(2)(d)). # A child is a person under 16 (WRA 2012 s.40). # # Housing Benefit: HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b), "a @@ -319,3 +321,36 @@ # rent; their own category would be 1 bedroom: B. housing_benefit_LHA_allowed_bedrooms: [2, 1] housing_benefit_LHA_category: [C, B] + +- name: A member who heads the household but does not claim adds no Universal Credit bedroom + period: 2025 + input: + people: + mother: + age: 45 + is_parent: true + son: + age: 20 + current_education: NOT_IN_EDUCATION + is_household_head: true + benunits: + family: + members: [mother, son] + households: + home: + members: [mother, son] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_resolved_household_head: [false, true] + is_claimant_or_partner: [true, false] + is_qualifying_young_person_for_universal_credit: [false, false] + # The model treats the son, as household head, as the householder in + # whose name the home is rented. He does not claim, so he is not a + # renter (Sch 4 paras 1(2) and 9(1)(a)), not a qualifying young person + # (para 9(1)(b)) and, liable for his occupation, not a non-dependant + # (para 9(2)(d)). Only the renter's bedroom counts (para 10(1)(a)): 1 + # bedroom, category B. + LHA_allowed_bedrooms: 1 + LHA_category: B diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 7e0fc46782..61d4bbe0d3 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -10,7 +10,10 @@ 2. Exact effect: when the added person is not the claimant or partner, their family gains one Housing Benefit bedroom, and one Universal Credit bedroom unless they are a qualifying young person no one is responsible for (para - 9(2)(g)). If they join a non-dependant's family, the claim that counts the + 9(2)(g)) or they head the household and are not a qualifying young person + (the model treats the household head as liable for the rent, so such a + member is neither a renter nor a non-dependant: paras 1(2), 9(1) and + 9(2)(d)). If they join a non-dependant's family, the claim that counts the household's non-dependants (uc_non_dependants_counted) gains one Universal Credit bedroom unless they are a qualifying young person no one is responsible for (para 9(2)(g) applies to the whole extended benefit unit), @@ -21,7 +24,8 @@ 3. Reference: every family's bedrooms equal an independent count of the size criteria. Universal Credit: one for the renter or couple; one for each other member aged 16 or over, except a qualifying young person no one is - responsible for; for the counting claim, one for each person aged 16 or + responsible for and a household head who is not a qualifying young + person; for the counting claim, one for each person aged 16 or over in a non-dependant's family, with the same exception; and the fewest rooms that hold the family's own children. Housing Benefit, for a family liable for the rent: one for the claimant or couple; one for each @@ -106,14 +110,21 @@ def scenario(draw): population = draw(st.lists(household(), min_size=1, max_size=4)) h = draw(st.integers(0, len(population) - 1)) f = draw(st.integers(0, len(population[h]) - 1)) - return population, (h, f), draw(young_person()) + person = draw(young_person()) + # On some draws the person added to the head's family heads the + # household in place of its claimant. + if f == 0: + person["is_household_head"] = draw(st.booleans()) + return population, (h, f), person def build(population, added=None, supply_roles=True): """One situation for the population, optionally with a person added to family ``added[0]`` = (household, family) with attributes ``added[1]``. - Returns the situation and, per benefit unit, its (household, family).""" + Returns the situation and, per benefit unit, its (household, family). + An added person with ``is_household_head`` heads the household in place + of the head family's first adult.""" people, benunits, homes, keys = {}, {}, {}, [] for h, families in enumerate(population): members = [] @@ -136,7 +147,10 @@ def build(population, added=None, supply_roles=True): ids.append(pid) if added is not None and added[0] == (h, f): pid = f"{name}_added" - people[pid] = {**added[1], "is_household_head": False} + people[pid] = { + **added[1], + "is_household_head": added[1].get("is_household_head", False), + } if supply_roles: people[pid]["is_claimant_or_partner"] = False ids.append(pid) @@ -145,7 +159,12 @@ def build(population, added=None, supply_roles=True): extra["liable_for_share_of_household_rent"] = True if fam["role"] in ("boarder", "lodger"): people[ids[0]][f"rent_paid_as_{fam['role']}"] = 5_000 - if fam["role"] == "head": + added_head = ( + added is not None + and added[0] == (h, f) + and added[1].get("is_household_head", False) + ) + if fam["role"] == "head" and not added_head: people[ids[0]]["is_household_head"] = True benunits[name] = {"members": ids, **extra} members.extend(ids) @@ -181,15 +200,21 @@ def claim_roles(sim): return counted.astype(bool), liable -def unclaimed_young_people(sim, situation): - """Each person: a qualifying young person no one is responsible for.""" - qualifying = sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) - responsible = sim.calculate( - "is_child_or_qualifying_young_person_for_universal_credit", YEAR +def has_own_uc_bedroom(sim, situation): + """Each person aged 16 or over who is not the claimant or partner: whether + they have a Universal Credit bedroom of their own. A qualifying young + person the renter is responsible for has one (para 10(1)(b)); one no one + is responsible for has none (para 9(2)(g)). Anyone else has one as a + non-dependant (para 10(1)(c)), unless they head the household, whom the + model treats as liable for the rent (para 9(2)(d)).""" + qualifying = np.asarray( + sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) ) - return dict( - zip(situation["people"], np.asarray(qualifying) & ~np.asarray(responsible)) + responsible = np.asarray( + sim.calculate("is_child_or_qualifying_young_person_for_universal_credit", YEAR) ) + head = np.asarray(sim.calculate("is_resolved_household_head", YEAR)) + return dict(zip(situation["people"], responsible | (~qualifying & ~head))) def fewest_rooms_for_children(children): @@ -245,6 +270,28 @@ def test_adding_a_person_aged_16_to_19_never_lowers_bedrooms(case, supply_roles) ), ) ) +# The same young person in a non-dependant's family: no Universal Credit +# bedroom in the renter's claim either (para 9(2)(g)). +@example( + ( + [ + [ + dict(role="head", adults=[40], children=[]), + dict(role="non_dependant", adults=[45], children=[]), + ] + ], + (0, 1), + dict( + age=17, + is_male=True, + current_education="UPPER_SECONDARY", + age_started_or_accepted_current_education_or_training=16, + is_before_universal_credit_qualifying_young_person_terminal_date=True, + is_looked_after_by_local_authority=True, + receives_benefits_in_own_right=False, + ), + ) +) def test_exact_effect_of_adding_a_person_aged_16_to_19(case): population, target, person = case before, keys = build(population) @@ -257,16 +304,16 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): counted_before, liable_before = claim_roles(sim_before) assert np.array_equal(counted, counted_before) assert np.array_equal(liable, liable_before) - unclaimed = unclaimed_young_people(sim, after)[f"h{target[0]}_f{target[1]}_added"] + gains = has_own_uc_bedroom(sim, after)[f"h{target[0]}_f{target[1]}_added"] # 2. Exact effect. expected_uc = np.zeros(len(keys)) expected_hb = np.zeros(len(keys)) t = keys.index(target) - expected_uc[t] += 0 if unclaimed else 1 + expected_uc[t] += 1 if gains else 0 expected_hb[t] += 1 role = population[target[0]][target[1]]["role"] same_household = np.array([h == target[0] for h, _ in keys]) - if role == "non_dependant" and not unclaimed: + if role == "non_dependant" and gains: expected_uc[same_household & counted] += 1 if role in ("non_dependant", "boarder", "lodger"): expected_hb[same_household & liable] += 1 @@ -281,14 +328,14 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): situation, keys = build(population, (target, person)) sim, uc, hb = bedrooms(situation) counted, liable = claim_roles(sim) - unclaimed = unclaimed_young_people(sim, situation) + gains = has_own_uc_bedroom(sim, situation) occupier_roles = ("non_dependant", "boarder", "lodger") # 3. Reference count. for b, (h, f) in enumerate(keys): families = population[h] fam = families[f] added = (h, f) == target - own_uc = int(added and not unclaimed[f"h{h}_f{f}_added"]) + own_uc = int(added and gains[f"h{h}_f{f}_added"]) own_hb = int(added) outside_uc = outside_hb = 0 hb_children = list(fam["children"]) @@ -300,7 +347,7 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): # UC: each non-dependant who is not a child (adults are # supplied as claimant or partner). outside_uc += len(other["adults"]) - outside_uc += int(joins and not unclaimed[f"h{h}_f{g}_added"]) + outside_uc += int(joins and gains[f"h{h}_f{g}_added"]) if liable[b] and other["role"] in occupier_roles: # HB: the occupier family's couple ((a)) or single adult # ((b)), the added person ((b)) and its children. diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 3090676b8f..0ad2dd976e 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -47,7 +47,9 @@ class LHA_allowed_bedrooms(Variable): "is not the claimant or partner, one for each non-dependant aged 16 or " "over, and the children's bedrooms. A qualifying young person the " "renter is responsible for has their own bedroom; any other member " - "aged 16 or over is a non-dependant and has one too. A qualifying " + "aged 16 or over is a non-dependant and has one too, unless they " + "head the household: the model treats the household head as liable " + "for the rent, so a head who does not claim has no bedroom. A qualifying " "young person no one is responsible for, such as one looked after by " "a local authority, is not a non-dependant and has no bedroom, in " "this family or another. Joint tenants and other sharers of the rent, " @@ -80,12 +82,16 @@ def formula(benunit, period, parameters): # responsible (regs 4 and 5). qualifying_young_person = family_member & responsible # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a - # non-dependant with a bedroom. The formula does not read - # is_resolved_household_head, so it also gives this bedroom to a - # member who heads the household, although para 9(2)(d) stops a - # person liable to make payments on a commercial basis for their - # occupation being a non-dependant. - family_non_dependant = family_member & ~qualifying + # non-dependant with a bedroom, unless they head the household. The + # model treats the household head (is_resolved_household_head) as + # the householder in whose name the accommodation is rented, and so + # as liable to make payments for their occupation. Such a member who + # does not claim is not a renter (paras 1(2) and 9(1)(a)), is not a + # qualifying young person (para 9(1)(b)) and is not a non-dependant + # (para 9(2)(d)), so para 10(1) gives them no bedroom. + family_non_dependant = ( + family_member & ~qualifying & ~person("is_resolved_household_head", period) + ) family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) # Para 10(1)(c): a non-dependant from another family who is not a # child. From e70031659c513ba496d1ee2720021afa70872786 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 19:37:12 +0000 Subject: [PATCH 62/67] Say why a household head keeps or lacks an LHA bedroom The changelog, the formula comment, the documentation and the YAML comments now say that a household head who does not claim is excluded as a non-dependant (para 9(2)(d)) under the formula's reading of the head as liable, and that a head who is a qualifying young person the renter is responsible for keeps the para 10(1)(b) bedroom. A new YAML case pins that bedroom. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6JY6LcDfn841zGhQk2 --- changelog.d/2008.fixed.md | 2 +- .../family/LHA_young_person_bedroom.yaml | 59 ++++++++++++++++--- ...est_lha_bedroom_young_people_properties.py | 14 ++--- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 28 +++++---- 4 files changed, 73 insertions(+), 30 deletions(-) diff --git a/changelog.d/2008.fixed.md b/changelog.d/2008.fixed.md index 99a3cadd6c..4ce966d425 100644 --- a/changelog.d/2008.fixed.md +++ b/changelog.d/2008.fixed.md @@ -1 +1 @@ -Give each member of the benefit unit aged 16 or over who is not the claimant, partner or household head a bedroom in the Universal Credit LHA size criteria, as a qualifying young person (UC Regs 2013 Sch 4 para 10(1)(b)) or as a non-dependant (para 10(1)(c)). A qualifying young person no one is responsible for, such as one looked after by a local authority, adds no bedroom in any family (para 9(2)(g)), and nor does a household head who does not claim, whom the model treats as liable for the rent (para 9(2)(d)). +Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit LHA size criteria, as a qualifying young person the renter is responsible for (UC Regs 2013 Sch 4 para 10(1)(b)) or as a non-dependant (para 10(1)(c)). A qualifying young person no one is responsible for, such as one looked after by a local authority, adds no bedroom in any family (para 9(2)(g)), and nor does a member who heads the household, does not claim and is not a qualifying young person the renter is responsible for, because the formula treats the household head as liable for the rent and so not a non-dependant (para 9(2)(d)). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml index 6c902a55f5..e2da106a6d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -1,17 +1,18 @@ # A member of the benefit unit aged 16 or over who is not the claimant or -# partner has their own bedroom in the size criteria. +# partner has their own bedroom in the size criteria, with the exceptions below. # # Universal Credit: UC Regs 2013 Sch 4 para 10(1)(b), "a qualifying young # person for whom the renter or either joint renter is responsible"; anyone # else aged 16 or over living with the renter is a non-dependant (para 9(2)) # with a bedroom under para 10(1)(c), "a non-dependant who is not a child", # except a qualifying young person no one is responsible for (para 9(2)(g)) -# and a member who heads the household, whom the model treats as liable for -# the rent (para 9(2)(d)). +# and a member who heads the household and is not a qualifying young person +# the renter is responsible for, whom the formula treats as liable for the +# rent (para 9(2)(d)). # A child is a person under 16 (WRA 2012 s.40). # # Housing Benefit: HB Regs 2006 and HB (SPC) Regs 2006 reg 13D(3)(b), "a -# person who is not a child", where a child is a person under 16 (reg 2(1)). +# person who is not a child". # # Rents of £200 a week (£10,400 a year) sit between the model's Maidstone # one-bedroom (B, £173.08 a week) and two-bedroom (C, £208.85 a week) rates, @@ -346,11 +347,51 @@ is_resolved_household_head: [false, true] is_claimant_or_partner: [true, false] is_qualifying_young_person_for_universal_credit: [false, false] - # The model treats the son, as household head, as the householder in + # The formula reads the son, as household head, as the householder in # whose name the home is rented. He does not claim, so he is not a - # renter (Sch 4 paras 1(2) and 9(1)(a)), not a qualifying young person - # (para 9(1)(b)) and, liable for his occupation, not a non-dependant - # (para 9(2)(d)). Only the renter's bedroom counts (para 10(1)(a)): 1 - # bedroom, category B. + # renter (Sch 4 paras 1(2) and 9(1)(a)). Aged 20 and not in education, he + # is not a qualifying young person (para 9(1)(b)). Liable for his + # occupation, he is not a non-dependant (para 9(2)(d)). Only the renter's + # bedroom counts (para 10(1)(a)): 1 bedroom, category B. LHA_allowed_bedrooms: 1 LHA_category: B + +- name: A household head who is a qualifying young person the renter is responsible for has their own bedroom + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 45 + is_male: false + is_parent: true + daughter: + age: 17 + is_male: false + current_education: UPPER_SECONDARY + is_household_head: true + benunits: + family: + members: [mother, daughter] + households: + home: + members: [mother, daughter] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_resolved_household_head: [false, true] + is_claimant_or_partner: [true, false] + is_child_or_qualifying_young_person_for_universal_credit: [false, true] + # The daughter heads the household but is a qualifying young person her + # mother is responsible for, so she is in the extended benefit unit + # (Sch 4 para 9(1)(b)) whoever is liable for the rent. The renter + # (para 10(1)(a)) and the qualifying young person (para 10(1)(b)): + # 2 bedrooms, category C, and the rent is met in full. + LHA_allowed_bedrooms: 2 + LHA_category: C + uc_housing_costs_element: 10_400 + # HB: one bedroom each for the claimant and the daughter, an occupier + # who is not a child (reg 13D(3)(b)). + housing_benefit_LHA_allowed_bedrooms: 2 + housing_benefit_LHA_category: C diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 61d4bbe0d3..9d9875dd4d 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -11,8 +11,8 @@ family gains one Housing Benefit bedroom, and one Universal Credit bedroom unless they are a qualifying young person no one is responsible for (para 9(2)(g)) or they head the household and are not a qualifying young person - (the model treats the household head as liable for the rent, so such a - member is neither a renter nor a non-dependant: paras 1(2), 9(1) and + (the formula treats the household head as liable for the rent, so such + a member is neither a renter nor a non-dependant: paras 1(2), 9(1)(a) and 9(2)(d)). If they join a non-dependant's family, the claim that counts the household's non-dependants (uc_non_dependants_counted) gains one Universal Credit bedroom unless they are a qualifying young person no one is @@ -42,8 +42,8 @@ 4. In-unit non-dependants: every member of a benefit unit aged 16 or over who is neither the claimant nor partner nor a Universal Credit qualifying young person nor the household head (the householder in whose name the - accommodation is rented, whom para 9(2)(d) excludes as liable for the - rent) is a non-dependant (para 9(2)) with a bedroom (para 10(1)(c)). So + accommodation is rented, whom the formula reads as liable for the rent, + so that para 9(2)(d) excludes them) is a non-dependant (para 9(2)) with a bedroom (para 10(1)(c)). So their family has one more Universal Credit bedroom with them than without them, and making them qualifying young persons the renter is responsible for (para 10(1)(b)) leaves every family's Universal Credit @@ -111,8 +111,8 @@ def scenario(draw): h = draw(st.integers(0, len(population) - 1)) f = draw(st.integers(0, len(population[h]) - 1)) person = draw(young_person()) - # On some draws the person added to the head's family heads the - # household in place of its claimant. + # On some draws the person added to the head's family heads the household + # in place of its first adult, who stays the claimant. if f == 0: person["is_household_head"] = draw(st.booleans()) return population, (h, f), person @@ -206,7 +206,7 @@ def has_own_uc_bedroom(sim, situation): person the renter is responsible for has one (para 10(1)(b)); one no one is responsible for has none (para 9(2)(g)). Anyone else has one as a non-dependant (para 10(1)(c)), unless they head the household, whom the - model treats as liable for the rent (para 9(2)(d)).""" + formula treats as liable for the rent (para 9(2)(d)).""" qualifying = np.asarray( sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) ) diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 0ad2dd976e..00fdbd6fe2 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -43,13 +43,14 @@ class LHA_allowed_bedrooms(Variable): label = "The number of bedrooms covered by LHA for the benefit unit" documentation = ( "Universal Credit size criteria: one bedroom for the renter or " - "couple, one for each member of the benefit unit aged 16 or over who " - "is not the claimant or partner, one for each non-dependant aged 16 or " - "over, and the children's bedrooms. A qualifying young person the " - "renter is responsible for has their own bedroom; any other member " - "aged 16 or over is a non-dependant and has one too, unless they " - "head the household: the model treats the household head as liable " - "for the rent, so a head who does not claim has no bedroom. A qualifying " + "couple, one for each qualifying young person the renter is " + "responsible for, one for each other member of the benefit unit aged " + "16 or over who is not the claimant, partner or household head, one " + "for each non-dependant aged 16 or over from another family, and the " + "children's bedrooms. The formula treats the household head as liable " + "for the rent, so a head who does not claim is not a non-dependant " + "(para 9(2)(d)); a head who is a qualifying young person the renter is " + "responsible for keeps their bedroom. A qualifying " "young person no one is responsible for, such as one looked after by " "a local authority, is not a non-dependant and has no bedroom, in " "this family or another. Joint tenants and other sharers of the rent, " @@ -83,12 +84,13 @@ def formula(benunit, period, parameters): qualifying_young_person = family_member & responsible # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a # non-dependant with a bedroom, unless they head the household. The - # model treats the household head (is_resolved_household_head) as - # the householder in whose name the accommodation is rented, and so - # as liable to make payments for their occupation. Such a member who - # does not claim is not a renter (paras 1(2) and 9(1)(a)), is not a - # qualifying young person (para 9(1)(b)) and is not a non-dependant - # (para 9(2)(d)), so para 10(1) gives them no bedroom. + # formula reads the household head (is_resolved_household_head, the + # householder in whose name the accommodation is rented) as liable to + # make payments on a commercial basis for their occupation, so para + # 9(2)(d) stops them being a non-dependant. A head who does not claim + # is not a renter either (paras 1(2) and 9(1)(a)). A head who is a + # qualifying young person the renter is responsible for keeps the + # para 10(1)(b) bedroom above (para 9(1)(b)). family_non_dependant = ( family_member & ~qualifying & ~person("is_resolved_household_head", period) ) From d8c224979f811f083b7e1537961a7e699fe13988 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 17:19:10 -0500 Subject: [PATCH 63/67] Tighten #2010's changelog fragment and test text Cut the changelog fragment to one sentence, as the repository's rule asks, rewrap the one docstring line over 88 characters, and state what the generator comment holds for inferred roles too. Text only; the test file's AST with docstrings blanked matches e70031659. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2008.fixed.md | 2 +- .../test_lha_bedroom_young_people_properties.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/changelog.d/2008.fixed.md b/changelog.d/2008.fixed.md index 4ce966d425..a24873b14d 100644 --- a/changelog.d/2008.fixed.md +++ b/changelog.d/2008.fixed.md @@ -1 +1 @@ -Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit LHA size criteria, as a qualifying young person the renter is responsible for (UC Regs 2013 Sch 4 para 10(1)(b)) or as a non-dependant (para 10(1)(c)). A qualifying young person no one is responsible for, such as one looked after by a local authority, adds no bedroom in any family (para 9(2)(g)), and nor does a member who heads the household, does not claim and is not a qualifying young person the renter is responsible for, because the formula treats the household head as liable for the rent and so not a non-dependant (para 9(2)(d)). +Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit LHA size criteria (UC Regs 2013 Sch 4 para 10(1)(b) and (c)), except a qualifying young person no one is responsible for (para 9(2)(g)) and a household head who does not claim and is not a qualifying young person the renter is responsible for, whom the formula treats as liable for the rent (para 9(2)(d)). diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 9d9875dd4d..133fc95890 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -43,11 +43,11 @@ who is neither the claimant nor partner nor a Universal Credit qualifying young person nor the household head (the householder in whose name the accommodation is rented, whom the formula reads as liable for the rent, - so that para 9(2)(d) excludes them) is a non-dependant (para 9(2)) with a bedroom (para 10(1)(c)). So - their family has one more Universal Credit bedroom with them than - without them, and making them qualifying young persons the renter is - responsible for (para 10(1)(b)) leaves every family's Universal Credit - bedrooms unchanged. This ties the bedroom to the same + so that para 9(2)(d) excludes them) is a non-dependant (para 9(2)) with + a bedroom (para 10(1)(c)). So their family has one more Universal Credit + bedroom with them than without them, and making them qualifying young + persons the renter is responsible for (para 10(1)(b)) leaves every family's + Universal Credit bedrooms unchanged. This ties the bedroom to the same definition that marks a benefit-unit member as a non-dependant for the non-dependant deduction. """ @@ -112,7 +112,7 @@ def scenario(draw): f = draw(st.integers(0, len(population[h]) - 1)) person = draw(young_person()) # On some draws the person added to the head's family heads the household - # in place of its first adult, who stays the claimant. + # in place of its first adult, whose claimant role does not change. if f == 0: person["is_household_head"] = draw(st.booleans()) return population, (h, f), person From fa6ddaeeb9aaef38219372632f84df32dfcae644 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 00:40:32 +0000 Subject: [PATCH 64/67] Give an in-unit non-dependant's bedroom to the claim that bears their contribution LHA_allowed_bedrooms reads is_benefit_unit_non_dependant_for_universal_credit, the definition uc_non_dep_deductions uses, and counts an in-unit non-dependant of a family that makes no claim in the claim that counts the household's non-dependants, as uc_non_dep_deductions does. Adds the adult son's case through final UC and household income, its own-unit control, a non-claiming head's son in a sharer's claim, and properties tying rooms to contributions and to the coding of the benefit unit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KBpj2nEGrPeiuxZmW --- changelog.d/2008.fixed.md | 2 +- .../family/LHA_young_person_bedroom.yaml | 117 ++++++++++ ...est_lha_bedroom_young_people_properties.py | 206 +++++++++++++++++- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 48 ++-- 4 files changed, 347 insertions(+), 26 deletions(-) diff --git a/changelog.d/2008.fixed.md b/changelog.d/2008.fixed.md index a24873b14d..11354b34f2 100644 --- a/changelog.d/2008.fixed.md +++ b/changelog.d/2008.fixed.md @@ -1 +1 @@ -Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit LHA size criteria (UC Regs 2013 Sch 4 para 10(1)(b) and (c)), except a qualifying young person no one is responsible for (para 9(2)(g)) and a household head who does not claim and is not a qualifying young person the renter is responsible for, whom the formula treats as liable for the rent (para 9(2)(d)). +Give each member of the benefit unit aged 16 or over who is not the claimant or partner a bedroom in the Universal Credit LHA size criteria (UC Regs 2013 Sch 4 para 10(1)(b) and (c)), except a qualifying young person no one is responsible for (para 9(2)(g)) and a household head who does not claim and is not a qualifying young person the renter is responsible for, whom the formula treats as liable for the rent (para 9(2)(d)). The bedroom reads the definition that the housing cost contribution uses (is_benefit_unit_non_dependant_for_universal_credit), and the claim that bears an in-unit non-dependant's contribution has their bedroom. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml index e2da106a6d..8d3ae56336 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/LHA_young_person_bedroom.yaml @@ -395,3 +395,120 @@ # who is not a child (reg 13D(3)(b)). housing_benefit_LHA_allowed_bedrooms: 2 housing_benefit_LHA_category: C + +# A non-dependant in the renter's benefit unit has a bedroom (para 10(1)(c)) +# and bears a housing cost contribution (para 13). The claim that bears the +# contribution has the bedroom, and coding the same adult in a benefit unit +# of their own changes no claim. +# +# 2026-27 amounts in the parameter files: UC standard allowance for a single +# claimant aged 25 or over £424.90 a month, £5,098.80 a year; housing cost +# contribution £96.55 a month, £1,158.60 a year, for a non-dependant aged 21 +# or over (paras 14(1), 16(2)(a)); Household Energy Payment £100 (council +# tax bands A to D in England; the default band is D); TV Licence £180. + +- name: An adult son in the renter's benefit unit has a bedroom and bears a housing cost contribution + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 45 + is_household_head: true + son: + age: 22 + current_education: NOT_IN_EDUCATION + benunits: + family: + members: [mother, son] + households: + home: + members: [mother, son] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_benefit_unit_non_dependant_for_universal_credit: [false, true] + # The renter and the son, a non-dependant who is not a child: 2 bedrooms, + # category C (£208.85 a week, £10,860.20 a year, above the rent). + LHA_allowed_bedrooms: 2 + LHA_category: C + # 96.55 x 12 = 1,158.60. + uc_non_dep_deductions: 1_158.60 + # 10,400 - 1,158.60 = 9,241.40. + uc_housing_costs_element: 9_241.40 + # 5,098.80 + 9,241.40 = 14,340.20, with no income to taper. + universal_credit: 14_340.20 + # 14,340.20 + 100 (Household Energy Payment) - 180 (TV Licence). + household_net_income: 14_260.20 + +- name: The same adult son in a benefit unit of his own, not claiming, leaves his mother's claim unchanged + period: 2026 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 45 + is_household_head: true + son: + age: 22 + current_education: NOT_IN_EDUCATION + benunits: + family: + members: [mother] + son_unit: + members: [son] + would_claim_uc: false + households: + home: + members: [mother, son] + rent: 10_400 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_non_dependant_of_household_head: [false, true] + # The renter and the son, now another family's non-dependant: 2 + # bedrooms, category C, as when he is in her benefit unit. + LHA_allowed_bedrooms: [2, 1] + LHA_category: [C, A] + uc_non_dep_deductions: [1_158.60, 0] + uc_housing_costs_element: [9_241.40, 0] + universal_credit: [14_340.20, 0] + household_net_income: 14_260.20 + +- name: A claiming joint tenant has the bedroom of the non-claiming head's adult son whose contribution it bears + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + # Over State Pension age, so not eligible for Universal Credit. + age: 80 + is_household_head: true + is_claimant_or_partner: true + son: + age: 30 + is_claimant_or_partner: false + sharer: + age: 40 + is_claimant_or_partner: true + benunits: + head_family: + members: [head, son] + sharer_family: + members: [sharer] + liable_for_share_of_household_rent: true + households: + home: + members: [head, son, sharer] + rent: 12_000 + tenure_type: RENT_PRIVATELY + brma: MAIDSTONE + output: + is_uc_eligible: [false, true] + uc_non_dependants_counted: [false, true] + # The head's family makes no Universal Credit claim, so the sharer's + # claim counts the son (para 9(2)(f)): the sharer bears his contribution + # (96.55 x 12 = 1,158.60) and has his bedroom (renter 1 + son 1 = 2). + uc_non_dep_deductions: [0, 1_158.60] + LHA_allowed_bedrooms: [2, 2] diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 133fc95890..5b62b03c48 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -20,13 +20,18 @@ and every family liable for the rent gains one Housing Benefit bedroom, since they are an occupier of each such claim (reg 13D(12)). If they join a boarder's or lodger's family, every family liable for the rent gains one - Housing Benefit bedroom. No other family's bedrooms change. + Housing Benefit bedroom. If they are an in-unit non-dependant (invariant + 4) of a family that makes no Universal Credit claim of its own, the claim + that counts the household's non-dependants gains one Universal Credit + bedroom too, as it bears their contribution. No other family's bedrooms + change. 3. Reference: every family's bedrooms equal an independent count of the size criteria. Universal Credit: one for the renter or couple; one for each other member aged 16 or over, except a qualifying young person no one is responsible for and a household head who is not a qualifying young person; for the counting claim, one for each person aged 16 or - over in a non-dependant's family, with the same exception; and the + over in a non-dependant's family, with the same exception, and one for + each in-unit non-dependant of a family that makes no claim; and the fewest rooms that hold the family's own children. Housing Benefit, for a family liable for the rent: one for the claimant or couple; one for each other member aged 16 or over; for each non-dependant's, boarder's or @@ -47,9 +52,20 @@ a bedroom (para 10(1)(c)). So their family has one more Universal Credit bedroom with them than without them, and making them qualifying young persons the renter is responsible for (para 10(1)(b)) leaves every family's - Universal Credit bedrooms unchanged. This ties the bedroom to the same - definition that marks a benefit-unit member as a non-dependant for the - non-dependant deduction. + Universal Credit bedrooms unchanged. The model's + is_benefit_unit_non_dependant_for_universal_credit, which the formula + reads, marks exactly these members. +5. Rooms and contributions: nobody owes a housing cost contribution without + being a non-dependant (para 13). Adding an adult aged 16 to 64, not in + education, to any family: if they are an in-unit non-dependant, they owe + a contribution unless aged under 21 or receiving Attendance Allowance + (para 16(2)); the claim that bears it (their own family's if it makes a + claim, otherwise the one that counts the household's non-dependants) + gains exactly that contribution, unless its renter is exempt (para 15), + and one bedroom; no other family's contributions change. +6. Coding: moving such an adult, not claiming, between a family and a + benefit unit of their own leaves every claiming family's Universal Credit + bedrooms, contributions and award unchanged. """ from itertools import combinations @@ -200,6 +216,17 @@ def claim_roles(sim): return counted.astype(bool), liable +def own_claims(sim): + """Per benefit unit: whether it makes its own Universal Credit claim as a + renter, the test uc_non_dep_deductions applies to an in-unit + non-dependant's family.""" + return ( + np.asarray(sim.calculate("benunit_is_rent_liable", YEAR)) + & np.asarray(sim.calculate("is_uc_eligible", YEAR)) + & np.asarray(sim.calculate("claims_universal_credit", YEAR)) + ) + + def has_own_uc_bedroom(sim, situation): """Each person aged 16 or over who is not the claimant or partner: whether they have a Universal Credit bedroom of their own. A qualifying young @@ -305,6 +332,10 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): assert np.array_equal(counted, counted_before) assert np.array_equal(liable, liable_before) gains = has_own_uc_bedroom(sim, after)[f"h{target[0]}_f{target[1]}_added"] + names = list(after["people"]) + marked = in_unit_non_dependants(sim)[ + names.index(f"h{target[0]}_f{target[1]}_added") + ] # 2. Exact effect. expected_uc = np.zeros(len(keys)) expected_hb = np.zeros(len(keys)) @@ -313,7 +344,8 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): expected_hb[t] += 1 role = population[target[0]][target[1]]["role"] same_household = np.array([h == target[0] for h, _ in keys]) - if role == "non_dependant" and gains: + passed_on = role != "non_dependant" and marked and not own_claims(sim)[t] + if (role == "non_dependant" and gains) or passed_on: expected_uc[same_household & counted] += 1 if role in ("non_dependant", "boarder", "lodger"): expected_hb[same_household & liable] += 1 @@ -329,6 +361,9 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): sim, uc, hb = bedrooms(situation) counted, liable = claim_roles(sim) gains = has_own_uc_bedroom(sim, situation) + own_claim = own_claims(sim) + names = list(situation["people"]) + marked = in_unit_non_dependants(sim) occupier_roles = ("non_dependant", "boarder", "lodger") # 3. Reference count. for b, (h, f) in enumerate(keys): @@ -348,6 +383,10 @@ def test_bedrooms_match_an_independent_count_of_the_size_criteria(case): # supplied as claimant or partner). outside_uc += len(other["adults"]) outside_uc += int(joins and gains[f"h{h}_f{g}_added"]) + elif counted[b] and joins and not own_claim[keys.index((h, g))]: + # UC: an in-unit non-dependant of a family that makes no + # claim of its own counts in this claim (para 9(2)(f)). + outside_uc += int(marked[names.index(f"h{h}_f{g}_added")]) if liable[b] and other["role"] in occupier_roles: # HB: the occupier family's couple ((a)) or single adult # ((b)), the added person ((b)) and its children. @@ -362,14 +401,21 @@ def in_unit_non_dependants(sim): """Each person: aged 16 or over, neither the claimant nor partner, not a Universal Credit qualifying young person, and not the household head (UC Regs 2013 Sch 4 para 9(2), with (d) for the head, the householder - in whose name the accommodation is rented).""" + in whose name the accommodation is rented). Checks that the model's + is_benefit_unit_non_dependant_for_universal_credit marks the same + people.""" age = np.asarray(sim.calculate("age", YEAR)) claimant_or_partner = np.asarray(sim.calculate("is_claimant_or_partner", YEAR)) qualifying = np.asarray( sim.calculate("is_qualifying_young_person_for_universal_credit", YEAR) ) household_head = np.asarray(sim.calculate("is_resolved_household_head", YEAR)) - return (age >= 16) & ~claimant_or_partner & ~qualifying & ~household_head + marked = (age >= 16) & ~claimant_or_partner & ~qualifying & ~household_head + model = np.asarray( + sim.calculate("is_benefit_unit_non_dependant_for_universal_credit", YEAR) + ) + assert np.array_equal(marked, model) + return marked @PROPERTY_SETTINGS @@ -390,7 +436,8 @@ def test_every_in_unit_non_dependant_has_a_bedroom(case): _, uc_without, _ = bedrooms(without) expected = np.zeros(len(keys)) expected[keys.index(target)] = 1 - if population[target[0]][target[1]]["role"] == "non_dependant": + role = population[target[0]][target[1]]["role"] + if role == "non_dependant" or not own_claims(sim)[keys.index(target)]: counted, _ = claim_roles(sim) same_household = np.array([h == target[0] for h, _ in keys]) expected[same_household & counted] += 1 @@ -415,3 +462,144 @@ def test_every_in_unit_non_dependant_has_a_bedroom(case): sim_as_dependants, uc_as_dependants, _ = bedrooms(as_dependants) assert not in_unit_non_dependants(sim_as_dependants).any() assert np.array_equal(uc, uc_as_dependants) + + +@st.composite +def adult_member(draw): + """A member aged 16 or over who is not in education, so never a + qualifying young person, with or without Attendance Allowance (an + exemption under UC Regs 2013 Sch 4 para 16(2)).""" + return dict( + age=draw(st.integers(16, 64)), + is_male=draw(st.booleans()), + current_education="NOT_IN_EDUCATION", + is_looked_after_by_local_authority=False, + attendance_allowance=draw(st.sampled_from([0, 5_000])), + ) + + +@st.composite +def adult_scenario(draw): + population = draw(st.lists(household(), min_size=1, max_size=3)) + h = draw(st.integers(0, len(population) - 1)) + f = draw(st.integers(0, len(population[h]) - 1)) + # Whether each family would claim Universal Credit, so that the claim + # bearing a non-dependant is sometimes not their own family's. + would_claim = [[draw(st.booleans()) for _ in families] for families in population] + return population, (h, f), draw(adult_member()), would_claim + + +def with_claim_flags(situation, would_claim): + for name, unit in situation["benunits"].items(): + if name.startswith("h") and "_f" in name: + h, f = name[1:].split("_f") + unit["would_claim_uc"] = would_claim[int(h)][int(f)] + return situation + + +def in_own_benefit_unit(population, target, person, would_claim): + """The population with ``person`` in a benefit unit of their own, in the + household of family ``target``, not claiming Universal Credit.""" + situation, keys = build(population) + situation = with_claim_flags(situation, would_claim) + h = target[0] + situation["people"]["moved"] = { + **person, + "is_household_head": False, + "is_claimant_or_partner": True, + } + situation["benunits"]["moved"] = {"members": ["moved"], "would_claim_uc": False} + situation["households"][f"h{h}"]["members"].append("moved") + return situation, keys + + +def claim_and_contributions(situation): + sim = Simulation(situation=situation) + + def calc(variable): + return np.asarray(sim.calculate(variable, YEAR)) + + own_claim = ( + calc("benunit_is_rent_liable") + & calc("is_uc_eligible") + & calc("claims_universal_credit") + ) + return sim, calc, own_claim + + +@PROPERTY_SETTINGS +@given(adult_scenario()) +def test_every_in_unit_non_dependant_with_a_room_owes_a_contribution(case): + population, target, person, would_claim = case + before, keys = build(population) + after, _ = build(population, (target, person)) + before = with_claim_flags(before, would_claim) + after = with_claim_flags(after, would_claim) + sim_before, calc_before, own_claim_before = claim_and_contributions(before) + sim, calc, own_claim = claim_and_contributions(after) + counted = calc("uc_non_dependants_counted") + assert np.array_equal(own_claim, own_claim_before) + assert np.array_equal(counted, calc_before("uc_non_dependants_counted")) + # 5. Nobody owes a contribution without being a non-dependant (Sch 4 + # para 9(2) and para 13). + owes = calc("uc_individual_non_dep_deduction") > 0 + non_dependant = calc("is_benefit_unit_non_dependant_for_universal_credit") | calc( + "is_non_dependant_of_household_head" + ) + assert not np.any(owes & ~non_dependant) + names = list(after["people"]) + i = names.index(f"h{target[0]}_f{target[1]}_added") + in_unit = calc("is_benefit_unit_non_dependant_for_universal_credit")[i] + head = calc("is_resolved_household_head")[i] + # The added member is never a qualifying young person or a claimant, so + # the definition reduces to the inputs. + assert in_unit == (person["age"] >= 16 and not head) + if not in_unit: + return + # An in-unit non-dependant owes a contribution unless aged under 21 or + # receiving Attendance Allowance (para 16(2)(a), (2)(c)). + p = sim.tax_benefit_system.parameters( + YEAR + ).gov.dwp.universal_credit.elements.housing.non_dep_deduction + exempt = person["age"] < p.age_threshold or person["attendance_allowance"] > 0 + assert owes[i] == (not exempt) + # The claim that bears them: their own family's if it makes one, + # otherwise the one counting the household's non-dependants. + b = keys.index(target) + role = population[target[0]][target[1]]["role"] + same_household = np.array([h == target[0] for h, _ in keys]) + if own_claim[b] and role != "non_dependant": + bearer = np.arange(len(keys)) == b + else: + bearer = same_household & counted.astype(bool) + renter_exempt = calc("uc_non_dep_deductions_renter_exempt") + expected = bearer * (not exempt) * ~renter_exempt * p.amount * 12 + delta = calc("uc_non_dep_deductions") - calc_before("uc_non_dep_deductions") + assert np.allclose(delta, expected) + # The claim that bears the contribution also has their bedroom. + rooms = calc("LHA_allowed_bedrooms") - calc_before("LHA_allowed_bedrooms") + assert np.all(rooms[bearer] == 1) + + +@PROPERTY_SETTINGS +@given(adult_scenario()) +def test_moving_a_non_claiming_adult_out_of_the_benefit_unit_changes_no_claim(case): + population, target, person, would_claim = case + inside, keys = build(population, (target, person)) + inside = with_claim_flags(inside, would_claim) + outside, _ = in_own_benefit_unit(population, target, person, would_claim) + _, calc_in, own_claim_in = claim_and_contributions(inside) + _, calc_out, own_claim_out = claim_and_contributions(outside) + n = len(keys) + # The household head's family keeps its claim either way. + assert np.array_equal(own_claim_in, own_claim_out[:n]) + # 6. Every family making a claim keeps its bedrooms, contribution and + # award whether the adult is coded in a benefit unit or in their own. + for variable in ( + "LHA_allowed_bedrooms", + "uc_non_dep_deductions", + "universal_credit", + ): + a = calc_in(variable)[own_claim_in] + b = calc_out(variable)[:n][own_claim_in] + assert np.allclose(a, b), variable diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 00fdbd6fe2..096206a38b 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -45,10 +45,14 @@ class LHA_allowed_bedrooms(Variable): "Universal Credit size criteria: one bedroom for the renter or " "couple, one for each qualifying young person the renter is " "responsible for, one for each other member of the benefit unit aged " - "16 or over who is not the claimant, partner or household head, one " - "for each non-dependant aged 16 or over from another family, and the " - "children's bedrooms. The formula treats the household head as liable " - "for the rent, so a head who does not claim is not a non-dependant " + "16 or over who is not the claimant, partner or household head (see " + "is_benefit_unit_non_dependant_for_universal_credit, whose members " + "also bear a housing cost contribution), one for each non-dependant " + "aged 16 or over from another family, one for each such member of a " + "family that makes no Universal Credit claim of its own (in the claim " + "that counts the household's non-dependants, which also bears their " + "contribution), and the children's bedrooms. The formula treats the " + "household head as liable for the rent, so a head who does not claim is not a non-dependant " "(para 9(2)(d)); a head who is a qualifying young person the renter is " "responsible for keeps their bedroom. A qualifying " "young person no one is responsible for, such as one looked after by " @@ -82,28 +86,40 @@ def formula(benunit, period, parameters): # Para 10(1)(b): a qualifying young person for whom the renter is # responsible (regs 4 and 5). qualifying_young_person = family_member & responsible - # Para 10(1)(c) with para 9(2): any other member aged 16 or over is a - # non-dependant with a bedroom, unless they head the household. The - # formula reads the household head (is_resolved_household_head, the - # householder in whose name the accommodation is rented) as liable to - # make payments on a commercial basis for their occupation, so para + # Para 10(1)(c) with para 9(2): any other member who is not a child + # is a non-dependant with a bedroom, unless they head the household. + # is_benefit_unit_non_dependant_for_universal_credit marks them, and + # uc_non_dep_deductions charges a housing cost contribution for the + # same people (para 13), so the room and the contribution follow one + # definition. It reads the household head (is_resolved_household_head, + # the householder in whose name the accommodation is rented) as liable + # to make payments on a commercial basis for their occupation, so para # 9(2)(d) stops them being a non-dependant. A head who does not claim # is not a renter either (paras 1(2) and 9(1)(a)). A head who is a # qualifying young person the renter is responsible for keeps the # para 10(1)(b) bedroom above (para 9(1)(b)). - family_non_dependant = ( - family_member & ~qualifying & ~person("is_resolved_household_head", period) + family_non_dependant = person( + "is_benefit_unit_non_dependant_for_universal_credit", period ) family_rooms = benunit.sum(qualifying_young_person | family_non_dependant) # Para 10(1)(c): a non-dependant from another family who is not a # child. - non_dependant = ( - aged_16_or_over - & ~no_one_responsible - & person("is_non_dependant_of_household_head", period) + other_family = person("is_non_dependant_of_household_head", period) + non_dependant = aged_16_or_over & ~no_one_responsible & other_family + # An in-unit non-dependant whose own family makes no Universal Credit + # claim counts in the claim that counts the household's other + # non-dependants (para 9(2)(f)), as in uc_non_dep_deductions, so that + # claim has their bedroom as well as their housing cost contribution. + own_claim = ( + benunit("benunit_is_rent_liable", period) + & benunit("is_uc_eligible", period) + & benunit("claims_universal_credit", period) ) + passed_on = family_non_dependant & ~other_family & ~benunit.project(own_claim) counted = benunit("uc_non_dependants_counted", period) - non_dependants = counted * benunit.max(person.household.sum(non_dependant)) + non_dependants = counted * benunit.max( + person.household.sum(non_dependant | passed_on) + ) return ( 1 + family_rooms + non_dependants + bedrooms_for_children(benunit, period) ) From b3e0d76b887851d0d04a56c84ba092cc16086e54 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 02:10:42 +0000 Subject: [PATCH 65/67] Charge a head applying alone for her benefit unit's couple in the IIDB CTR case Main's case predates the in-unit non-dependant deduction: the mother and father are the grandmother's non-dependants, one deduction of 5.20 a week. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KBpj2nEGrPeiuxZmW --- .../council_tax_reduction_prescribed_benefits.yaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml index ea63047128..e66ef7b8b4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml @@ -570,10 +570,15 @@ # A single pensioner who reached pensionable age before April 2021: # 256.00 * 52 = 13,312.00. council_tax_reduction_applicable_amount: 13_312 - # 1,800 - 0.2 x (14,980.16 - 13,312.00) = 1,800 - 333.632 = 1,466.368. - # The couple are in her benefit unit, so the model charges no - # non-dependant deduction for them (#2017). - council_tax_benefit: 1_466.368 + # Applying alone, she has the mother and father, a couple outside her + # family, as her non-dependants (SI 2012/2885 reg 9(1), (2)(a)). + # Neither works, so each takes the lowest pensioner amount, £5.20 a + # week, and the couple pays one deduction, the higher (Sch 1 para 8): + # 5.20 x 52 = 270.40. + council_tax_reduction_non_dep_deductions: 270.40 + # 1,800 - 0.2 x (14,980.16 - 13,312.00) - 270.40 + # = 1,800 - 333.632 - 270.40 = 1,195.968. + council_tax_benefit: 1_195.968 - name: A guarantee credit recipient with IIDB has nil Council Tax Reduction income period: 2026 From 7757ab0769e26dbce1d0d0760c5cd543b1500eb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 02:10:59 +0000 Subject: [PATCH 66/67] Count an in-unit non-dependant's bedroom once where their family is the counting claim A family that is not benunit_is_rent_liable (no rent input) can still be the claim uc_non_dependants_counted selects; family_rooms already holds the bedroom there, as uc_non_dep_deductions charges the contribution once. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KBpj2nEGrPeiuxZmW --- .../test_lha_bedroom_young_people_properties.py | 12 ++++++++---- .../variables/gov/dwp/LHA_allowed_bedrooms.py | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 5b62b03c48..62aacfa964 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -344,7 +344,9 @@ def test_exact_effect_of_adding_a_person_aged_16_to_19(case): expected_hb[t] += 1 role = population[target[0]][target[1]]["role"] same_household = np.array([h == target[0] for h, _ in keys]) - passed_on = role != "non_dependant" and marked and not own_claims(sim)[t] + passed_on = ( + role != "non_dependant" and marked and not own_claims(sim)[t] and not counted[t] + ) if (role == "non_dependant" and gains) or passed_on: expected_uc[same_household & counted] += 1 if role in ("non_dependant", "boarder", "lodger"): @@ -428,7 +430,8 @@ def test_every_in_unit_non_dependant_has_a_bedroom(case): names = list(situation["people"]) # 4. A marked member has a bedroom of their own: without them, their # family has one fewer, and so does the claim counting the household's - # non-dependants if they are in a non-dependant's family. Only the added + # non-dependants if they are in a non-dependant's family, or in a family + # that neither makes its own claim nor is that claim. Only the added # person can be marked: the generated adults are claimants or partners # and the generated children are under 16. if marked[names.index(f"h{target[0]}_f{target[1]}_added")]: @@ -437,8 +440,9 @@ def test_every_in_unit_non_dependant_has_a_bedroom(case): expected = np.zeros(len(keys)) expected[keys.index(target)] = 1 role = population[target[0]][target[1]]["role"] - if role == "non_dependant" or not own_claims(sim)[keys.index(target)]: - counted, _ = claim_roles(sim) + counted, _ = claim_roles(sim) + t = keys.index(target) + if role == "non_dependant" or not (own_claims(sim)[t] or counted[t]): same_household = np.array([h == target[0] for h, _ in keys]) expected[same_household & counted] += 1 assert np.array_equal(uc - uc_without, expected) diff --git a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py index 096206a38b..a2ae0337f7 100644 --- a/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py +++ b/policyengine_uk/variables/gov/dwp/LHA_allowed_bedrooms.py @@ -110,13 +110,17 @@ def formula(benunit, period, parameters): # claim counts in the claim that counts the household's other # non-dependants (para 9(2)(f)), as in uc_non_dep_deductions, so that # claim has their bedroom as well as their housing cost contribution. + # Where that claim is their own family's, family_rooms already holds + # the bedroom. own_claim = ( benunit("benunit_is_rent_liable", period) & benunit("is_uc_eligible", period) & benunit("claims_universal_credit", period) ) - passed_on = family_non_dependant & ~other_family & ~benunit.project(own_claim) counted = benunit("uc_non_dependants_counted", period) + passed_on = ( + family_non_dependant & ~other_family & ~benunit.project(own_claim | counted) + ) non_dependants = counted * benunit.max( person.household.sum(non_dependant | passed_on) ) From 2d362d2fce53d0db8995790561d0fd7c470ea4e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 03:09:36 +0000 Subject: [PATCH 67/67] Expect the counting claim to lose a passed-on bedroom when the member becomes a qualifying young person Another family's qualifying young person is not the counting renter's non-dependant (para 9(2)(g)), so property 4's conversion check allows that one change. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LwQq6KBpj2nEGrPeiuxZmW --- ...est_lha_bedroom_young_people_properties.py | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py index 62aacfa964..e622ff5a98 100644 --- a/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py +++ b/policyengine_uk/tests/test_lha_bedroom_young_people_properties.py @@ -52,7 +52,9 @@ a bedroom (para 10(1)(c)). So their family has one more Universal Credit bedroom with them than without them, and making them qualifying young persons the renter is responsible for (para 10(1)(b)) leaves every family's - Universal Credit bedrooms unchanged. The model's + Universal Credit bedrooms unchanged, except that the claim counting the + household's non-dependants loses the bedroom of one whose own family + makes no claim and is not that claim (para 9(2)(g)). The model's is_benefit_unit_non_dependant_for_universal_credit, which the formula reads, marks exactly these members. 5. Rooms and contributions: nobody owes a housing cost contribution without @@ -447,7 +449,19 @@ def test_every_in_unit_non_dependant_has_a_bedroom(case): expected[same_household & counted] += 1 assert np.array_equal(uc - uc_without, expected) # Making every marked member a qualifying young person the renter is - # responsible for leaves every family's bedrooms unchanged. + # responsible for leaves every family's bedrooms unchanged, except that + # the claim counting the household's non-dependants loses the bedroom of + # one whose own family makes no claim and is not that claim: another + # family's qualifying young person is not that renter's non-dependant + # (para 9(2)(g)). + expected_change = np.zeros(len(keys)) + if marked[names.index(f"h{target[0]}_f{target[1]}_added")]: + counted, _ = claim_roles(sim) + t = keys.index(target) + role = population[target[0]][target[1]]["role"] + if role != "non_dependant" and not (own_claims(sim)[t] or counted[t]): + same_household = np.array([h == target[0] for h, _ in keys]) + expected_change[same_household & counted] = -1 as_dependants = { **situation, "people": { @@ -465,7 +479,7 @@ def test_every_in_unit_non_dependant_has_a_bedroom(case): } sim_as_dependants, uc_as_dependants, _ = bedrooms(as_dependants) assert not in_unit_non_dependants(sim_as_dependants).any() - assert np.array_equal(uc, uc_as_dependants) + assert np.array_equal(uc_as_dependants - uc, expected_change) @st.composite