From 6af1b3cde8ddfe9cc865a3f46c57c6022b4df716 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 03:01:50 -0400 Subject: [PATCH 1/2] Let pension-age families make new Housing Benefit claims housing_benefit_eligible required a reported Housing Benefit claim and would_claim_uc False for every family. would_claim_uc defaults to True in household calculations, so pension-age renters never received Housing Benefit there. In the Enhanced FRS, pension-age families who report Housing Benefit lost it whenever the Universal Credit take-up draw came out True. New Housing Benefit claims are barred except where the claimant and any partner have reached the qualifying age for State Pension Credit (SI 2014/1230 reg 6A(4); NI SR 2016/226 reg 4A(4)). Universal Credit is not available to them (WRA 2012 s.4(1)(b)). A family in which every adult is over State Pension age is now eligible on that route, without a reported claim and whatever would_claim_uc says. Other families keep the continuing-award rule: a reported claim and no Universal Credit claim. Also corrects the Housing Benefit docs page and the take-up parameter description, which said only existing claimants are eligible. Co-Authored-By: Claude Opus 5.5 --- .../hb-pension-age-new-claims.fixed.md | 1 + .../programs/gov/dwp/housing-benefit.ipynb | 14 +- .../gov/dwp/housing_benefit/takeup.yaml | 6 +- .../housing_benefit_pension_age_claims.yaml | 274 ++++++++++++++++++ .../family/uc_legacy_mutual_exclusion.yaml | 10 +- ..._housing_benefit_pension_age_properties.py | 233 +++++++++++++++ .../housing_benefit_eligible.py | 39 ++- 7 files changed, 561 insertions(+), 16 deletions(-) create mode 100644 changelog.d/hb-pension-age-new-claims.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml create mode 100644 policyengine_uk/tests/test_housing_benefit_pension_age_properties.py diff --git a/changelog.d/hb-pension-age-new-claims.fixed.md b/changelog.d/hb-pension-age-new-claims.fixed.md new file mode 100644 index 0000000000..5fd5d1dc81 --- /dev/null +++ b/changelog.d/hb-pension-age-new-claims.fixed.md @@ -0,0 +1 @@ +Let families in which every adult is over State Pension age make new Housing Benefit claims, as SI 2014/1230 reg 6A(4) allows. They no longer need a reported claim, and `would_claim_uc` no longer blocks them because Universal Credit is not available to them. Household calculations previously paid pension-age renters no Housing Benefit, and pension-age families in the Enhanced FRS who report Housing Benefit lost it whenever they drew `would_claim_uc`. Working-age and mixed-age families keep the existing continuing-award rule. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 05d2cd7026..8702664253 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -19,14 +19,14 @@ "source": [ "## Eligibility\n", "\n", - "`housing_benefit_eligible` requires all of:\n", + "`housing_benefit_eligible` requires the benunit to be either in **social housing** or eligible for the **Local Housing Allowance** rate (broadly, private renters), with assessable capital at or below the working-age (or pension-age) capital limit, and to reach Housing Benefit by one of two routes:\n", "\n", - "- the benunit was **already claiming** Housing Benefit in the previous period (proxied by a positive `housing_benefit_reported`) — this gates out new working-age claimants per the UC migration policy,\n", - "- the benunit is either in **social housing** or eligible for the **Local Housing Allowance** rate (broadly, private renters),\n", - "- the benunit is **not claiming Universal Credit**, and\n", - "- assessable capital is at or below the working-age (or pension-age) capital limit.\n", + "- **Pension age**: every adult in the benunit has reached the qualifying age for State Pension Credit (State Pension age). New Housing Benefit claims are open to them ([SI 2014/1230 reg. 6A(4)](https://www.legislation.gov.uk/uksi/2014/1230/regulation/6A)), and Universal Credit is not, so neither a previous claim nor `would_claim_uc` matters.\n", + "- **Continuing award**: any other benunit was **already claiming** Housing Benefit (proxied by a positive `housing_benefit_reported`) and is **not claiming Universal Credit**. Working-age and mixed-age families cannot make new claims; they claim the Universal Credit housing element instead. For mixed-age couples the reported award stands in for the saving for couples who were entitled to Housing Benefit or Pension Credit on 14 May 2019 ([SI 2019/37 art. 4](https://www.legislation.gov.uk/uksi/2019/37/article/4)).\n", "\n", - "The take-up step is handled separately by the input variable `would_claim_housing_benefit`, populated stochastically when the dataset is built so that aggregates match published DWP caseloads." + "The pension-age test counts the same benunit members as Universal Credit's working-age test, so no benunit is eligible for both. Claims for specified or temporary accommodation, which remain open at any age, are not modelled.\n", + "\n", + "Take-up is handled by `would_claim_housing_benefit`, which is true for benunits that report Housing Benefit and, when no reported benefits are supplied (as in household calculations), for everyone. In the Enhanced FRS take-up therefore follows reported claims." ] }, { @@ -43,7 +43,7 @@ "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household.\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` so the post-cap variable matches reported caseload after take-up." + "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/takeup.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml index 2b9cdd6dba..26c847e978 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/takeup.yaml @@ -1,5 +1,7 @@ -description: Share of eligible Housing Benefit recipients that participate. By definition, - this is 100%, because only current claimants are eligible (no new claims). +description: Share of eligible Housing Benefit recipients that participate. Not used by + the model, which takes Housing Benefit take-up from reported claims in survey data and + assumes full take-up in household calculations. Pension-age families can make new + claims; working-age families only continue existing awards. metadata: economy: false label: Housing benefit take-up rate diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml new file mode 100644 index 0000000000..394d7ba16e --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_pension_age_claims.yaml @@ -0,0 +1,274 @@ +# Who may claim Housing Benefit: SI 2014/1230 reg 6A (NI: SR 2016/226 reg 4A). +# Pension-age units (claimant and any partner at State Pension Credit +# qualifying age) may make new claims (reg 6A(4)); others keep only a +# continuing award (reported HB and not claiming UC) in this model. + +- name: Single pensioner council tenant makes a new Housing Benefit claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + output: + guarantee_credit: 2_376 + housing_benefit_applicable_income: 0 + housing_benefit_eligible: true + housing_benefit: 6_240 + universal_credit: 0 + +- name: Pensioner couple renting privately is capped at the Local Housing Allowance + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + state_pension_reported: 2_500 + younger: + age: 68 + state_pension_reported: 2_500 + benunits: + benunit: + members: [older, younger] + BRMA_LHA_rate: 8_000 + households: + household: + members: [older, younger] + rent: 9_000 + tenure_type: RENT_PRIVATELY + output: + housing_benefit_eligible: true + housing_benefit_applicable_income: 0 + housing_benefit: 8_000 + universal_credit: 0 + +- name: New pension-age claim is tapered at 65% above the applicable amount + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 80 + benunits: + benunit: + members: [person] + BRMA_LHA_rate: 12_000 + housing_benefit_applicable_income: 20_000 + households: + household: + members: [person] + rent: 9_000 + tenure_type: RENT_PRIVATELY + output: + housing_benefit_eligible: true + housing_benefit_applicable_amount: 13_312 + housing_benefit: 4_652.80 + +- name: Pensioner with capital over 16,000 and no Guarantee Credit is not eligible + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 15_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + guarantee_credit: 0 + housing_benefit_assessable_capital: 20_000 + housing_benefit_eligible: false + housing_benefit: 0 + +- name: Pensioner on Guarantee Credit has capital disregarded + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + guarantee_credit: 1_336 + housing_benefit_assessable_capital: 0 + housing_benefit_eligible: true + housing_benefit: 6_240 + +- name: Would-claim-UC flag does not block a pension-age claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person] + would_claim_uc: true + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + output: + housing_benefit_eligible: true + housing_benefit: 6_240 + universal_credit: 0 + +- name: Survey-style pensioner reporting HB with would_claim_uc true is paid + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 10_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [person] + would_claim_uc: true + claims_all_entitled_benefits: false + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + output: + would_claim_housing_benefit: true + housing_benefit_eligible: true + housing_benefit: 6_240 + +- name: Mixed-age couple without a continuing award cannot claim HB + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + state_pension_reported: 10_000 + younger: + age: 60 + benunits: + benunit: + members: [older, younger] + households: + household: + members: [older, younger] + rent: 6_240 + tenure_type: RENT_PRIVATELY + output: + housing_benefit_eligible: false + housing_benefit: 0 + is_uc_eligible: true + +- name: Mixed-age couple with a continuing award keeps HB and gets no UC + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + state_pension_reported: 10_000 + housing_benefit_reported: 1 + younger: + age: 60 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + households: + household: + members: [older, younger] + rent: 6_240 + tenure_type: RENT_PRIVATELY + output: + housing_benefit_eligible: true + universal_credit: 0 + +- name: Working-age council tenant cannot make a new HB claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 40 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + output: + housing_benefit_eligible: false + housing_benefit: 0 + +- name: Pension-age owner-occupier gets no HB + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + rent: 6_240 + tenure_type: OWNED_OUTRIGHT + output: + housing_benefit_eligible: false + housing_benefit: 0 + +- name: Northern Ireland pensioner council tenant makes a new claim + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person] + households: + household: + members: [person] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + region: NORTHERN_IRELAND + output: + housing_benefit_eligible: true + housing_benefit: 6_240 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/uc_legacy_mutual_exclusion.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/uc_legacy_mutual_exclusion.yaml index 4415e83b27..aa7b80676f 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/uc_legacy_mutual_exclusion.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/uc_legacy_mutual_exclusion.yaml @@ -1,8 +1,12 @@ # Regression tests for issue #201: UC and legacy benefit `claims_*` consistency. # -# `housing_benefit_eligible` explicitly includes `& ~claiming_uc` so a benunit -# claiming Universal Credit cannot also be eligible for Housing Benefit. These -# tests pin that contract so a future refactor can't quietly break it. +# A working-age benunit keeps Housing Benefit only while it continues an +# existing award and does not claim Universal Credit, so a benunit claiming +# Universal Credit cannot also be eligible for Housing Benefit. Wholly +# pension-age benunits can make new Housing Benefit claims whatever +# `would_claim_uc` says, because Universal Credit is not available to them +# (see housing_benefit_pension_age_claims.yaml). These tests pin the +# working-age contract so a future refactor can't quietly break it. - name: HB eligibility excludes benunits claiming Universal Credit period: 2025 diff --git a/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py b/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py new file mode 100644 index 0000000000..37672b3c3d --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_pension_age_properties.py @@ -0,0 +1,233 @@ +"""Property-based tests for who can claim Housing Benefit. + +New Housing Benefit claims are barred except where the claimant, and any +partner, has reached the qualifying age for State Pension Credit (SI 2014/1230 +reg 6A(4)); Universal Credit is not available to them (Welfare Reform Act 2012 +s.4(1)(b)). Other families keep Housing Benefit only while they continue an +existing award and do not claim Universal Credit. + +Invariants, for any generated population of families: + +1. Structural: 0 <= housing_benefit <= benunit_rent; Housing Benefit never + exceeds the LHA cap for LHA tenants; it is never paid to non-renters or + above the capital limit; and no family gets both Housing Benefit and + Universal Credit. +2. Calculator mode (no reported benefits): a family is eligible exactly when + every adult is over State Pension age, it rents, and its capital is within + the limit; eligible families are paid their full entitlement (pensioners + are exempt from the benefit cap). +3. Dataset mode (claims_all_entitled_benefits False, as in the FRS): Housing + Benefit is paid only to reported claimants, and for families with a + working-age adult eligibility equals the continuing-award rule (reported, + not claiming Universal Credit, renting, capital within the limit). +4. Metamorphic: flipping would_claim_uc never changes a wholly pension-age + family's Housing Benefit. +5. Metamorphic: a wholly pension-age family's Housing Benefit is + non-increasing in private pension income. + +A pensioner with an 18 or 19 year old dependant counts as having a working-age +adult for Housing Benefit, Pension Credit and Universal Credit alike, so the +mutual exclusion holds for that shape too. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +TENURES = [ + "RENT_FROM_COUNCIL", + "RENT_FROM_HA", + "RENT_PRIVATELY", + "OWNED_OUTRIGHT", + "OWNED_WITH_MORTGAGE", +] +# State Pension age is 66 until 2026-27 finishes phasing up; 67 and over is +# unambiguously pension age and 60 and under unambiguously working age. +PENSION_AGE = st.integers(67, 100) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single_pension": [("claimant", PENSION_AGE)], + "couple_pension": [("claimant", PENSION_AGE), ("partner", PENSION_AGE)], + "mixed_age": [("claimant", PENSION_AGE), ("partner", WORKING_AGE)], + "single_working": [("claimant", WORKING_AGE)], + "couple_working": [("claimant", WORKING_AGE), ("partner", WORKING_AGE)], + "pension_with_dependant": [ + ("claimant", PENSION_AGE), + ("dependant", st.integers(18, 19)), + ], +} +money = st.floats(0, 20_000, allow_nan=False, allow_infinity=False) + + +@st.composite +def families(draw, reported_allowed): + shape = draw(st.sampled_from(sorted(SHAPES))) + return dict( + shape=shape, + members=[(role, draw(age)) for role, age in SHAPES[shape]], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(money), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + state_pension=draw(money), + private_pension=draw(st.one_of(st.just(0.0), st.floats(0, 30_000))), + would_claim_uc=draw(st.booleans()), + hb_reported=draw(st.sampled_from([0.0, 1.0])) if reported_allowed else 0.0, + ) + + +def situation(units, claims_all=None, flip_would_claim_uc=False, pension_bump=0.0): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, (role, age) in enumerate(unit["members"]): + name = f"p{i}_{j}" + person = {"age": {YEAR: age}} + if role != "dependant" and age >= 67: + person["state_pension_reported"] = {YEAR: unit["state_pension"]} + person["private_pension_income"] = { + YEAR: unit["private_pension"] + pension_bump + } + if role == "claimant": + person["is_parent"] = {YEAR: unit["shape"] == "pension_with_dependant"} + if unit["hb_reported"]: + person["housing_benefit_reported"] = {YEAR: unit["hb_reported"]} + if role == "dependant": + person["current_education"] = {YEAR: "UPPER_SECONDARY"} + people[name] = person + names.append(name) + benunit = { + "members": names, + "would_claim_uc": {YEAR: unit["would_claim_uc"] ^ flip_would_claim_uc}, + } + # claims_all_entitled_benefits sums reported benefits across the whole + # simulation, so set it per family whenever any family reports one. + if claims_all is not None: + benunit["claims_all_entitled_benefits"] = {YEAR: claims_all} + benunits[f"b{i}"] = benunit + households[f"h{i}"] = { + "members": names, + "rent": {YEAR: unit["rent"]}, + "tenure_type": {YEAR: unit["tenure"]}, + "savings": {YEAR: unit["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +VARIABLES = [ + "housing_benefit", + "housing_benefit_eligible", + "housing_benefit_entitlement", + "housing_benefit_assessable_capital", + "universal_credit", + "benunit_rent", + "LHA_eligible", + "LHA_cap", +] + + +def calculate(units, **kwargs): + sim = Simulation(situation=situation(units, **kwargs)) + values = {v: np.asarray(sim.calculate(v, YEAR)) for v in VARIABLES} + values["social"] = ( + np.asarray(sim.calculate("in_social_housing", YEAR, map_to="benunit")) > 0 + ) + values["renting"] = values["social"] | values["LHA_eligible"].astype(bool) + capital = sim.tax_benefit_system.parameters( + YEAR + ).gov.dwp.housing_benefit.means_test.capital + # Both limits are 16,000; the test holds families to the lower of them. + limit = min(capital.pension_age.limit, capital.working_age.limit) + values["capital_ok"] = values["housing_benefit_assessable_capital"] <= limit + return values + + +def wholly_pension_age(unit): + return all(role != "dependant" and age >= 67 for role, age in unit["members"]) + + +def check_structural(values): + hb = values["housing_benefit"] + lha = values["LHA_eligible"].astype(bool) + assert np.all(hb >= 0) + assert np.all(hb <= values["benunit_rent"] + 0.01) + assert np.all(hb[lha] <= values["LHA_cap"][lha] + 0.01) + assert not np.any((hb > 0) & (values["universal_credit"] > 0)) + assert not np.any((hb > 0) & ~values["renting"]) + assert not np.any((hb > 0) & ~values["capital_ok"]) + + +@PROPERTY_SETTINGS +@given(st.lists(families(reported_allowed=False), min_size=1, max_size=30)) +def test_new_claims_follow_the_pension_age_route(units): + values = calculate(units) + check_structural(values) + for i, unit in enumerate(units): + expected = ( + wholly_pension_age(unit) + and values["renting"][i] + and values["capital_ok"][i] + ) + assert bool(values["housing_benefit_eligible"][i]) == expected, unit + if expected: + assert ( + abs( + values["housing_benefit"][i] + - values["housing_benefit_entitlement"][i] + ) + < 0.01 + ), unit + + +@PROPERTY_SETTINGS +@given(st.lists(families(reported_allowed=True), min_size=1, max_size=30)) +def test_dataset_take_up_stays_anchored_to_reported_claims(units): + values = calculate(units, claims_all=False) + check_structural(values) + for i, unit in enumerate(units): + if values["housing_benefit"][i] > 0: + assert unit["hb_reported"] > 0, unit + if not wholly_pension_age(unit): + continuing_award = ( + unit["hb_reported"] > 0 + and not unit["would_claim_uc"] + and values["renting"][i] + and values["capital_ok"][i] + ) + assert bool(values["housing_benefit_eligible"][i]) == continuing_award + + +@PROPERTY_SETTINGS +@given(st.lists(families(reported_allowed=True), min_size=1, max_size=30)) +def test_would_claim_uc_does_not_affect_pension_age_housing_benefit(units): + before = calculate(units, claims_all=True) + after = calculate(units, claims_all=True, flip_would_claim_uc=True) + for i, unit in enumerate(units): + if wholly_pension_age(unit): + assert ( + abs(before["housing_benefit"][i] - after["housing_benefit"][i]) < 0.01 + ), unit + + +@PROPERTY_SETTINGS +@given( + st.lists(families(reported_allowed=False), min_size=1, max_size=30), + st.floats(1, 10_000, allow_nan=False, allow_infinity=False), +) +def test_pension_age_housing_benefit_is_non_increasing_in_income(units, bump): + before = calculate(units) + after = calculate(units, pension_bump=bump) + for i, unit in enumerate(units): + if wholly_pension_age(unit): + assert after["housing_benefit"][i] <= before["housing_benefit"][i] + 0.01, ( + unit, + bump, + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py index cfc92c7069..08e4d40ce6 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py @@ -5,14 +5,46 @@ class housing_benefit_eligible(Variable): value_type = bool entity = BenUnit label = "eligible for the Housing Benefit" + documentation = ( + "Whether this family can receive Housing Benefit. A family in which " + "every adult has reached the qualifying age for State Pension Credit " + "can make a new claim; Universal Credit is not available to it. Any " + "other family keeps Housing Benefit only while it continues an " + "existing award (reported Housing Benefit) and does not claim " + "Universal Credit. Claims for specified or temporary accommodation " + "are not modelled." + ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/6A", + "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", + "https://www.legislation.gov.uk/uksi/2019/37/article/4", + "https://www.legislation.gov.uk/ukpga/2012/5/section/4", + "https://www.legislation.gov.uk/nisr/2016/226/regulation/4A", + ) def formula(benunit, period, parameters): - social = benunit.any(benunit.members("in_social_housing", period)) + person = benunit.members + sp_age = person("is_SP_age", period) + # New claims are barred except where the claimant, and any partner, + # has reached the qualifying age for State Pension Credit + # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)). This counts + # the same members as is_uc_eligible and is_pension_credit_eligible, + # so no family qualifies for both Housing Benefit and Universal + # Credit; change the three together. + adult = person("is_adult", period) + adult_count = benunit.sum(adult) + pension_age = (adult_count > 0) & (benunit.sum(adult & sp_age) == adult_count) + # Working-age and mixed-age families (since 15 May 2019) claim + # Universal Credit instead. They keep an existing award until they + # claim it (reg 8(2A)); for mixed-age couples the reported award + # stands in for the SI 2019/37 art. 4 saving (reg 6A(5)). already_claiming = add(benunit, period, ["housing_benefit_reported"]) > 0 claiming_uc = benunit("would_claim_uc", period) + continuing_award = already_claiming & ~claiming_uc + social = benunit.any(person("in_social_housing", period)) lha_eligible = benunit("LHA_eligible", period) - any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) + any_over_SP_age = benunit.any(sp_age) capital = benunit("housing_benefit_assessable_capital", period) hb_capital = parameters(period).gov.dwp.housing_benefit.means_test.capital limit = where( @@ -21,8 +53,7 @@ def formula(benunit, period, parameters): hb_capital.working_age.limit, ) return ( - already_claiming + (pension_age | continuing_award) & (social | lha_eligible) - & ~claiming_uc & (capital <= limit) ) From 59560c3d400429b1f812b9b73f9a4635ff52795f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 03:08:19 -0400 Subject: [PATCH 2/2] Say no family receives both HB and UC, and note the working-age abolition Review follow-up: a protected mixed-age couple can be eligible for both, so the invariant is about receipt. Also note that every adult stands in for the claimant and partner, and that the 1 July 2026 end of working-age awards (SI 2025/1148 art. 7) is not modelled. Co-Authored-By: Claude Opus 5.5 --- .../book/programs/gov/dwp/housing-benefit.ipynb | 2 +- .../housing_benefit/housing_benefit_eligible.py | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 8702664253..4f928b173d 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -24,7 +24,7 @@ "- **Pension age**: every adult in the benunit has reached the qualifying age for State Pension Credit (State Pension age). New Housing Benefit claims are open to them ([SI 2014/1230 reg. 6A(4)](https://www.legislation.gov.uk/uksi/2014/1230/regulation/6A)), and Universal Credit is not, so neither a previous claim nor `would_claim_uc` matters.\n", "- **Continuing award**: any other benunit was **already claiming** Housing Benefit (proxied by a positive `housing_benefit_reported`) and is **not claiming Universal Credit**. Working-age and mixed-age families cannot make new claims; they claim the Universal Credit housing element instead. For mixed-age couples the reported award stands in for the saving for couples who were entitled to Housing Benefit or Pension Credit on 14 May 2019 ([SI 2019/37 art. 4](https://www.legislation.gov.uk/uksi/2019/37/article/4)).\n", "\n", - "The pension-age test counts the same benunit members as Universal Credit's working-age test, so no benunit is eligible for both. Claims for specified or temporary accommodation, which remain open at any age, are not modelled.\n", + "The pension-age test counts the same benunit members as Universal Credit's working-age test, and the continuing-award route requires not claiming Universal Credit, so no benunit receives both. Working-age awards outside specified or temporary accommodation ended on 1 July 2026 in Great Britain ([SI 2025/1148 art. 7](https://www.legislation.gov.uk/uksi/2025/1148/article/7)); PolicyEngine does not yet model that. Claims for specified or temporary accommodation, which remain open at any age, are not modelled.\n", "\n", "Take-up is handled by `would_claim_housing_benefit`, which is true for benunits that report Housing Benefit and, when no reported benefits are supplied (as in household calculations), for everyone. In the Enhanced FRS take-up therefore follows reported claims." ] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py index 08e4d40ce6..9cab415763 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_eligible.py @@ -28,17 +28,22 @@ def formula(benunit, period, parameters): sp_age = person("is_SP_age", period) # New claims are barred except where the claimant, and any partner, # has reached the qualifying age for State Pension Credit - # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)). This counts - # the same members as is_uc_eligible and is_pension_credit_eligible, - # so no family qualifies for both Housing Benefit and Universal - # Credit; change the three together. + # (SI 2014/1230 reg 6A(4); NI: SR 2016/226 reg 4A(4)). Every adult in + # the benefit unit stands in for the claimant and partner, as in + # is_uc_eligible and is_pension_credit_eligible, so a pensioner with + # an 18 or 19 year old dependant is routed to Universal Credit. + # Because is_uc_eligible needs a working-age adult, no family on this + # route receives Universal Credit; change the three together. adult = person("is_adult", period) adult_count = benunit.sum(adult) pension_age = (adult_count > 0) & (benunit.sum(adult & sp_age) == adult_count) # Working-age and mixed-age families (since 15 May 2019) claim # Universal Credit instead. They keep an existing award until they - # claim it (reg 8(2A)); for mixed-age couples the reported award - # stands in for the SI 2019/37 art. 4 saving (reg 6A(5)). + # claim it (reg 8(2A)), so this route also rules out receiving + # Universal Credit. For mixed-age couples the reported award stands + # in for the SI 2019/37 art. 4 saving (reg 6A(5)). Working-age awards + # outside specified or temporary accommodation ended on 1 July 2026 + # in Great Britain (SI 2025/1148 art. 7); that is not modelled. already_claiming = add(benunit, period, ["housing_benefit_reported"]) > 0 claiming_uc = benunit("would_claim_uc", period) continuing_award = already_claiming & ~claiming_uc