From e38e0cde009415e2de27c08b0813b6cb4db2750b Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 08:09:51 -0400 Subject: [PATCH 1/4] Add the universal credit limb of the Housing Benefit passport SI 2006/213 Sch 4 para 12, Sch 5 para 4 and Sch 6 para 5 (NI: SR 2006/405 Sch 5 para 12, Sch 6 para 4 and Sch 7 para 5) disregard the earnings, whole income and whole capital of a claimant on universal credit, as for income support, income-based JSA and income-related ESA. 'On universal credit' means entitled, whether in payment or not (reg 2(3B)), read as Universal Credit before the benefit cap (is_uc_entitled). Co-Authored-By: Claude Opus 5.5 --- .../means_test/universal_credit_passport.yaml | 50 ++++++++++++++++ .../housing_benefit_applicable_income.py | 44 +++++++------- ...ing_benefit_applicable_income_disregard.py | 25 ++++---- .../housing_benefit_assessable_capital.py | 28 +++++---- .../housing_benefit_on_passporting_benefit.py | 59 +++++++++++++++++++ 5 files changed, 162 insertions(+), 44 deletions(-) create mode 100644 policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/universal_credit_passport.yaml create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/universal_credit_passport.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/universal_credit_passport.yaml new file mode 100644 index 0000000000..bb93a1a7e3 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/universal_credit_passport.yaml @@ -0,0 +1,50 @@ +description: >- + Whether the Housing Benefit passport covers a claimant on Universal Credit: + the whole of the earnings, other income and capital of a claimant who is + entitled to Universal Credit, whether it is in payment or not, is + disregarded, as it is for a claimant on Income Support, income-based + Jobseeker's Allowance or income-related Employment and Support Allowance. + The universal credit words came into force on different dates in Great + Britain and Northern Ireland. Annual formulas read the value sampled on + 30 April of the model year, so the limb applies from 2014-15 in Great + Britain and 2019-20 in Northern Ireland. +great_britain: + description: Whether the Housing Benefit passport covers a claimant on Universal Credit in Great Britain. SI 2013/2070 inserted "universal credit," into SI 2006/213 Sch 4 para 12, Sch 5 para 4 and Sch 6 para 5, and the meaning of being on Universal Credit as reg 2(3B), from 28 October 2013. + values: + 2006-03-06: false + 2013-10-28: true + metadata: + label: Housing Benefit passport covers Universal Credit in Great Britain + unit: bool + period: year + reference: + - title: The Housing Benefit Regulations 2006, Schedule 4 paragraph 12 + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12 + - title: The Housing Benefit Regulations 2006, Schedule 5 paragraph 4 + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4 + - title: The Housing Benefit Regulations 2006, Schedule 6 paragraph 5 + href: https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5 + - title: The Housing Benefit Regulations 2006, regulation 2(3B) + href: https://www.legislation.gov.uk/uksi/2006/213/regulation/2 + - title: The Universal Credit (Transitional Provisions) and Housing Benefit (Amendment) Regulations 2013, regulation 7 + href: https://www.legislation.gov.uk/uksi/2013/2070/regulation/7 +northern_ireland: + description: Whether the Housing Benefit passport covers a claimant on Universal Credit in Northern Ireland. SR 2018/92 inserted "universal credit," into SR 2006/405 Sch 5 para 12, Sch 6 para 4 and Sch 7 para 5, and the meaning of being on Universal Credit as reg 2(3B), from 8 May 2018. + values: + 2006-11-20: false + 2018-05-08: true + metadata: + label: Housing Benefit passport covers Universal Credit in Northern Ireland + unit: bool + period: year + reference: + - title: The Housing Benefit Regulations (Northern Ireland) 2006, Schedule 5 paragraph 12 + href: https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12 + - title: The Housing Benefit Regulations (Northern Ireland) 2006, Schedule 6 paragraph 4 + href: https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4 + - title: The Housing Benefit Regulations (Northern Ireland) 2006, Schedule 7 paragraph 5 + href: https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5 + - title: The Housing Benefit Regulations (Northern Ireland) 2006, regulation 2(3B) + href: https://www.legislation.gov.uk/nisr/2006/405/regulation/2 + - title: The Universal Credit (Persons Required to Provide Information, Miscellaneous Amendments and Saving and Transitional Provision) Regulations (Northern Ireland) 2018, regulation 4 + href: https://www.legislation.gov.uk/nisr/2018/92/regulation/4 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 7d522ac645..ca999d53c2 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 @@ -7,14 +7,16 @@ class housing_benefit_applicable_income(Variable): 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 " + "nil for a family on Universal Credit, 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." + "Allowance (housing_benefit_on_passporting_benefit), 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/2", "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", ) definition_period = YEAR @@ -95,22 +97,22 @@ def formula(benunit, period, parameters): ) guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) # 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 + # "the whole of his income" where a claimant is on universal credit, + # 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 or Universal Credit is + # covered. Universal Credit is read before the benefit cap + # (housing_benefit_on_passporting_benefit), which avoids a circular + # dependency through the cap. + on_passporting_benefit = benunit( + "housing_benefit_on_passporting_benefit", period ) - return where(guarantee_credit | on_income_related_benefit, 0, applicable_income) + return where(guarantee_credit | on_passporting_benefit, 0, applicable_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py index 7a6c766d23..dfc5797e4b 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py @@ -11,10 +11,11 @@ class housing_benefit_applicable_income_disregard(Variable): "capped at net earnings. The additional earnings disregard of £17.10 " "is added where a work condition is met and net earnings at least " "equal the other disregards, the childcare charges deducted and " - "£17.10. A claimant who, or whose partner, is on Income Support, " - "income-based Jobseeker's Allowance or income-related Employment and " - "Support Allowance has all earnings disregarded, at any age: the " - "working-age Regulations then apply (SI 2006/213 reg 5(1)(b)). The " + "£17.10. A family on Universal Credit, Income Support, income-based " + "Jobseeker's Allowance or income-related Employment and Support " + "Allowance (housing_benefit_on_passporting_benefit) has all earnings " + "disregarded, at any age: the working-age Regulations then apply " + "(SI 2006/213 reg 5(1)(b)). The " "£20 disregards for disabled people, carers and some part-time " "occupations are not modelled." ) @@ -25,6 +26,8 @@ class housing_benefit_applicable_income_disregard(Variable): "https://www.legislation.gov.uk/uksi/2006/214/schedule/4", "https://www.legislation.gov.uk/nisr/2006/405/schedule/5", "https://www.legislation.gov.uk/nisr/2006/406/schedule/5", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12", ) def formula(benunit, period, parameters): @@ -56,12 +59,14 @@ def formula(benunit, period, parameters): additional_amount, 0, ) - # Working age Sch 4 para 12. The working-age Regulations apply at any + # Working age Sch 4 para 12 (NI: SR 2006/405 Sch 5 para 12): "Where a + # claimant is on universal credit, income support, an income-based + # jobseeker's allowance or an income-related employment and support + # allowance, his earnings." The working-age Regulations apply at any # age where the claimant or partner is on one of these benefits (SI # 2006/213 reg 5(1)(b); SI 2006/214 reg 5(2)), so there is no age - # condition. Families on Universal Credit cannot receive Housing - # Benefit in the model. - on_income_related_benefit = ( - add(benunit, period, ["income_support", "jsa_income", "esa_income"]) > 0 + # condition. + on_passporting_benefit = benunit( + "housing_benefit_on_passporting_benefit", period ) - return where(on_income_related_benefit, net_earnings, standard + additional) + return where(on_passporting_benefit, net_earnings, standard + additional) 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 5e68e5a8be..29f908d5a2 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,10 +13,11 @@ 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. 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." + "claimant's. It is nil for a family on Universal Credit, Income " + "Support, income-based Jobseeker's Allowance or income-related " + "Employment and Support Allowance " + "(housing_benefit_on_passporting_benefit), and for a pension-age " + "family in receipt of the Pension Credit guarantee credit." ) definition_period = YEAR unit = GBP @@ -25,6 +26,7 @@ class housing_benefit_assessable_capital(Variable): "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", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", ) def formula(benunit, period, parameters): @@ -54,17 +56,17 @@ def formula(benunit, period, parameters): ) 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 + # "the whole of his capital" where a claimant is on universal credit, + # 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 benefit cap points. + on_passporting_benefit = benunit( + "housing_benefit_on_passporting_benefit", period ) return where( - guarantee_credit | on_income_related_benefit, + guarantee_credit | on_passporting_benefit, 0, max_(0, household_capital_proxy + person_capital), ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py new file mode 100644 index 0000000000..820205ea62 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -0,0 +1,59 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_on_passporting_benefit(Variable): + label = "on a benefit that passports the Housing Benefit means test" + documentation = ( + "Whether this benefit unit is on Universal Credit, Income Support, " + "income-based Jobseeker's Allowance or income-related Employment and " + "Support Allowance, which disregards the whole of its earnings, other " + "income and capital in the Housing Benefit means test. Income Support, " + "income-based JSA and income-related ESA count when a positive amount " + "is paid (in_receipt_of_income_support_jsa_ib_or_esa_ir). A person is " + "on Universal Credit on any day they are entitled to it, whether it is " + "in payment or not (SI 2006/213 and SR 2006/405 reg 2(3B)). The model " + "reads that as Universal Credit above nil before the benefit cap and " + "deductions (is_uc_entitled), for a family that claims it " + "(would_claim_uc), so a Universal Credit award that the cap or " + "deductions reduce to nil still counts. The model tests the year " + "rather than each day, and both members of a joint claim are on " + "Universal Credit. The universal credit limb applies from 28 October " + "2013 in Great Britain and 8 May 2018 in Northern Ireland " + "(gov.dwp.housing_benefit.means_test.universal_credit_passport)." + ) + entity = BenUnit + definition_period = YEAR + value_type = bool + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/2", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5", + ) + + def formula(benunit, period, parameters): + p = parameters( + period + ).gov.dwp.housing_benefit.means_test.universal_credit_passport + country = benunit.household("country", period) + # Great Britain's date applies everywhere but Northern Ireland, + # including an unknown country. + universal_credit_limb = where( + country == country.possible_values.NORTHERN_IRELAND, + p.northern_ireland, + p.great_britain, + ) + # Universal Credit before the benefit cap and deductions: Universal + # Credit depends on Housing Benefit through the benefit cap + # (benefit_cap_reduction), so reading the amount paid + # (universal_credit) here would be circular. Before the cap is also + # the law's reading: entitlement counts whether or not it is paid. + on_universal_credit = benunit("is_uc_entitled", period) & universal_credit_limb + on_legacy_benefit = benunit( + "in_receipt_of_income_support_jsa_ib_or_esa_ir", period + ) + return on_universal_credit | on_legacy_benefit From ecad0702c634ee075364a027b3ad412e57119fe0 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 08:38:41 -0400 Subject: [PATCH 2/4] Test the universal credit limb of the Housing Benefit passport Hand-computed YAML cases for the passport and its in-force dates, a Hypothesis property file (definition, passport to maximum, metamorphic, differential against the Income Support limb, monotonic, dates, order independence), and would_claim_uc: false on existing fixtures that test a family not on Universal Credit. Docs and changelog. Co-Authored-By: Claude Opus 5.5 --- .../hb-universal-credit-passport.fixed.md | 1 + .../programs/gov/dwp/housing-benefit.ipynb | 2 +- .../housing_benefit_applicable_income.yaml | 3 + ...g_benefit_applicable_income_disregard.yaml | 15 + .../housing_benefit_net_earnings.yaml | 1 + .../housing_benefit_capital.yaml | 2 + ...ing_benefit_universal_credit_passport.yaml | 331 +++++++++++++++ .../housing_benefit/legacy_capital.yaml | 4 + .../legacy_claimant_relationships.yaml | 4 + ...ousing_benefit_on_passporting_benefit.yaml | 128 ++++++ .../gov/dwp/lifetime_isa_capital.yaml | 3 + .../tests/test_carer_support_payment.py | 6 +- ...g_benefit_earnings_disregard_properties.py | 10 +- ...it_universal_credit_passport_properties.py | 396 ++++++++++++++++++ .../tests/test_lifetime_isa_capital.py | 12 +- 15 files changed, 910 insertions(+), 8 deletions(-) create mode 100644 changelog.d/hb-universal-credit-passport.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml create mode 100644 policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py diff --git a/changelog.d/hb-universal-credit-passport.fixed.md b/changelog.d/hb-universal-credit-passport.fixed.md new file mode 100644 index 0000000000..7a64c7832a --- /dev/null +++ b/changelog.d/hb-universal-credit-passport.fixed.md @@ -0,0 +1 @@ +- Disregard the earnings, whole income and whole capital of a Housing Benefit claimant on Universal Credit, as SI 2006/213 Schedule 4 paragraph 12, Schedule 5 paragraph 4 and Schedule 6 paragraph 5 (Northern Ireland: SR 2006/405 Schedule 5 paragraph 12, Schedule 6 paragraph 4 and Schedule 7 paragraph 5) require, so they get maximum Housing Benefit. Being on Universal Credit means being entitled to it, whether it is in payment or not (regulation 2(3B)), which the model reads as Universal Credit above nil before the benefit cap and deductions. The limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). Adds `housing_benefit_on_passporting_benefit`, which the three passports now read. No family receives both benefits in the model yet, so outputs on the Enhanced FRS do not change. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 86ba080317..346c8b8789 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -41,7 +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", + " If the benunit is on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`housing_benefit_on_passporting_benefit`), the whole of its earnings, other income and capital is disregarded ([SI 2006/213 Sch. 4 para. 12](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12), [Sch. 5 para. 4](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4) and [Sch. 6 para. 5](https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5); Northern Ireland: [SR 2006/405 Sch. 5 para. 12](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12), [Sch. 6 para. 4](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4) and [Sch. 7 para. 5](https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5)). `housing_benefit_applicable_income` and `housing_benefit_assessable_capital` are then zero, so the award is that maximum. This applies at any age: a mixed-age couple whose younger member is on one of these benefits is assessed under the working-age regulations ([SI 2006/213 reg. 5(1)(b)](https://www.legislation.gov.uk/uksi/2006/213/regulation/5)). Income Support, income-based Jobseeker's Allowance and income-related Employment and Support Allowance count when a positive amount is paid (`in_receipt_of_income_support_jsa_ib_or_esa_ir`). A person is on Universal Credit on any day they are entitled to it, whether it is in payment or not ([reg. 2(3B)](https://www.legislation.gov.uk/uksi/2006/213/regulation/2)); PolicyEngine reads that as Universal Credit above nil before the benefit cap and deductions (`is_uc_entitled`), for a family that claims it, so the passport adds no circular dependency through the benefit cap. The universal credit limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). It matters only where Housing Benefit is paid alongside Universal Credit, for specified or temporary accommodation, which the eligibility rules above do not yet model.\n", "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `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 9cb749c7cd..a89861cb30 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 @@ -25,6 +25,7 @@ - name: Baseline inputs period: 2025 input: + would_claim_uc: false # not on Universal Credit, which passports the means test # Benefits child_benefit: 1_000 income_support: 0 @@ -51,6 +52,7 @@ # earnings. 3,250 - 1,500 / 2 = 2,500. period: 2025 input: + would_claim_uc: false # not on Universal Credit, which passports the means test # Benefits child_benefit: 0 income_support: 0 @@ -77,6 +79,7 @@ # para 56 does not apply. 3,250 + 2,000 - 1,500 / 2 = 4,500. period: 2025 input: + would_claim_uc: false # not on Universal Credit, which passports the means test # Benefits child_benefit: 0 income_support: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml index 95d4477903..4f18f63525 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml @@ -49,6 +49,7 @@ employment_income: 5_200 # £100 × 52 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: housing_benefit_applicable_income_disregard: 260 # £5 × 52, not uprated @@ -63,6 +64,7 @@ employment_income: 5_200 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: housing_benefit_applicable_income_disregard: 260 # £5 × 52, not uprated @@ -79,6 +81,7 @@ age: 38 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant, partner] output: housing_benefit_applicable_income_disregard: 520 # £10 × 52 @@ -132,6 +135,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: housing_benefit_applicable_income_disregard: 1_300 # £25 × 52 @@ -209,6 +213,7 @@ weekly_hours: 30 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: housing_benefit_applicable_income_disregard: 260 # £5 × 52 @@ -224,6 +229,7 @@ weekly_hours: 30 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: housing_benefit_applicable_income_disregard: 1_149.20 # (£5 + £17.10) × 52 @@ -256,6 +262,7 @@ weekly_hours: 30 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: housing_benefit_applicable_income_disregard: 260 # £5 × 52 @@ -273,6 +280,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: # £8,000 ≥ (£25 + £17.10) × 52 = £2,189.20. @@ -291,6 +299,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: # £2,080 < (£25 + £17.10) × 52 = £2,189.20. @@ -327,6 +336,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: housing_benefit_applicable_income_disregard: 1_300 # £25 × 52 @@ -345,6 +355,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: # Childcare deducted is min(£4,160, one-child maximum) = £4,160. @@ -366,6 +377,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: # £5,200 ≥ £1,300 + £1,040 + £889.20 = £3,229.20. @@ -387,6 +399,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant, partner, child] output: housing_benefit_applicable_income_disregard: 1_409.20 # (£10 + £17.10) × 52 @@ -406,6 +419,7 @@ weekly_hours: 16 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant, partner] output: # One member must work 30 hours; hours are not added across the couple. @@ -423,6 +437,7 @@ is_disabled_for_benefits: true benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: # Para 17(2)(b)(iv)(aa): a single claimant with the disability premium diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_net_earnings.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_net_earnings.yaml index b6707a83bb..81e4733ddc 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_net_earnings.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_net_earnings.yaml @@ -136,6 +136,7 @@ age: 0 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: # SI 2006/213 reg 35(1)(i). £8,000 is below the personal allowance and diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml index f598331f3a..af28ec2ca0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml @@ -140,8 +140,10 @@ age: 30 benunits: benunit_1: + would_claim_uc: false # not on Universal Credit, which passports the means test members: person_1 benunit_2: + would_claim_uc: false # not on Universal Credit, which passports the means test members: person_2 households: household: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml new file mode 100644 index 0000000000..df0be17e6d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml @@ -0,0 +1,331 @@ +# SI 2006/213 Sch 4 para 12, Sch 5 para 4 and Sch 6 para 5 (NI: SR 2006/405 +# Sch 5 para 12, Sch 6 para 4 and Sch 7 para 5): where a claimant is on +# universal credit, income support, an income-based jobseeker's allowance or an +# income-related employment and support allowance, the claimant's earnings, the +# whole of the claimant's income and the whole of the claimant's capital are +# disregarded. A person is on universal credit on any day they are entitled to +# it, whether it is in payment or not (reg 2(3B)). With nil income Housing +# Benefit is the appropriate maximum: the eligible rent less non-dependant +# deductions (SSCBA 1992 s.130(3)(a); reg 70). +# +# In law working-age Housing Benefit is paid alongside Universal Credit for +# specified or temporary accommodation (UC (Transitional Provisions) Regs 2014 +# reg 5(2)(a)), whose rent is excluded from the Universal Credit housing costs +# element (UC Regs 2013 Sch 1 para 3(h)-(i)). The model has no such route yet +# (PolicyEngine/policyengine-uk#1910 adds one), so these cases supply +# housing_benefit_eligible and a nil uc_housing_costs_element as inputs. +# +# Expected values are hand-computed from the regulations: council tenancies +# (no LHA), no non-dependants, 2025-26 amounts. Universal Credit: standard +# allowance £400.14 a month for a single claimant aged 25 or over (UC Regs +# reg 36), a 55% taper on earnings with no work allowance (reg 22), and +# tariff income of £4.35 a month for each £250 of capital over £6,000 +# (reg 72). Housing Benefit: £5 a week single earnings disregard (Sch 4 +# para 10), tariff income of £1 a week for each £250 of capital over £6,000 +# (reg 52), 65% taper (reg 71), 52 weeks a year. + +- name: Single claimant on Universal Credit has earnings, income and capital disregarded and gets maximum Housing Benefit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + employment_income: 8_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + housing_benefit_eligible: true + uc_housing_costs_element: 0 + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 7_000 + output: + # 400.14 x 12 = 4,801.68, less 55% of 8,000 = 4,400, less tariff income + # ceil((7,000 - 6,000) / 250) = 4 x 4.35 x 12 = 208.80: 192.88. + universal_credit_pre_benefit_cap: 192.88 + is_uc_entitled: true + universal_credit: 192.88 + housing_benefit_on_passporting_benefit: true + # Sch 4 para 12: all the earnings. + housing_benefit_applicable_income_disregard: 8_000 + # Sch 6 para 5: the £7,000 is disregarded, so there is no tariff income. + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + # Sch 5 para 4: the whole income. + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 5_200 + housing_benefit_pre_benefit_cap: 5_200 + housing_benefit: 5_200 + +- name: The same claimant not on Universal Credit is means-tested on earnings and capital + # Not claiming Universal Credit, so on the continuing-award route. Applicable + # income = 8,000 - 260 (£5 x 52) + tariff income 208 (4 x £1 x 52) = 7,948. + # With the applicable amount set to 4,000, entitlement = 5,200 - 0.65 x + # (7,948 - 4,000) = 5,200 - 2,566.20 = 2,633.80. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + employment_income: 8_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + housing_benefit_applicable_amount: 4_000 + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 7_000 + output: + is_uc_entitled: false + housing_benefit_on_passporting_benefit: false + housing_benefit_applicable_income_disregard: 260 + housing_benefit_assessable_capital: 7_000 + housing_benefit_tariff_income: 208 + housing_benefit_applicable_income: 7_948 + housing_benefit_eligible: true + housing_benefit_entitlement: 2_633.80 + +- name: Universal Credit reduced to nil by deductions still passports Housing Benefit + # Reg 2(3B): on universal credit "whether it is in payment or not". The + # £1,000 entitlement is all deducted, so nothing is paid. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + employment_income: 3_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + universal_credit_pre_benefit_cap: 1_000 + uc_deductions: 1_000 + housing_benefit_eligible: true + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 10_000 + output: + is_uc_entitled: true + universal_credit: 0 + housing_benefit_on_passporting_benefit: true + housing_benefit_applicable_income_disregard: 3_000 + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 5_200 + +- name: Universal Credit of one penny passports Housing Benefit + # The minimum Universal Credit award is one penny (WRA 2012 s.5(1)(b); UC + # Regs reg 17), so an entitlement of £0.01 is an entitlement. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + employment_income: 3_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + universal_credit_pre_benefit_cap: 0.01 + housing_benefit_eligible: true + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 10_000 + output: + is_uc_entitled: true + housing_benefit_on_passporting_benefit: true + housing_benefit_applicable_income: 0 + housing_benefit_assessable_capital: 0 + housing_benefit_entitlement: 5_200 + +- name: A Universal Credit claim that comes to nil does not passport Housing Benefit + # No entitlement below one penny. Applicable income = 3,000 - 260 + tariff + # income 832 (ceil((10,000 - 6,000) / 250) = 16, x £1 x 52) = 3,572; with + # the applicable amount set to 3,000, entitlement = 5,200 - 0.65 x 572 = + # 4,828.20. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + employment_income: 3_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + universal_credit_pre_benefit_cap: 0 + housing_benefit_eligible: true + housing_benefit_applicable_amount: 3_000 + households: + household: + members: [claimant] + tenure_type: RENT_FROM_COUNCIL + rent: 5_200 + savings: 10_000 + output: + is_uc_entitled: false + housing_benefit_on_passporting_benefit: false + housing_benefit_applicable_income_disregard: 260 + housing_benefit_assessable_capital: 10_000 + housing_benefit_tariff_income: 832 + housing_benefit_applicable_income: 3_572 + housing_benefit_entitlement: 4_828.20 + +- name: Couple on a joint Universal Credit claim has both members' earnings and the household capital disregarded + # Joint claimants are both entitled to Universal Credit (WRA 2012 s.3(2)), and + # the partner's income and capital are treated as the claimant's (reg 25). + # Without the passport the couple's net earnings would be 9,000, less the + # £10 a week couple disregard. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 35 + employment_income: 3_000 + housing_benefit_reported: 1 + partner: + age: 33 + employment_income: 6_000 + benunits: + benunit: + members: [claimant, partner] + would_claim_uc: true + universal_credit_pre_benefit_cap: 2_000 + housing_benefit_eligible: true + households: + household: + members: [claimant, partner] + tenure_type: RENT_FROM_HA + rent: 6_240 + savings: 14_000 + output: + housing_benefit_on_passporting_benefit: true + housing_benefit_applicable_income_disregard: 9_000 + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 6_240 + +- name: Mixed-age couple on Universal Credit is assessed under the working-age regulations with its income disregarded + # Reg 5(1)(b): the working-age regulations apply to a claimant over the + # qualifying age for State Pension Credit whose partner is on universal + # credit, so the older member's pension is disregarded too (Sch 5 para 4). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + private_pension_income: 5_000 + housing_benefit_reported: 1 + partner: + age: 40 + employment_income: 2_000 + benunits: + benunit: + members: [claimant, partner] + would_claim_uc: true + universal_credit_pre_benefit_cap: 3_000 + housing_benefit_eligible: true + guarantee_credit: 0 + households: + household: + members: [claimant, partner] + tenure_type: RENT_FROM_COUNCIL + rent: 5_720 + savings: 9_000 + output: + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: true + housing_benefit_applicable_income_disregard: 2_000 + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 5_720 + +- name: Northern Ireland claimant on Universal Credit has earnings, income and capital disregarded + # SR 2006/405 Sch 5 para 12, Sch 6 para 4 and Sch 7 para 5, in force for + # universal credit from 8 May 2018 (SR 2018/92). + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + employment_income: 3_000 + housing_benefit_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: true + universal_credit_pre_benefit_cap: 1_000 + housing_benefit_eligible: true + households: + household: + members: [claimant] + country: NORTHERN_IRELAND + tenure_type: RENT_FROM_COUNCIL + rent: 4_160 + savings: 10_000 + output: + housing_benefit_on_passporting_benefit: true + housing_benefit_applicable_income_disregard: 3_000 + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 4_160 + +- name: A family on Universal Credit with income and capital gets the same Housing Benefit as on income-based JSA + # The universal credit and income-based JSA limbs of the passport have the + # same effect: maximum Housing Benefit. Same household as the income-based + # JSA case in housing_benefit_income_related_benefit_passport.yaml. + 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: true + universal_credit_pre_benefit_cap: 3_500 + housing_benefit_eligible: true + 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_on_passporting_benefit: true + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + housing_benefit_applicable_income: 0 + housing_benefit_entitlement: 5_200 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml index b1ff157058..d89602c192 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_capital.yaml @@ -12,8 +12,10 @@ age: 30 benunits: claimant_benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant] unrelated_benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [unrelated_claimant] households: household: @@ -39,8 +41,10 @@ age: 30 benunits: claimant_benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant, student] unrelated_benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [unrelated_claimant] households: household: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml index 64f8469107..e30e13c8b2 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/legacy_claimant_relationships.yaml @@ -22,6 +22,7 @@ is_in_non_advanced_education: true benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person0, person1] housing_benefit_eligible: true benefits_premiums: 0 @@ -61,6 +62,7 @@ is_in_non_advanced_education: true benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person0, person1, person2] housing_benefit_eligible: true benefits_premiums: 0 @@ -125,6 +127,7 @@ is_claimant_or_partner: true benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person0, person1] housing_benefit_eligible: true benefits_premiums: 0 @@ -159,6 +162,7 @@ weekly_hours: 0 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person0, person1] housing_benefit_eligible: true benefits_premiums: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml new file mode 100644 index 0000000000..655939c656 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/housing_benefit_on_passporting_benefit.yaml @@ -0,0 +1,128 @@ +# A benefit unit is on a benefit that passports the Housing Benefit means test +# when it is on universal credit, income support, an income-based jobseeker's +# allowance or an income-related employment and support allowance (SI 2006/213 +# Sch 4 para 12, Sch 5 para 4, Sch 6 para 5; NI: SR 2006/405 Sch 5 para 12, +# Sch 6 para 4, Sch 7 para 5). On universal credit means entitled to it, +# whether in payment or not (reg 2(3B)). The universal credit words came into +# force on 28 October 2013 in Great Britain (SI 2013/2070 reg 7) and on 8 May +# 2018 in Northern Ireland (SR 2018/92 reg 4). Model years are fiscal years +# read on 30 April, so the limb applies from 2014 in Great Britain and 2019 in +# Northern Ireland. + +- name: Not on any of the benefits + period: 2025 + input: + is_uc_entitled: false + income_support: 0 + jsa_income: 0 + esa_income: 0 + output: + housing_benefit_on_passporting_benefit: false + +- name: Entitled to Universal Credit + period: 2025 + input: + is_uc_entitled: true + income_support: 0 + jsa_income: 0 + esa_income: 0 + output: + housing_benefit_on_passporting_benefit: true + +- name: Income Support alone, without Universal Credit + period: 2025 + input: + is_uc_entitled: false + income_support: 0.01 + jsa_income: 0 + esa_income: 0 + output: + housing_benefit_on_passporting_benefit: true + +- name: Claims Universal Credit but the amount before the cap is nil + period: 2025 + input: + would_claim_uc: true + universal_credit_pre_benefit_cap: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + output: + is_uc_entitled: false + housing_benefit_on_passporting_benefit: false + +- name: Universal Credit before the benefit cap counts even when the cap leaves nothing to pay + period: 2025 + input: + would_claim_uc: true + universal_credit_pre_benefit_cap: 2_000 + benefit_cap_reduction: 2_000 + income_support: 0 + jsa_income: 0 + esa_income: 0 + output: + universal_credit: 0 + housing_benefit_on_passporting_benefit: true + +- name: Great Britain in 2013-14, before the universal credit limb on 28 October 2013 + period: 2013 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + country: ENGLAND + output: + housing_benefit_on_passporting_benefit: false + +- name: Great Britain in 2013-14, Income Support still passports + period: 2013 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + country: ENGLAND + output: + housing_benefit_on_passporting_benefit: true + +- name: Great Britain in 2014-15, the first year with the universal credit limb + period: 2014 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + country: SCOTLAND + output: + housing_benefit_on_passporting_benefit: true + +- name: Wales in 2018-19 has the limb + period: 2018 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + country: WALES + output: + housing_benefit_on_passporting_benefit: true + +- name: Northern Ireland in 2018-19, read on 30 April 2018, before the limb on 8 May 2018 + period: 2018 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + country: NORTHERN_IRELAND + output: + housing_benefit_on_passporting_benefit: false + +- name: Northern Ireland in 2019-20 has the limb + period: 2019 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + country: NORTHERN_IRELAND + output: + housing_benefit_on_passporting_benefit: true + +- name: Northern Ireland in 2025-26 with Universal Credit + period: 2025 + input: + is_uc_entitled: true + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + country: NORTHERN_IRELAND + output: + housing_benefit_on_passporting_benefit: true diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml index ad7433be18..0755742aeb 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml @@ -228,8 +228,10 @@ lifetime_isa_balance: 10_000 benunits: parents: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent_1, parent_2] child: + would_claim_uc: false # not on Universal Credit, which passports the means test members: adult_child households: household: @@ -421,6 +423,7 @@ lifetime_isa_balance: 10_000 benunits: family: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, dependant] households: household: diff --git a/policyengine_uk/tests/test_carer_support_payment.py b/policyengine_uk/tests/test_carer_support_payment.py index c8594b7581..c833bfae38 100644 --- a/policyengine_uk/tests/test_carer_support_payment.py +++ b/policyengine_uk/tests/test_carer_support_payment.py @@ -126,7 +126,11 @@ def test_partner_csp_for_caring_gives_couple_severe_disability_single_rate_or_ni def test_csp_counts_for_uc_non_dep_exemption_and_housing_benefit_income(): - sim = Simulation(situation=_situation(YEAR_2025)) + situation = _situation(YEAR_2025) + # Not claiming Universal Credit: on it, the whole of the Housing Benefit + # income would be disregarded (SI 2006/213 Sch 5 para 4). + situation["benunits"]["benunit"]["would_claim_uc"] = {YEAR_2025: False} + sim = Simulation(situation=situation) csp_amount = sim.calculate("carer_support_payment", YEAR_2025)[0] hb_disregard = sim.calculate( diff --git a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py index c20932dfd2..06c0663331 100644 --- a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py @@ -5,11 +5,12 @@ else's, plus the £17.10 additional earnings disregard where a work condition is met and net earnings at least equal the other disregards, the deductible childcare charges and £17.10. A claimant who, or whose partner, is on -Income Support, income-based JSA or income-related ESA has all earnings -disregarded (SI 2006/213 Sch 4 para 12), at any age: SI 2006/213 then applies -whatever the claimant's age (reg 5(1)(b)). +Universal Credit, Income Support, income-based JSA or income-related ESA has +all earnings disregarded (SI 2006/213 Sch 4 para 12), at any age: SI 2006/213 +then applies whatever the claimant's age (reg 5(1)(b)). -Invariants, for any generated population of families not on those benefits: +Invariants, for any generated population of families not on those benefits +(none claims Universal Credit): 1. Bounds: 0 <= disregard <= min(net earnings, (£25 + £17.10) x 52), and net earnings never exceed the claimant's and partner's gross earnings. @@ -143,6 +144,7 @@ def situation(units, earnings_bump=0.0, income_support=0.0, age_map=None): names.append(name) benunits[f"b{i}"] = { "members": names, + "would_claim_uc": {YEAR: False}, "income_support": {YEAR: income_support}, "jsa_income": {YEAR: 0.0}, "esa_income": {YEAR: 0.0}, diff --git a/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py new file mode 100644 index 0000000000..690f1dd5e9 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py @@ -0,0 +1,396 @@ +"""Property-based tests for the universal credit limb of the Housing Benefit +passport. + +SI 2006/213 Sch 4 para 12, Sch 5 para 4 and Sch 6 para 5 (NI: SR 2006/405 +Sch 5 para 12, Sch 6 para 4 and Sch 7 para 5) disregard the earnings, the +whole income and the whole capital of a claimant on universal credit, as for +one on income support, income-based JSA or income-related ESA. A person is on +universal credit on any day they are entitled to it, whether it is in payment +or not (reg 2(3B)); the model reads Universal Credit before the benefit cap +and deductions. Housing Benefit is then the appropriate maximum: the eligible +rent, capped at the LHA rate for LHA tenants, less non-dependant deductions +(SSCBA 1992 s.130(3)(a); reg 70). + +The model has no route yet to Housing Benefit alongside Universal Credit +(specified or temporary accommodation, UC (Transitional Provisions) Regs 2014 +reg 5(2)(a)), so each claimant benefit unit supplies housing_benefit_eligible. + +Invariants, for any generated population of renting families (some with a +non-dependant in the household), each on Universal Credit (a supplied amount +before the cap, or one the model computes), not on it, or on a legacy benefit: + +1. Definition: housing_benefit_on_passporting_benefit is true exactly when + Income Support, income-based JSA or income-related ESA is positive, or the + family claims Universal Credit and its amount before the cap is positive. +2. Passport: a passported family has nil applicable income, assessable capital + and tariff income; its earnings disregard is its net earnings; and its + entitlement is exactly the maximum Housing Benefit. Every family's + entitlement is at most that maximum. +3. Metamorphic: for a family on Universal Credit, raising its earnings, + pensions and savings changes neither the passport nor its entitlement. +4. Differential: the universal credit limb and the Income Support limb give + the same Housing Benefit means test. +5. Monotonic: putting a family on Universal Credit never lowers its Housing + Benefit before the cap or its eligibility. (After the cap it can: Universal + Credit counts towards the cap. The test reads the pre-cap amount.) +6. Dates: the universal credit limb applies in every model year from 2014 in + Great Britain and from 2019 in Northern Ireland (in force 28 October 2013 + and 8 May 2018; model years are read on 30 April), and never before. +7. Order independence: with Housing Benefit and Universal Credit both paid, + calculating Housing Benefit, Universal Credit, the benefit cap or the + passport first gives the same results, so the passport (which reads + Universal Credit before the cap) adds no circular dependency. +""" + +import itertools + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.system import system + +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; 67 and over is unambiguously pension +# age and 60 and under unambiguously working age. A family whose members are +# all over pension age cannot claim Universal Credit, so there is no such +# shape here. +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), +} +RENTING_TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA", "RENT_PRIVATELY"] +LEGACY = ["income_support", "jsa_income", "esa_income"] +COUNTRIES = ["ENGLAND", "SCOTLAND", "WALES", "NORTHERN_IRELAND"] +money = st.floats(0, 20_000, allow_nan=False, allow_infinity=False) +# A supplied Universal Credit amount before the cap: nil, the one-penny +# minimum, or more. +uc_amount = st.one_of( + st.just(0.0), st.just(0.01), st.floats(1, 15_000, allow_nan=False) +) + + +@st.composite +def families(draw, uc_modes=("off", "supplied", "computed"), legacy=True): + shape = draw(st.sampled_from(sorted(SHAPES))) + ages, n_children = SHAPES[shape] + mode = draw(st.sampled_from(uc_modes)) + # At most one legacy benefit, and none for half the families, so that + # most families on Universal Credit are passported by it alone. + legacy_benefit = draw(st.sampled_from([None] * 3 + LEGACY)) if legacy else None + legacy_amount = draw(st.floats(1, 12_000, allow_nan=False)) + return dict( + 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)), + ) + for age in ages + ], + children=[draw(st.integers(0, 15)) for _ in range(n_children)], + tenure=draw(st.sampled_from(RENTING_TENURES)), + country=draw(st.sampled_from(COUNTRIES)), + rent=draw(money), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + uc_mode=mode, + uc_amount=draw(uc_amount) if mode == "supplied" else None, + legacy={ + benefit: legacy_amount if benefit == legacy_benefit else 0.0 + for benefit in LEGACY + }, + non_dependant_earnings=draw(st.one_of(st.none(), st.floats(0, 40_000))), + ) + + +# Appended to every generated population, so that each example has a family +# passported by Universal Credit alone, with earnings and capital the passport +# must disregard. +ON_UNIVERSAL_CREDIT_ONLY = dict( + adults=[dict(age=40, employment_income=8_000.0, private_pension_income=0.0)], + children=[], + tenure="RENT_FROM_COUNCIL", + country="ENGLAND", + rent=5_200.0, + savings=12_000.0, + uc_mode="supplied", + uc_amount=1_000.0, + legacy={benefit: 0.0 for benefit in LEGACY}, + non_dependant_earnings=None, +) + + +def situation(units, bump=0.0, overrides=None): + """Build one claimant benefit unit per family, first in its household, + followed by any non-dependant's own benefit unit. + + bump raises every adult's earnings and pensions and the savings; overrides + replaces fields of each family (for example its uc_mode or legacy amounts). + """ + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + unit = {**unit, **(overrides[i] if overrides else {})} + members = [] + for j, adult in enumerate(unit["adults"]): + name = f"a{i}_{j}" + person = { + "age": {YEAR: adult["age"]}, + "employment_income": {YEAR: adult["employment_income"] + bump}, + "private_pension_income": { + YEAR: adult["private_pension_income"] + bump + }, + } + 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) + benunit = { + "members": list(members), + # Stands in for a claim in specified or temporary accommodation. + "housing_benefit_eligible": {YEAR: True}, + # Pension Credit's guarantee credit passports a pension-age + # family in its own right; hold it at nil to isolate this one. + "guarantee_credit": {YEAR: 0.0}, + **{benefit: {YEAR: unit["legacy"][benefit]} for benefit in LEGACY}, + } + if unit["uc_mode"] == "off": + benunit["would_claim_uc"] = {YEAR: False} + else: + benunit["would_claim_uc"] = {YEAR: True} + if unit["uc_mode"] == "supplied": + benunit["universal_credit_pre_benefit_cap"] = {YEAR: unit["uc_amount"]} + benunits[f"b{i}"] = benunit + 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, + "country": {YEAR: unit["country"]}, + "tenure_type": {YEAR: unit["tenure"]}, + "rent": {YEAR: unit["rent"]}, + "savings": {YEAR: unit["savings"] + bump}, + } + return {"people": people, "benunits": benunits, "households": households} + + +VARIABLES = [ + "housing_benefit_on_passporting_benefit", + "in_receipt_of_income_support_jsa_ib_or_esa_ir", + "is_uc_entitled", + "would_claim_uc", + "universal_credit_pre_benefit_cap", + "housing_benefit_net_earnings", + "housing_benefit_applicable_income_disregard", + "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", +] +MEANS_TEST = [ + "housing_benefit_applicable_income_disregard", + "housing_benefit_applicable_income", + "housing_benefit_assessable_capital", + "housing_benefit_tariff_income", + "housing_benefit_entitlement", +] + + +def calculate(units, order=None, **kwargs): + sim = Simulation(situation=situation(units, **kwargs)) + for variable in order or []: + sim.calculate(variable, YEAR) + 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))] + return {k: v[claimants] for k, v in values.items()} + + +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 passported(values): + return values["housing_benefit_on_passporting_benefit"].astype(bool) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=20)) +def test_definition_passport_and_maximum(units): + units = units + [ON_UNIVERSAL_CREDIT_ONLY] + values = calculate(units) + passport = passported(values) + # 1. Definition. + legacy = np.array([any(u["legacy"][b] > 0 for b in LEGACY) for u in units]) + claims_uc = values["would_claim_uc"].astype(bool) + on_uc = claims_uc & (values["universal_credit_pre_benefit_cap"] > 0) + assert np.array_equal(values["is_uc_entitled"].astype(bool), on_uc) + assert np.array_equal(passport, legacy | on_uc) + # 2. Passport. + for variable in [ + "housing_benefit_applicable_income", + "housing_benefit_assessable_capital", + "housing_benefit_tariff_income", + ]: + assert np.all(values[variable][passport] == 0), variable + assert np.allclose( + values["housing_benefit_applicable_income_disregard"][passport], + values["housing_benefit_net_earnings"][passport], + atol=0.01, + ) + maximum = maximum_housing_benefit(values) + entitlement = values["housing_benefit_entitlement"] + assert np.all(entitlement <= maximum + 0.01) + assert np.allclose(entitlement[passport], maximum[passport], atol=0.01) + + +@PROPERTY_SETTINGS +@given( + st.lists(families(uc_modes=("supplied",), legacy=False), min_size=1, max_size=20), + st.floats(1, 50_000, allow_nan=False), +) +def test_universal_credit_passport_ignores_income_and_capital(units, bump): + units = units + [ON_UNIVERSAL_CREDIT_ONLY] + before = calculate(units) + after = calculate(units, bump=bump) + passport = passported(before) + # A supplied amount does not move with income, so neither does the + # passport, which holds exactly where the amount is positive. + amounts = np.array([unit["uc_amount"] for unit in units]) + assert np.array_equal(passport, amounts > 0) + assert np.array_equal(passport, passported(after)) + for variable in ["housing_benefit_entitlement", "housing_benefit_eligible"]: + assert np.allclose( + before[variable][passport], after[variable][passport], atol=0.01 + ), variable + + +@PROPERTY_SETTINGS +@given( + st.lists(families(legacy=False), min_size=1, max_size=20), + st.floats(0.01, 15_000, allow_nan=False), +) +def test_universal_credit_and_income_support_limbs_agree(units, amount): + no_legacy = {benefit: 0.0 for benefit in LEGACY} + on_uc = calculate( + units, + overrides=[ + {"uc_mode": "supplied", "uc_amount": amount, "legacy": no_legacy} + for _ in units + ], + ) + on_income_support = calculate( + units, + overrides=[ + {"uc_mode": "off", "legacy": {**no_legacy, "income_support": amount}} + for _ in units + ], + ) + assert passported(on_uc).all() + assert passported(on_income_support).all() + assert not on_uc["in_receipt_of_income_support_jsa_ib_or_esa_ir"].any() + for variable in MEANS_TEST: + assert np.allclose(on_uc[variable], on_income_support[variable], atol=0.01), ( + variable + ) + + +@PROPERTY_SETTINGS +@given( + st.lists(families(legacy=False), min_size=1, max_size=20), + st.floats(0.01, 15_000, allow_nan=False), +) +def test_universal_credit_never_lowers_pre_cap_housing_benefit(units, amount): + off = calculate(units, overrides=[{"uc_mode": "off"} for _ in units]) + on = calculate( + units, + overrides=[{"uc_mode": "supplied", "uc_amount": amount} for _ in units], + ) + assert not passported(off).any() + assert passported(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_entitlement"] >= off["housing_benefit_entitlement"] - 0.01 + ) + assert np.all(on["housing_benefit_eligible"] >= off["housing_benefit_eligible"]) + + +def test_universal_credit_limb_dates(): + in_force = {"great_britain": 2014, "northern_ireland": 2019} + for year, country in itertools.product(range(2013, 2031), COUNTRIES): + jurisdiction = ( + "northern_ireland" if country == "NORTHERN_IRELAND" else "great_britain" + ) + sim = Simulation( + situation={ + "people": {"p": {"age": {year: 40}}}, + "benunits": { + "b": { + "members": ["p"], + "is_uc_entitled": {year: True}, + "in_receipt_of_income_support_jsa_ib_or_esa_ir": {year: False}, + } + }, + "households": {"h": {"members": ["p"], "country": {year: country}}}, + } + ) + expected = year >= in_force[jurisdiction] + result = sim.calculate("housing_benefit_on_passporting_benefit", year)[0] + assert bool(result) == expected, (year, country) + p = system.parameters(year).gov.dwp.housing_benefit.means_test + assert bool(p.universal_credit_passport[jurisdiction]) == expected + + +@PROPERTY_SETTINGS +@given( + st.lists(families(uc_modes=("computed",), legacy=False), min_size=1, max_size=10) +) +def test_calculation_order_does_not_change_results(units): + orders = [ + ["housing_benefit"], + ["universal_credit"], + ["benefit_cap_reduction"], + ["housing_benefit_on_passporting_benefit"], + ["household_net_income"], + ] + results = [calculate(units, order=order) for order in orders] + for other in results[1:]: + for variable in VARIABLES: + assert np.allclose( + results[0][variable].astype(float), + other[variable].astype(float), + atol=0.01, + ), variable diff --git a/policyengine_uk/tests/test_lifetime_isa_capital.py b/policyengine_uk/tests/test_lifetime_isa_capital.py index c06f16af6d..09b1c3434b 100644 --- a/policyengine_uk/tests/test_lifetime_isa_capital.py +++ b/policyengine_uk/tests/test_lifetime_isa_capital.py @@ -70,8 +70,16 @@ def test_emptying_a_person_sources_list_disregards_the_lifetime_isa_there_only() "2025-01-01.2100-12-31": [] } } - baseline = Simulation(situation=LIFETIME_ISA_HOLDER) - reformed = Simulation(situation=LIFETIME_ISA_HOLDER, reform=reform) + # Not claiming Universal Credit: on it, the whole of the Housing Benefit + # capital would be disregarded (SI 2006/213 Sch 6 para 5). + holder = { + **LIFETIME_ISA_HOLDER, + "benunits": { + "benunit": {"members": ["person"], "would_claim_uc": {"2025": False}} + }, + } + baseline = Simulation(situation=holder) + reformed = Simulation(situation=holder, reform=reform) assert baseline.calculate("uc_assessable_capital", 2025)[0] == 8_500 assert reformed.calculate("uc_assessable_capital", 2025)[0] == 1_000 From 61da46a05e9c1a92aed24577d60f058945197be6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 09:05:50 -0400 Subject: [PATCH 3/4] Address review: pin vacuous fixtures, strengthen the order property - Pin would_claim_uc: false on 18 more YAML cases whose families were UC-entitled, so the passport, not the rule under test, produced the expected value (found by rebuilding every case with the limb on and off). A mutation removing the earnings-disregard cap now fails them again. - housing_benefit_eligible.yaml: its absolute_error_margin of 5 voided the boolean checks, and two cases expecting True computed False; pin the continuing-award route and drop the margin. - test_means_test_income_properties checks both claim states, and the CTR pensioner property's invariant 6 pins families off Universal Credit. - The order-independence property compares Housing Benefit, Universal Credit, the benefit cap reduction and household net income, with a capped sentinel family on both benefits. - Explicit weekly hours in the hand-computed cases; docstring on the age condition; changelog wording. Co-Authored-By: Claude Opus 5.5 --- .../hb-universal-credit-passport.fixed.md | 2 +- .../housing_benefit_applicable_income.yaml | 2 + ...g_benefit_applicable_income_disregard.yaml | 9 +++ .../housing_benefit_eligible.yaml | 8 +- ...ing_benefit_universal_credit_passport.yaml | 6 ++ .../gov/dwp/lifetime_isa_capital.yaml | 3 + ...pensioner_earnings_disregard_properties.py | 6 +- ...it_universal_credit_passport_properties.py | 74 ++++++++++++++----- .../test_means_test_income_properties.py | 37 ++++++---- .../housing_benefit_on_passporting_benefit.py | 7 +- 10 files changed, 113 insertions(+), 41 deletions(-) diff --git a/changelog.d/hb-universal-credit-passport.fixed.md b/changelog.d/hb-universal-credit-passport.fixed.md index 7a64c7832a..d9cb9bad7f 100644 --- a/changelog.d/hb-universal-credit-passport.fixed.md +++ b/changelog.d/hb-universal-credit-passport.fixed.md @@ -1 +1 @@ -- Disregard the earnings, whole income and whole capital of a Housing Benefit claimant on Universal Credit, as SI 2006/213 Schedule 4 paragraph 12, Schedule 5 paragraph 4 and Schedule 6 paragraph 5 (Northern Ireland: SR 2006/405 Schedule 5 paragraph 12, Schedule 6 paragraph 4 and Schedule 7 paragraph 5) require, so they get maximum Housing Benefit. Being on Universal Credit means being entitled to it, whether it is in payment or not (regulation 2(3B)), which the model reads as Universal Credit above nil before the benefit cap and deductions. The limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). Adds `housing_benefit_on_passporting_benefit`, which the three passports now read. No family receives both benefits in the model yet, so outputs on the Enhanced FRS do not change. +- Disregard the earnings, whole income and whole capital of a Housing Benefit claimant on Universal Credit, as SI 2006/213 Schedule 4 paragraph 12, Schedule 5 paragraph 4 and Schedule 6 paragraph 5 (Northern Ireland: SR 2006/405 Schedule 5 paragraph 12, Schedule 6 paragraph 4 and Schedule 7 paragraph 5) require, so where Housing Benefit is paid alongside Universal Credit the means test gives the maximum. Being on Universal Credit means being entitled to it, whether it is in payment or not (regulation 2(3B)), which the model reads as Universal Credit above nil before the benefit cap and deductions. The limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). Adds `housing_benefit_on_passporting_benefit`, which the three passports now read. No family receives both benefits in the model yet, so outputs on the Enhanced FRS do not change. 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 a89861cb30..f5218d1b0b 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 @@ -1,6 +1,7 @@ - name: Capped at 0 period: 2025 input: + would_claim_uc: false # not on Universal Credit, which passports the means test # Benefits child_benefit: 1_000 income_support: 0 @@ -105,6 +106,7 @@ # Applicable income is floored at nil: 3,250 - 100,000 / 2 < 0. period: 2025 input: + would_claim_uc: false # not on Universal Credit, which passports the means test # Benefits child_benefit: 0 income_support: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml index 4f18f63525..7cf5e90192 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml @@ -35,6 +35,7 @@ employment_income: 156 # £3 × 52 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: housing_benefit_applicable_income_disregard: 156 # min(£5, £3) × 52 @@ -99,6 +100,7 @@ employment_income: 208 # £4 × 52 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant, partner] output: # Para 7: the partner's earnings make up the £10 where the claimant's @@ -118,6 +120,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: housing_benefit_applicable_income_disregard: 1_040 # min(£25, £20) × 52 @@ -245,6 +248,7 @@ weekly_hours: 30 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] output: # Para 17(1): net earnings "equal or exceed" £5 × 52 + £0 childcare + @@ -318,6 +322,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: # Para 17(1): £2,189.20 "equal or exceed" £1,300 + £0 childcare + £889.20. @@ -463,6 +468,7 @@ employment_income: 1_040 # £20 a week benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [person] income_support: 3_000 output: @@ -480,6 +486,7 @@ employment_income: 3_000 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [claimant, partner] esa_income: 4_000 output: @@ -501,6 +508,7 @@ employment_income: 2_000 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: housing_benefit_net_earnings: 0 @@ -522,6 +530,7 @@ age: 5 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, young_person, child] output: # A qualifying young person is a "young person" (SI 2006/213 reg diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_eligible.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_eligible.yaml index b00f14f60d..3a1990c8a0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_eligible.yaml @@ -1,7 +1,7 @@ - name: Social housing eligible period: 2021 - absolute_error_margin: 5 input: + would_claim_uc: false # continuing award route: not claiming Universal Credit in_social_housing: true housing_benefit_reported: 1 LHA_eligible: false @@ -10,8 +10,8 @@ - name: LHA eligible period: 2021 - absolute_error_margin: 5 input: + would_claim_uc: false # continuing award route: not claiming Universal Credit in_social_housing: false housing_benefit_reported: 1 LHA_eligible: true @@ -20,8 +20,8 @@ - name: Not already enrolled period: 2021 - absolute_error_margin: 5 input: + would_claim_uc: false # continuing award route: not claiming Universal Credit in_social_housing: false housing_benefit_reported: 0 LHA_eligible: true @@ -30,8 +30,8 @@ - name: Not eligible period: 2021 - absolute_error_margin: 5 input: + would_claim_uc: false # continuing award route: not claiming Universal Credit in_social_housing: false housing_benefit_reported: 1 LHA_eligible: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml index df0be17e6d..4c7eabd6eb 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_universal_credit_passport.yaml @@ -32,6 +32,8 @@ claimant: age: 40 employment_income: 8_000 + # Under 30 hours: no additional earnings disregard (Sch 4 para 17). + weekly_hours: 12 housing_benefit_reported: 1 benunits: benunit: @@ -75,6 +77,8 @@ claimant: age: 40 employment_income: 8_000 + # Under 30 hours: no additional earnings disregard (Sch 4 para 17). + weekly_hours: 12 housing_benefit_reported: 1 benunits: benunit: @@ -172,6 +176,8 @@ claimant: age: 30 employment_income: 3_000 + # Under 30 hours: no additional earnings disregard (Sch 4 para 17). + weekly_hours: 6 housing_benefit_reported: 1 benunits: benunit: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml index 0755742aeb..723937bda3 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml @@ -372,6 +372,7 @@ lifetime_isa_balance: 10_000 benunits: family: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, young_person] households: household: @@ -400,6 +401,7 @@ lifetime_isa_balance: 10_000 benunits: family: + would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, dependant] households: household: @@ -512,6 +514,7 @@ lifetime_isa_balance: 10_000 benunits: benunit: + would_claim_uc: false # not on Universal Credit, which passports the means test members: person households: household: diff --git a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py index 34b3c49c58..0db11ccbd2 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_pensioner_earnings_disregard_properties.py @@ -34,8 +34,9 @@ every year from 2015 to 2030, at working and pension age: £5, £10 or £25 (SI 2006/213 Sch 4 paras 4, 7, 10; SI 2006/214 Sch 4 paras 2, 7), plus £17.10 (£37.10 in 2020, SI 2020/371 reg 5) where a work condition holds, - for families with enough earnings and no Income Support, income-based JSA - or income-related ESA (whose working-age earnings are all disregarded). + for families with enough earnings and on none of Universal Credit, Income + Support, income-based JSA or income-related ESA (whose working-age + earnings are all disregarded, Sch 4 para 12). """ import numpy as np @@ -129,6 +130,7 @@ def situation(units, year, earnings_bump=0.0, no_income_related_benefits=False): if no_income_related_benefits: for benefit in ("income_support", "jsa_income", "esa_income"): benunits[f"b{i}"][benefit] = {year: 0.0} + benunits[f"b{i}"]["would_claim_uc"] = {year: False} households[f"h{i}"] = { "members": names, "country": {year: unit["nation"]}, diff --git a/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py index 690f1dd5e9..e7d2d7143a 100644 --- a/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_universal_credit_passport_properties.py @@ -37,9 +37,12 @@ Great Britain and from 2019 in Northern Ireland (in force 28 October 2013 and 8 May 2018; model years are read on 30 April), and never before. 7. Order independence: with Housing Benefit and Universal Credit both paid, - calculating Housing Benefit, Universal Credit, the benefit cap or the - passport first gives the same results, so the passport (which reads - Universal Credit before the cap) adds no circular dependency. + calculating Housing Benefit, Universal Credit, the benefit cap, the + passport or household net income first gives the same means test, the + same Housing Benefit, Universal Credit and benefit cap reduction for every + benefit unit, and the same household net income. Each population includes + a family whose cap binds, so the passport (which reads Universal Credit + before the cap) adds no circular dependency through the cap. """ import itertools @@ -115,6 +118,24 @@ def families(draw, uc_modes=("off", "supplied", "computed"), legacy=True): ) +# Appended to the order test's population: a lone parent with four children +# paying £18,000 private rent, on Universal Credit (computed) and Housing +# Benefit, whose benefit cap binds. +CAPPED_ON_UNIVERSAL_CREDIT = dict( + adults=[dict(age=30, employment_income=3_000.0, private_pension_income=0.0)], + children=[1, 3, 5, 7], + tenure="RENT_PRIVATELY", + country="ENGLAND", + rent=18_000.0, + savings=0.0, + uc_mode="computed", + uc_amount=None, + legacy={benefit: 0.0 for benefit in LEGACY}, + non_dependant_earnings=None, +) +PAID = ["housing_benefit", "universal_credit", "benefit_cap_reduction"] + + # Appended to every generated population, so that each example has a family # passported by Universal Credit alone, with earnings and capital the passport # must disregard. @@ -223,10 +244,8 @@ def situation(units, bump=0.0, overrides=None): ] -def calculate(units, order=None, **kwargs): +def calculate(units, **kwargs): sim = Simulation(situation=situation(units, **kwargs)) - for variable in order or []: - sim.calculate(variable, YEAR) 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))] @@ -379,18 +398,33 @@ def test_universal_credit_limb_dates(): st.lists(families(uc_modes=("computed",), legacy=False), min_size=1, max_size=10) ) def test_calculation_order_does_not_change_results(units): - orders = [ - ["housing_benefit"], - ["universal_credit"], - ["benefit_cap_reduction"], - ["housing_benefit_on_passporting_benefit"], - ["household_net_income"], - ] - results = [calculate(units, order=order) for order in orders] + units = units + [CAPPED_ON_UNIVERSAL_CREDIT] + capped = f"b{len(units) - 1}" + results = [] + for first in [ + "housing_benefit", + "universal_credit", + "benefit_cap_reduction", + "housing_benefit_on_passporting_benefit", + "household_net_income", + ]: + sim = Simulation(situation=situation(units)) + sim.calculate(first, YEAR) + values = { + v: np.asarray(sim.calculate(v, YEAR), dtype=float) for v in VARIABLES + PAID + } + values["household_net_income"] = np.asarray( + sim.calculate("household_net_income", YEAR), dtype=float + ) + results.append(values) + # The sentinel is passported by Universal Credit and capped, so a + # dependency of the passport on the capped amounts would show here. + i = list(sim.populations["benunit"].ids).index(capped) + assert results[0]["housing_benefit_on_passporting_benefit"][i] == 1 + assert results[0]["benefit_cap_reduction"][i] > 0 + assert results[0]["housing_benefit_pre_benefit_cap"][i] > 0 for other in results[1:]: - for variable in VARIABLES: - assert np.allclose( - results[0][variable].astype(float), - other[variable].astype(float), - atol=0.01, - ), variable + for variable in results[0]: + assert np.allclose(results[0][variable], other[variable], atol=0.01), ( + variable + ) diff --git a/policyengine_uk/tests/test_means_test_income_properties.py b/policyengine_uk/tests/test_means_test_income_properties.py index fcfb8f56a2..49a9088ef2 100644 --- a/policyengine_uk/tests/test_means_test_income_properties.py +++ b/policyengine_uk/tests/test_means_test_income_properties.py @@ -8,6 +8,10 @@ Each example builds families twice in one simulation: once with the other member's income and once without, in separate households and benefit units. +It does so with every family claiming Universal Credit (the default) and with +none claiming it: a family on Universal Credit has its whole Housing Benefit +income disregarded (SI 2006/213 Sch 5 para 4), so the Housing Benefit means +test is only exercised off it. """ import numpy as np @@ -76,7 +80,7 @@ def families(draw): return family -def situation(families_): +def situation(families_, would_claim_uc=None): people, benunits, households = {}, {}, {} for i, family in enumerate(families_): names = [] @@ -85,6 +89,8 @@ def situation(families_): people[name] = {k: {YEAR: v} for k, v in inputs.items()} names.append(name) benunits[f"b{i}"] = {"members": names} + if would_claim_uc is not None: + benunits[f"b{i}"]["would_claim_uc"] = {YEAR: would_claim_uc} households[f"h{i}"] = {"members": names} return {"people": people, "benunits": benunits, "households": households} @@ -109,15 +115,20 @@ def test_income_of_non_claimants_never_counts(units): ] for family in units ] - sim = Simulation(situation=situation(units + without)) - offsets = np.cumsum([0] + [len(f) for f in units]) - n = len(units) - for variable in FAMILY_FLAGS: - flags = sim.calculate(variable, YEAR) - for i in range(n): - # The generator's construction, checked against the model. - assert not flags[offsets[i + 1] - 1], (variable, units[i]) - for variable in INCOME_TESTS: - values = sim.calculate(variable, YEAR) - for i in range(n): - assert abs(values[i] - values[n + i]) < 0.01, (variable, units[i]) + for would_claim_uc in (None, False): + sim = Simulation(situation=situation(units + without, would_claim_uc)) + offsets = np.cumsum([0] + [len(f) for f in units]) + n = len(units) + for variable in FAMILY_FLAGS: + flags = sim.calculate(variable, YEAR) + for i in range(n): + # The generator's construction, checked against the model. + assert not flags[offsets[i + 1] - 1], (variable, units[i]) + for variable in INCOME_TESTS: + values = sim.calculate(variable, YEAR) + for i in range(n): + assert abs(values[i] - values[n + i]) < 0.01, ( + variable, + would_claim_uc, + units[i], + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index 820205ea62..b685ace384 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -17,7 +17,12 @@ class housing_benefit_on_passporting_benefit(Variable): "(would_claim_uc), so a Universal Credit award that the cap or " "deductions reduce to nil still counts. The model tests the year " "rather than each day, and both members of a joint claim are on " - "Universal Credit. The universal credit limb applies from 28 October " + "Universal Credit. There is no age condition: the working-age " + "regulations apply to a claimant over the qualifying age for State " + "Pension Credit whose partner is on one of these benefits (reg " + "5(1)(b)), and the model's Universal Credit needs a claimant or " + "partner under State Pension age in any case (is_uc_eligible). The " + "universal credit limb applies from 28 October " "2013 in Great Britain and 8 May 2018 in Northern Ireland " "(gov.dwp.housing_benefit.means_test.universal_credit_passport)." ) From 1d478e2760bb9c6e9b78853d55350348aa37e5ad Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 13:25:52 -0400 Subject: [PATCH 4/4] Name the State Pension Credit qualifying age in the passport docstring is_uc_eligible tests has_attained_state_pension_credit_qualifying_age since #1907, not State Pension age. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit/housing_benefit_on_passporting_benefit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index b685ace384..b0b3a3edc8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -21,7 +21,8 @@ class housing_benefit_on_passporting_benefit(Variable): "regulations apply to a claimant over the qualifying age for State " "Pension Credit whose partner is on one of these benefits (reg " "5(1)(b)), and the model's Universal Credit needs a claimant or " - "partner under State Pension age in any case (is_uc_eligible). The " + "partner under the qualifying age for State Pension Credit in any " + "case (is_uc_eligible). The " "universal credit limb applies from 28 October " "2013 in Great Britain and 8 May 2018 in Northern Ireland " "(gov.dwp.housing_benefit.means_test.universal_credit_passport)."