diff --git a/changelog.d/hb-passport-income-related-benefits.fixed.md b/changelog.d/hb-passport-income-related-benefits.fixed.md new file mode 100644 index 000000000..b0def4125 --- /dev/null +++ b/changelog.d/hb-passport-income-related-benefits.fixed.md @@ -0,0 +1 @@ +- Disregard the whole income and capital of a Housing Benefit claimant in receipt of Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance, as SI 2006/213 Schedule 5 paragraph 4 and Schedule 6 paragraph 5 (Northern Ireland: SR 2006/405 Schedule 6 paragraph 4 and Schedule 7 paragraph 5) require, so they get maximum Housing Benefit. These benefits were counted as income and tapered. The passport has no age condition, so it covers mixed-age couples whose younger member is on one of these benefits (SI 2006/213 regulation 5(1)(b)). Adds `in_receipt_of_income_support_jsa_ib_or_esa_ir`. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 4f928b173..0fe5b3054 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -41,6 +41,7 @@ "1. **Eligible rent** (`benunit_rent`): the contracted rent for the property, capped at the Local Housing Allowance rate where the LHA applies.\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 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", + " If the benunit is in receipt of Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`in_receipt_of_income_support_jsa_ib_or_esa_ir`: a positive amount of any of them), the whole of its income and capital is disregarded ([SI 2006/213 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. 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 the eligible rent less non-dependant deductions. 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)). The universal credit limb of those paragraphs is not modelled, because no benunit receives both Housing Benefit and Universal Credit.\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/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml index 135bd6ceb..8b3d0b0bf 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income.yaml @@ -51,14 +51,14 @@ input: # Benefits child_benefit: 1_000 - income_support: 2_250 + income_support: 0 jsa_income: 0 esa_income: 0 # Income employment_income: 0 self_employment_income: 0 property_income: 0 - private_pension_income: 0 + private_pension_income: 2_250 # Tax income_tax: 0 national_insurance: 0 @@ -75,14 +75,14 @@ input: # Benefits child_benefit: 1_000 - income_support: 2_250 + income_support: 0 jsa_income: 0 esa_income: 0 # Income employment_income: 0 self_employment_income: 0 property_income: 1_000 - private_pension_income: 0 + private_pension_income: 2_250 # Tax income_tax: 0 national_insurance: 0 @@ -99,14 +99,14 @@ input: # Benefits child_benefit: 1_000 - income_support: 2_250 + income_support: 0 jsa_income: 0 esa_income: 0 # Income employment_income: 0 self_employment_income: 0 property_income: 1_000 - private_pension_income: 0 + private_pension_income: 2_250 # Tax income_tax: 0 national_insurance: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_income_related_benefit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_income_related_benefit_passport.yaml new file mode 100644 index 000000000..0fee7bed4 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_income_related_benefit_passport.yaml @@ -0,0 +1,228 @@ +# SI 2006/213 Sch 5 para 4 and Sch 6 para 5 (NI: SR 2006/405 Sch 6 para 4 and +# Sch 7 para 5): where a claimant is on income support, an income-based +# jobseeker's allowance or an income-related employment and support allowance, +# the whole of the claimant's income and capital is disregarded, so Housing +# Benefit is the maximum: the eligible rent, less any non-dependant deductions +# (reg 70). Expected values are hand-computed from the regulations: council +# tenancies (no LHA cap), no non-dependants. Working-age awards use 2025-26 +# (or housing_benefit_entitlement for 2026-27), before working-age awards were +# abolished from 1 July 2026 (SI 2025/1148 art. 7). + +- name: Lone parent on Income Support has income and capital disregarded and gets maximum Housing Benefit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + employment_income: 3_000 + housing_benefit_reported: 1 + child: + age: 3 + benunits: + benunit: + members: [parent, child] + would_claim_uc: false + income_support: 5_000 + households: + household: + members: [parent, child] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + savings: 12_000 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + # Sch 6 para 5: the £12,000 is disregarded, so there is no tariff income + # (it would otherwise be £1 a week for each £250 over £6,000: £24 x 52). + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + # Sch 5 para 4: earnings, Income Support and Child Benefit all disregarded. + housing_benefit_applicable_income: 0 + housing_benefit_eligible: true + housing_benefit_entitlement: 6_240 + housing_benefit: 6_240 + +- name: Single claimant on income-based JSA has income and capital disregarded + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + employment_income: 1_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + jsa_income: 3_500 + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 12_000 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 5_200 + housing_benefit: 5_200 + +- name: Contribution-based JSA is not a passport, so income and capital are taken into account + # The same claimant as above, with contribution-based JSA in place of + # income-based JSA. The disregard and applicable amount are set so the + # taper can be computed by hand: applicable income = 3,500 + 1,000 + + # tariff income 1,248 (ceil((12,000 - 6,000) / 250) = 24, x £1 x 52) - 260 + # = 5,488; entitlement = 5,200 - 0.65 x (5,488 - 4,000) = 4,232.80. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + employment_income: 1_000 + jsa_contrib: 3_500 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + jsa_income: 0 + housing_benefit_applicable_income_disregard: 260 + housing_benefit_applicable_amount: 4_000 + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 12_000 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + housing_benefit_assessable_capital: 12_000 + housing_benefit_tariff_income: 1_248 + housing_benefit_applicable_income: 5_488 + housing_benefit_entitlement: 4_232.80 + +- name: Income-based JSA claimant above the Housing Benefit capital limit stays eligible because the whole capital is disregarded + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + jsa_income: 3_500 + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 20_000 + output: + housing_benefit_assessable_capital: 0 + housing_benefit_eligible: true + housing_benefit_entitlement: 5_200 + +- name: Couple where the partner is on income-based JSA is passported, earnings included + # Sch 5 para 5 (a joint-claim couple) and the benefit-unit reading of + # para 4: the model holds the award for the benefit unit. The claimant's + # £12,000 of earnings would otherwise take the couple's income above their + # applicable amount. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 32 + employment_income: 12_000 + housing_benefit_reported: 1 + partner: + age: 30 + benunits: + benunit: + members: [claimant, partner] + would_claim_uc: false + jsa_income: 4_000 + households: + household: + members: [claimant, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 6_240 + +- name: Mixed-age couple whose younger member is on income-related ESA is passported + # SI 2006/213 reg 5(1)(b) applies the working-age regulations to a claimant + # over State Pension Credit age whose partner is on income-related ESA (and + # SI 2006/214 reg 5(2) disapplies the pension-age ones), so Sch 5 para 4 and + # Sch 6 para 5 disregard the State Pension and the savings. Guarantee Credit + # is set to nil to show the result does not come from the Pension Credit + # passport. Members over State Pension age keep the award all year. The + # private pension would otherwise take the couple's income above their + # applicable amount. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + state_pension_reported: 12_000 + private_pension_income: 15_000 + housing_benefit_reported: 1 + younger: + age: 60 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + guarantee_credit: 0 + esa_income: 6_000 + households: + household: + members: [older, younger] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + savings: 10_000 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 6_240 + housing_benefit: 6_240 + +- name: Mixed-age couple with contributory ESA only is not passported + period: 2026 + absolute_error_margin: 0.01 + input: + people: + older: + age: 70 + state_pension_reported: 12_000 + private_pension_income: 15_000 + housing_benefit_reported: 1 + younger: + age: 60 + esa_contrib: 6_000 + benunits: + benunit: + members: [older, younger] + would_claim_uc: false + guarantee_credit: 0 + esa_income: 0 + households: + household: + members: [older, younger] + tenure_type: RENT_FROM_COUNCIL + rent: 6_240 + savings: 10_000 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + housing_benefit_assessable_capital: 10_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml new file mode 100644 index 000000000..e15181a4f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml @@ -0,0 +1,92 @@ +# Receipt of Income Support, income-based JSA or income-related ESA: a +# positive amount of any of them, held for the benefit unit. + +- name: No income-related benefit + period: 2025 + input: + income_support: 0 + jsa_income: 0 + esa_income: 0 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + +- name: Income Support alone + period: 2025 + input: + income_support: 0.01 + jsa_income: 0 + esa_income: 0 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + +- name: Income-based JSA alone + period: 2025 + input: + income_support: 0 + jsa_income: 1_000 + esa_income: 0 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + +- name: Income-related ESA alone + period: 2025 + input: + income_support: 0 + jsa_income: 0 + esa_income: 1_000 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + +- name: Contributory JSA and ESA are not receipt + period: 2025 + input: + people: + claimant: + age: 40 + jsa_contrib: 2_000 + esa_contrib: 2_000 + benunits: + benunit: + members: [claimant] + income_support: 0 + jsa_income: 0 + esa_income: 0 + output: + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + +- name: A reported income-based JSA award screened out by the capital test is not receipt + # jsa_income is nil above the £16,000 capital limit, so nothing is paid. + period: 2025 + input: + people: + claimant: + age: 40 + jsa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + savings: 20_000 + output: + jsa_income: 0 + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + +- name: A reported income-based JSA award within the capital limit is receipt + period: 2025 + input: + people: + claimant: + age: 40 + jsa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + savings: 1_000 + output: + jsa_income: 3_000 + in_receipt_of_income_support_jsa_ib_or_esa_ir: true diff --git a/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py new file mode 100644 index 000000000..1a7152e00 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py @@ -0,0 +1,299 @@ +"""Property-based tests for the Housing Benefit passport through Income Support, +income-based JSA and income-related ESA. + +SI 2006/213 Sch 5 para 4 and Sch 6 para 5 (NI: SR 2006/405 Sch 6 para 4 and +Sch 7 para 5) disregard the whole of the income and capital of a claimant on +income support, an income-based jobseeker's allowance or an income-related +employment and support allowance. Housing Benefit is then the maximum: the +eligible rent, capped at the LHA rate for LHA tenants, less non-dependant +deductions (regs 70 and 74). Reg 5(1)(b) applies the working-age regulations +to a claimant over State Pension Credit age whose partner is on one of these +benefits, so the passport has no age condition. + +Invariants, for any generated population of renting families with a +continuing award (some with a non-dependant in the household): + +1. Receipt: in_receipt_of_income_support_jsa_ib_or_esa_ir is true exactly when + Income Support, income-based JSA or income-related ESA is positive. +2. Passport: a family in receipt has nil applicable income, assessable + capital and tariff income, and meets the capital limit. +3. Maximum: every family's entitlement is at most the maximum (rent, capped at + the LHA rate for LHA tenants, less non-dependant deductions, floored at + zero); a family in receipt gets exactly the maximum. +4. Metamorphic: for a family in receipt, raising its earnings, pensions and + savings changes neither its entitlement nor its eligibility. +5. Metamorphic: adding a positive Income Support, income-based JSA or + income-related ESA amount never lowers Housing Benefit before the benefit + cap. (After the cap it can: those benefits count towards the cap. That is + an intended difference, so the test reads the pre-cap amount.) +6. Ages: for social tenants with no non-dependants, the entitlement of a + family in receipt equals the rent whether its members are of working age, + a mixed-age couple or over State Pension age. +7. Contributory JSA and ESA are not a passport: moving an amount from + income-based to contribution-based JSA or ESA turns the passport off. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2025 +PROPERTY_SETTINGS = settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# 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, 90) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single_working": ([WORKING_AGE], 0), + "couple_working": ([WORKING_AGE, WORKING_AGE], 0), + "lone_parent": ([WORKING_AGE], 2), + "couple_with_children": ([WORKING_AGE, WORKING_AGE], 1), + "mixed_age": ([PENSION_AGE, WORKING_AGE], 0), + "single_pension": ([PENSION_AGE], 0), +} +RENTING_TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA", "RENT_PRIVATELY"] +SOCIAL_TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA"] +BENEFITS = ["income_support", "jsa_income", "esa_income"] +money = st.floats(0, 20_000, allow_nan=False, allow_infinity=False) +award = st.one_of(st.just(0.0), st.floats(1, 12_000, allow_nan=False)) + + +@st.composite +def families(draw, tenures=RENTING_TENURES, non_dependants=True, shapes=None): + shape = draw(st.sampled_from(sorted(shapes or SHAPES))) + ages, n_children = SHAPES[shape] + return dict( + shape=shape, + adults=[ + dict( + age=draw(age), + employment_income=draw(st.one_of(st.just(0.0), money)), + private_pension_income=draw(st.one_of(st.just(0.0), money)), + state_pension=draw(money), + ) + for age in ages + ], + children=[draw(st.integers(0, 15)) for _ in range(n_children)], + tenure=draw(st.sampled_from(tenures)), + rent=draw(money), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + awards={benefit: draw(award) for benefit in BENEFITS}, + non_dependant_earnings=( + draw(st.one_of(st.none(), st.floats(0, 40_000))) if non_dependants else None + ), + ) + + +def situation(units, bump=0.0, awards=None, ages=None): + """Build a simulation with one claimant benefit unit per family, first in + each household, followed by any non-dependant's own benefit unit. + + bump raises every adult's earnings and pensions and the savings; awards + overrides each family's benefit amounts; ages overrides adult ages. + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + members = [] + for j, adult in enumerate(unit["adults"]): + name = f"a{i}_{j}" + age = adult["age"] if ages is None else ages[i][j] + person = { + "age": {YEAR: age}, + "employment_income": {YEAR: adult["employment_income"] + bump}, + "private_pension_income": { + YEAR: adult["private_pension_income"] + bump + }, + } + if age >= 67: + person["state_pension_reported"] = {YEAR: adult["state_pension"]} + if j == 0: + person["housing_benefit_reported"] = {YEAR: 1.0} + people[name] = person + members.append(name) + for k, child_age in enumerate(unit["children"]): + name = f"c{i}_{k}" + people[name] = {"age": {YEAR: child_age}} + members.append(name) + family_awards = unit["awards"] if awards is None else awards[i] + benunits[f"b{i}"] = { + "members": list(members), + "would_claim_uc": {YEAR: False}, + **{benefit: {YEAR: family_awards[benefit]} for benefit in BENEFITS}, + } + household_members = list(members) + if unit["non_dependant_earnings"] is not None: + name = f"n{i}" + people[name] = { + "age": {YEAR: 30}, + "employment_income": {YEAR: unit["non_dependant_earnings"]}, + } + benunits[f"nb{i}"] = {"members": [name], "would_claim_uc": {YEAR: False}} + household_members.append(name) + households[f"h{i}"] = { + "members": household_members, + "tenure_type": {YEAR: unit["tenure"]}, + "rent": {YEAR: unit["rent"]}, + "savings": {YEAR: unit["savings"] + bump}, + } + return {"people": people, "benunits": benunits, "households": households} + + +VARIABLES = [ + "in_receipt_of_income_support_jsa_ib_or_esa_ir", + "income_support", + "jsa_income", + "esa_income", + "housing_benefit_applicable_income", + "housing_benefit_assessable_capital", + "housing_benefit_tariff_income", + "housing_benefit_entitlement", + "housing_benefit_eligible", + "housing_benefit_pre_benefit_cap", + "housing_benefit_non_dep_deductions", + "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} + benunit_ids = list(sim.populations["benunit"].ids) + claimants = [benunit_ids.index(f"b{i}") for i in range(len(units))] + values = {k: v[claimants] for k, v in values.items()} + capital = sim.tax_benefit_system.parameters( + YEAR + ).gov.dwp.housing_benefit.means_test.capital + values["capital_limit"] = min(capital.working_age.limit, capital.pension_age.limit) + return values + + +def maximum_housing_benefit(values): + rent = values["benunit_rent"] + lha = values["LHA_eligible"].astype(bool) + capped_rent = np.where(lha, np.minimum(rent, values["LHA_cap"]), rent) + return np.maximum(0, capped_rent - values["housing_benefit_non_dep_deductions"]) + + +def receipt(values): + return values["in_receipt_of_income_support_jsa_ib_or_esa_ir"].astype(bool) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=20)) +def test_receipt_passport_and_maximum(units): + values = calculate(units) + on_benefit = receipt(values) + amounts = np.stack([values[benefit] for benefit in BENEFITS]) + # 1. Receipt. + assert np.array_equal(on_benefit, (amounts > 0).any(axis=0)) + # 2. Passport. + for variable in [ + "housing_benefit_applicable_income", + "housing_benefit_assessable_capital", + "housing_benefit_tariff_income", + ]: + assert np.all(values[variable][on_benefit] == 0), variable + assert np.all( + values["housing_benefit_assessable_capital"][on_benefit] + <= values["capital_limit"] + ) + # 3. Maximum. + maximum = maximum_housing_benefit(values) + entitlement = values["housing_benefit_entitlement"] + assert np.all(entitlement <= maximum + 0.01) + assert np.allclose(entitlement[on_benefit], maximum[on_benefit], atol=0.01) + + +@PROPERTY_SETTINGS +@given( + st.lists(families(), min_size=1, max_size=20), + st.floats(1, 50_000, allow_nan=False), +) +def test_passported_entitlement_ignores_income_and_capital(units, bump): + before = calculate(units) + after = calculate(units, bump=bump) + on_benefit = receipt(before) + assert np.array_equal(on_benefit, receipt(after)) + for variable in ["housing_benefit_entitlement", "housing_benefit_eligible"]: + assert np.allclose( + before[variable][on_benefit], after[variable][on_benefit], atol=0.01 + ), variable + + +@PROPERTY_SETTINGS +@given( + st.lists(families(), min_size=1, max_size=20), + st.sampled_from(BENEFITS), + st.floats(1, 12_000, allow_nan=False), +) +def test_receipt_never_lowers_pre_cap_housing_benefit(units, benefit, amount): + without = [{b: 0.0 for b in BENEFITS} for _ in units] + with_benefit = [{**w, benefit: amount} for w in without] + off = calculate(units, awards=without) + on = calculate(units, awards=with_benefit) + assert not receipt(off).any() + assert receipt(on).all() + assert np.all( + on["housing_benefit_pre_benefit_cap"] + >= off["housing_benefit_pre_benefit_cap"] - 0.01 + ) + assert np.all(on["housing_benefit_eligible"] >= off["housing_benefit_eligible"]) + + +@PROPERTY_SETTINGS +@given( + st.lists( + families( + tenures=SOCIAL_TENURES, + non_dependants=False, + shapes=["couple_working", "mixed_age"], + ), + min_size=1, + max_size=20, + ), + st.sampled_from(BENEFITS), + st.floats(1, 12_000, allow_nan=False), +) +def test_passported_social_tenant_gets_the_rent_at_any_age(units, benefit, amount): + awards = [{**{b: 0.0 for b in BENEFITS}, benefit: amount} for _ in units] + rents = np.array([unit["rent"] for unit in units]) + for first, second in [(40, 35), (70, 35), (70, 68)]: + ages = [[first, second] for _ in units] + values = calculate(units, awards=awards, ages=ages) + assert receipt(values).all() + assert values["housing_benefit_eligible"].all() + assert np.allclose(values["housing_benefit_entitlement"], rents, atol=0.01) + + +@PROPERTY_SETTINGS +@given( + st.lists(families(non_dependants=False), min_size=1, max_size=20), + st.floats(1, 12_000, allow_nan=False), +) +def test_contributory_jsa_and_esa_are_not_a_passport(units, amount): + no_awards = [{b: 0.0 for b in BENEFITS} for _ in units] + contributory = situation(units, awards=no_awards) + for name, person in contributory["people"].items(): + # The first adult of each family is its claimant. + if name.startswith("a") and name.endswith("_0"): + person["jsa_contrib"] = {YEAR: amount} + person["esa_contrib"] = {YEAR: amount} + sim = Simulation(situation=contributory) + on_benefit = np.asarray( + sim.calculate("in_receipt_of_income_support_jsa_ib_or_esa_ir", YEAR) + ) + assert not on_benefit.any() + capital = np.asarray(sim.calculate("housing_benefit_assessable_capital", YEAR)) + savings = np.array([unit["savings"] for unit in units]) + # Without the passport the family's savings count as its capital (no + # non-dependants here, so no household apportionment). + assert np.allclose(capital, savings, atol=0.01) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 999da75d2..7f2d63f3d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -5,6 +5,18 @@ class housing_benefit_applicable_income(Variable): value_type = float entity = BenUnit label = "relevant income for Housing Benefit means test" + documentation = ( + "Income taken into account in the Housing Benefit means test. It is " + "nil for a family in receipt of Income Support, income-based " + "Jobseeker's Allowance or income-related Employment and Support " + "Allowance, whose whole income is disregarded, and for a pension-age " + "family in receipt of the Pension Credit guarantee credit." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", + ) definition_period = YEAR unit = GBP @@ -64,4 +76,23 @@ def formula(benunit, period, parameters): - childcare_element, ) guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) - return where(guarantee_credit, 0, applicable_income) + # SI 2006/213 Sch 5 para 4 (NI: SR 2006/405 Sch 6 para 4) disregards + # "the whole of his income" where a claimant is on income support, an + # income-based jobseeker's allowance or an income-related employment + # and support allowance. Para 5 does the same where the claimant's + # partner in a joint-claim couple is on income-based JSA. The model + # holds these awards for the benefit unit and cannot tell which member + # claims Housing Benefit (a couple choose, reg 82(1)), so it applies + # the disregard whichever member is on the benefit. There is no age + # condition: SI 2006/213 reg 5(1)(b) (NI: SR 2006/405 reg 5(1)(b)) + # applies these regulations to a claimant over the qualifying age for + # State Pension Credit whose partner is on one of these benefits, so a + # mixed-age couple whose younger member is on income-related ESA is + # covered. The universal credit limb is left out: the model never pays + # Housing Benefit and Universal Credit to the same family + # (housing_benefit_eligible), and Universal Credit depends on Housing + # Benefit through the benefit cap. + on_income_related_benefit = benunit( + "in_receipt_of_income_support_jsa_ib_or_esa_ir", period + ) + return where(guarantee_credit | on_income_related_benefit, 0, applicable_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py index ea49fbf51..d06ebf5ae 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py @@ -11,7 +11,14 @@ class housing_benefit_assessable_capital(Variable): "adult-share proxy; person-level sources, such as a Lifetime ISA, " "count only for the holder's own benunit, and only when the holder is " "its claimant or partner (is_uc_claimant): a dependant's capital is " - "not the claimant's." + "not the claimant's. It is nil for a family in receipt of Income " + "Support, income-based Jobseeker's Allowance or income-related " + "Employment and Support Allowance, and for a pension-age family in " + "receipt of the Pension Credit guarantee credit." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5", ) definition_period = YEAR unit = GBP @@ -39,8 +46,18 @@ def formula(benunit, period, parameters): 0, ) guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + # SI 2006/213 Sch 6 para 5 (NI: SR 2006/405 Sch 7 para 5) disregards + # "the whole of his capital" where a claimant is on income support, an + # income-based jobseeker's allowance or an income-related employment + # and support allowance (para 6: a joint-claim partner on income-based + # JSA). With no capital the tariff income is nil and the capital limit + # is met. See housing_benefit_applicable_income for the age and + # universal credit points. + on_income_related_benefit = benunit( + "in_receipt_of_income_support_jsa_ib_or_esa_ir", period + ) return where( - guarantee_credit, + guarantee_credit | on_income_related_benefit, 0, max_(0, household_capital_proxy + person_capital), ) diff --git a/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py b/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py new file mode 100644 index 000000000..6603fedd0 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class in_receipt_of_income_support_jsa_ib_or_esa_ir(Variable): + label = "in receipt of Income Support, income-based JSA or income-related ESA" + documentation = ( + "Whether this benefit unit is paid Income Support, income-based " + "Jobseeker's Allowance or income-related Employment and Support " + "Allowance: a positive amount of any of them. The model holds these " + "awards at benefit-unit level, so it does not distinguish the claimant " + "from the partner. The regulations also count as on income-based JSA " + "or income-related ESA some days on which nothing is paid (a sanction, " + "a waiting day, a loss-of-benefit restriction), which the model does " + "not see." + ) + entity = BenUnit + definition_period = YEAR + value_type = bool + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/2", + ) + + def formula(benunit, period, parameters): + return ( + (benunit("income_support", period) > 0) + | (benunit("jsa_income", period) > 0) + | (benunit("esa_income", period) > 0) + )