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 0000000000..b0def41257 --- /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/changelog.d/hb-passport-review-followups.changed.md b/changelog.d/hb-passport-review-followups.changed.md new file mode 100644 index 0000000000..738975e89f --- /dev/null +++ b/changelog.d/hb-passport-review-followups.changed.md @@ -0,0 +1 @@ +- Tighten the Housing Benefit passport tests: hold Guarantee Credit at nil in the contributory-benefit property control, and base the applicable-income arithmetic fixtures on earnings and working tax credit rather than Child Benefit. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 01ae7d9a50..86ba080317 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**: 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", + " 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 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)). 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 135bd6cebc..9cb749c7cd 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 @@ -47,15 +47,17 @@ housing_benefit_applicable_income: 1_000 - name: Pension contributions halved + # SI 2006/213 reg 36(3)(b): half of the pension contributions come off + # earnings. 3,250 - 1,500 / 2 = 2,500. period: 2025 input: # Benefits - child_benefit: 1_000 - income_support: 2_250 + child_benefit: 0 + income_support: 0 jsa_income: 0 esa_income: 0 # Income - employment_income: 0 + employment_income: 3_250 self_employment_income: 0 property_income: 0 private_pension_income: 0 @@ -71,23 +73,25 @@ housing_benefit_applicable_income: 2_500 - name: Added Tax and income + # Earnings plus working tax credit, which counts in full where Sch 5 + # para 56 does not apply. 3,250 + 2,000 - 1,500 / 2 = 4,500. period: 2025 input: # Benefits - child_benefit: 1_000 - income_support: 2_250 + child_benefit: 0 + income_support: 0 jsa_income: 0 esa_income: 0 # Income - employment_income: 0 + employment_income: 3_250 self_employment_income: 0 - property_income: 1_000 + property_income: 0 private_pension_income: 0 # Tax income_tax: 0 national_insurance: 0 # Other - tax_credits: 1_000 + tax_credits: 2_000 pension_contributions: 1_500 housing_benefit_applicable_income_disregard: 0 housing_benefit_applicable_income_childcare_element: 0 @@ -95,23 +99,24 @@ housing_benefit_applicable_income: 4_500 - name: Pension contributions capped at 0 + # Applicable income is floored at nil: 3,250 - 100,000 / 2 < 0. period: 2025 input: # Benefits - child_benefit: 1_000 - income_support: 2_250 + child_benefit: 0 + income_support: 0 jsa_income: 0 esa_income: 0 # Income - employment_income: 0 + employment_income: 3_250 self_employment_income: 0 - property_income: 1_000 + property_income: 0 private_pension_income: 0 # Tax income_tax: 0 national_insurance: 0 # Other - tax_credits: 1_000 + tax_credits: 0 pension_contributions: 100_000 housing_benefit_applicable_income_disregard: 0 housing_benefit_applicable_income_childcare_element: 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 0000000000..0fee7bed44 --- /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 0000000000..e15181a4f6 --- /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 0000000000..9a9fccd960 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_income_related_benefit_passport_properties.py @@ -0,0 +1,306 @@ +"""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, and + with no guarantee credit either (which passports a pension-age family in + its own right) the family's savings stay its capital. +""" + +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} + # A pension-age family can still have its capital disregarded through the + # Guarantee Credit passport, so hold that at nil to isolate this one. + for name, benunit in contributory["benunits"].items(): + if name.startswith("b"): + benunit["guarantee_credit"] = {YEAR: 0.0} + 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]) + # With neither 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 4053330c32..7d522ac645 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 @@ -82,4 +94,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 8c6b0064dc..5e68e5a8be 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 @@ -13,12 +13,19 @@ class housing_benefit_assessable_capital(Variable): "no weight. 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_claimant_or_partner): a dependant's capital is not the " - "claimant's." + "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." ) definition_period = YEAR unit = GBP quantity_type = STOCK - reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/25" + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/25", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5", + ) def formula(benunit, period, parameters): household = benunit.household @@ -46,8 +53,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 0000000000..6603fedd03 --- /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) + )