From 46214f11234e61a12aa8cce24f785443bf59c91e Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 08:46:43 -0400 Subject: [PATCH 1/5] Key the Housing Benefit Guarantee Credit passport on receipt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit housing_benefit_assessable_capital, housing_benefit_applicable_income and housing_benefit_tariff_income disregarded all income and capital whenever guarantee_credit was positive for a family with someone over State Pension age. guarantee_credit is computed whether or not the family claims Pension Credit (would_claim_pc) or can get it (is_pension_credit_eligible, false for mixed-age couples). So pensioners who do not take up Pension Credit, and mixed-age couples with a continuing Housing Benefit award, had their capital ignored and kept Housing Benefit above the £16,000 capital limit. SI 2006/214 reg 26 (NI SR 2006/406 reg 24) disregards the whole of the capital and income of "a claimant who is in receipt, or whose partner is in receipt, of a guarantee credit". The new in_receipt_of_guarantee_credit is true when the Pension Credit paid to the benefit unit includes a guarantee credit (pension_credit > 0 and guarantee_credit > 0). Under the Pension Credit freeze it keeps the baseline receipt, because the frozen award is the baseline award. The three Housing Benefit formulas now use it. The existing passport case in housing_benefit_capital.yaml set guarantee_credit 1 for a pensioner whose Pension Credit income exceeds the minimum guarantee, so no Pension Credit was paid; it now also sets is_pension_credit_eligible. Also documents the passport on the Housing Benefit docs page. Co-Authored-By: Claude Opus 5.5 --- changelog.d/hb-gc-passport-receipt.fixed.md | 1 + .../programs/gov/dwp/housing-benefit.ipynb | 2 +- .../housing_benefit_capital.yaml | 5 + ...ing_benefit_guarantee_credit_passport.yaml | 275 ++++++++++++++++++ .../in_receipt_of_guarantee_credit.yaml | 148 ++++++++++ ...it_guarantee_credit_passport_properties.py | 242 +++++++++++++++ .../housing_benefit_applicable_income.py | 5 +- .../housing_benefit_tariff_income.py | 5 +- .../housing_benefit_assessable_capital.py | 5 +- .../in_receipt_of_guarantee_credit.py | 33 +++ 10 files changed, 714 insertions(+), 7 deletions(-) create mode 100644 changelog.d/hb-gc-passport-receipt.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml create mode 100644 policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py diff --git a/changelog.d/hb-gc-passport-receipt.fixed.md b/changelog.d/hb-gc-passport-receipt.fixed.md new file mode 100644 index 0000000000..d6e5e21c18 --- /dev/null +++ b/changelog.d/hb-gc-passport-receipt.fixed.md @@ -0,0 +1 @@ +Key the Housing Benefit Guarantee Credit passport on a guarantee credit that is received (SI 2006/214 reg 26; NI SR 2006/406 reg 24), through a new `in_receipt_of_guarantee_credit` variable. Income and capital were disregarded whenever `guarantee_credit` was computed, including for pensioners who do not claim Pension Credit and for mixed-age couples who cannot get it; those families now have their income and capital assessed, so capital over £16,000 ends their Housing Benefit. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 4f928b173d..818e39a521 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -40,7 +40,7 @@ "\n", "1. **Eligible rent** (`benunit_rent`): the contracted rent for the property, capped at the Local Housing Allowance rate where the LHA applies.\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", - "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household.\n", + "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household. If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: Pension Credit is paid and includes a guarantee credit), the whole of their income and capital is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)), so applicable income, tariff income and assessable capital are zero and the capital limit cannot bite. A guarantee credit computed for a family that does not claim Pension Credit, or cannot get it (such as a mixed-age couple), does not count.\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/housing_benefit_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_capital.yaml index e9a0cd47c3..b2670e9a92 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 @@ -234,7 +234,12 @@ benunits: benunit: members: person + # The passport needs a guarantee credit that is paid. Pension Credit + # income here is 10,000 plus 80 x 52 = 4,160 deemed from capital, + # 14,160 in all, above the 2025 minimum guarantee of 11,536.20, so + # Pension Credit eligibility is set along with the credit. guarantee_credit: 1 + is_pension_credit_eligible: true would_claim_uc: false housing_benefit_applicable_income_disregard: 0 housing_benefit_applicable_income_childcare_element: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml new file mode 100644 index 0000000000..42740e2ab6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml @@ -0,0 +1,275 @@ +# Guarantee Credit passport: "In the case of a claimant who is in receipt, or +# whose partner is in receipt, of a guarantee credit, the whole of his capital +# and income shall be disregarded." (SI 2006/214 reg 26; NI: SR 2006/406 +# reg 24.) The passport follows the guarantee credit actually paid, not one +# computed for a family that does not claim Pension Credit or cannot get it. +# +# 2026 parameters: Pension Credit standard minimum guarantee 12,376 a year +# single and 18,889 couple; Pension Credit and pension-age Housing Benefit +# both count £1 a week for each £500 (or part) of capital over £10,000; +# Housing Benefit capital limit £16,000; aged personal allowance £256.00 a +# week single (13,312) and £383.35 couple (19,934.20); taper 65%. +# +# Robustness to the other two Housing Benefit means-test fixes: the tenants +# rent from the council, so the Local Housing Allowance order (fix 3) cannot +# apply; cases whose expected Housing Benefit depends on applicable income set +# housing_benefit_applicable_income_disregard to 0 (the value fix 2 gives a +# family with no earnings), and the other cases do not depend on it. + +- name: Case 1, pensioner not claiming Pension Credit has capital over 16,000 counted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: false + households: + household: + members: [person1] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + # Computed guarantee credit 12,376 - (10,000 + 20 x 52 = 1,040) = 1,336, + # but none is paid, so there is no passport. + guarantee_credit: 1_336 + pension_credit: 0 + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 20_000 + # (20,000 - 10,000) / 500 = 20 steps; 20 x 52 = 1,040. + housing_benefit_tariff_income: 1_040 + # 20,000 is over the 16,000 limit. + housing_benefit_eligible: false + housing_benefit: 0 + +- name: Case 2, the same pensioner claiming Pension Credit is passported. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: true + households: + household: + members: [person1] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + guarantee_credit: 1_336 + pension_credit: 1_336 + in_receipt_of_guarantee_credit: true + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + housing_benefit_applicable_income: 0 + housing_benefit_eligible: true + # No income counted, so the full eligible rent. + housing_benefit: 6_240 + +- name: Case 3, pensioner couple not receiving a guarantee credit is tapered on tariff income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + state_pension_reported: 9_800 + person2: + age: 68 + state_pension_reported: 9_800 + benunits: + benunit: + members: [person1, person2] + # The guarantee credit and Pension Credit eligibility are set so the + # case tests only whether it is paid. Each state pension is below the + # personal allowance, so no income tax. + guarantee_credit: 500 + is_pension_credit_eligible: true + would_claim_pc: false + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1, person2] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 15_000 + output: + pension_credit: 0 + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 15_000 + # (15,000 - 10,000) / 500 = 10 steps; 10 x 52 = 520. + housing_benefit_tariff_income: 520 + # 9,800 x 2 + 520 = 20,120. + housing_benefit_applicable_income: 20_120 + housing_benefit_applicable_amount: 19_934.20 + # Without the tariff income, 19,600 is below the applicable amount. + # 7,800 - 0.65 x (20,120 - 19,934.20) = 7,800 - 120.77 = 7,679.23. + housing_benefit: 7_679.23 + +- name: Case 4, the same couple receiving the guarantee credit has income and capital disregarded. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + state_pension_reported: 9_800 + person2: + age: 68 + state_pension_reported: 9_800 + benunits: + benunit: + members: [person1, person2] + guarantee_credit: 500 + is_pension_credit_eligible: true + would_claim_pc: true + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1, person2] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 15_000 + output: + # A person reaching State Pension age after April 2016 gets no Savings + # Credit, so Pension Credit is the guarantee credit of 500. + pension_credit: 500 + in_receipt_of_guarantee_credit: true + housing_benefit_assessable_capital: 0 + housing_benefit_tariff_income: 0 + housing_benefit_applicable_income: 0 + housing_benefit: 7_800 + +- name: Case 5, mixed-age couple with a continuing award is not passported. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + housing_benefit_reported: 1 + person2: + age: 60 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + would_claim_pc: true + households: + household: + members: [person1, person2] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + # Computed guarantee credit 18,889 - (10,000 + 1,040) = 7,849, but a + # couple with a partner under State Pension age is not eligible for + # Pension Credit, so none is paid. + guarantee_credit: 7_849 + is_pension_credit_eligible: false + pension_credit: 0 + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 20_000 + housing_benefit_tariff_income: 1_040 + housing_benefit_eligible: false + housing_benefit: 0 + +- name: Case 6, Savings Credit only does not passport Housing Benefit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 90 + state_pension_reported: 11_440 + benunits: + benunit: + members: [person1] + would_claim_pc: true + households: + household: + members: [person1] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + # Pension Credit income 11,440 + 1,040 = 12,480 exceeds 12,376, so no + # guarantee credit. Savings Credit (threshold 10,819.64): maximum + # 0.6 x (12,376 - 10,819.64) = 933.816, less 0.4 x (12,480 - 12,376) + # = 41.60, = 892.216. + guarantee_credit: 0 + savings_credit: 892.22 + pension_credit: 892.22 + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 20_000 + housing_benefit_eligible: false + housing_benefit: 0 + +- name: Case 7, guarantee credit recipient couple with capital over 16,000 keeps Housing Benefit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + state_pension_reported: 5_000 + person2: + age: 68 + state_pension_reported: 5_000 + benunits: + benunit: + members: [person1, person2] + would_claim_pc: true + households: + household: + members: [person1, person2] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 30_000 + output: + # (30,000 - 10,000) / 500 = 40 steps; 40 x 52 = 2,080. + # 18,889 - (10,000 + 2,080) = 6,809. + guarantee_credit: 6_809 + pension_credit: 6_809 + in_receipt_of_guarantee_credit: true + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + housing_benefit_eligible: true + housing_benefit: 7_800 + +- name: Case 8, Northern Ireland pensioner not claiming Pension Credit has capital counted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: false + households: + household: + members: [person1] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + region: NORTHERN_IRELAND + output: + guarantee_credit: 1_336 + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 20_000 + housing_benefit_eligible: false + housing_benefit: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml new file mode 100644 index 0000000000..62698a21de --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml @@ -0,0 +1,148 @@ +# Receipt of a guarantee credit (the Housing Benefit passport test in +# SI 2006/214 reg 26; NI: SR 2006/406 reg 24). A guarantee credit is received +# only when the benefit unit is paid Pension Credit and that award includes a +# guarantee credit. +# 2026 parameters: standard minimum guarantee £238.00 a week single and +# £363.25 couple (12,376 and 18,889 a year); Savings Credit threshold £208.07 +# a week single (10,819.64 a year), phase-in 60%, phase-out 40%. + +- name: Case 1, single pensioner claiming Pension Credit receives a guarantee credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: true + households: + household: + members: [person1] + output: + # 12,376 - 10,000 = 2,376. + guarantee_credit: 2_376 + pension_credit: 2_376 + in_receipt_of_guarantee_credit: true + +- name: Case 2, the same pensioner not claiming Pension Credit receives no guarantee credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: false + households: + household: + members: [person1] + output: + # The guarantee credit is still computed, but nothing is paid. + guarantee_credit: 2_376 + pension_credit: 0 + in_receipt_of_guarantee_credit: false + +- name: Case 3, mixed-age couple has a computed guarantee credit but is not eligible for Pension Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + person2: + age: 60 + benunits: + benunit: + members: [person1, person2] + would_claim_pc: true + households: + household: + members: [person1, person2] + output: + # Couple minimum guarantee 18,889 - 10,000 = 8,889. + guarantee_credit: 8_889 + is_pension_credit_eligible: false + pension_credit: 0 + in_receipt_of_guarantee_credit: false + +- name: Case 4, Savings Credit only is not receipt of a guarantee credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + # Reached State Pension age long before April 2016, so can get + # Savings Credit. + age: 90 + state_pension_reported: 12_480 + benunits: + benunit: + members: [person1] + would_claim_pc: true + households: + household: + members: [person1] + output: + # Income 12,480 (240 a week, below the personal allowance, so no income + # tax) exceeds the minimum guarantee of 12,376, so no guarantee credit. + # Savings Credit: maximum 0.6 x (12,376 - 10,819.64) = 933.816; phase-in + # 0.6 x (12,480 - 10,819.64) = 996.216, capped at 933.816; less + # 0.4 x (12,480 - 12,376) = 41.60; = 892.216. + income_tax: 0 + guarantee_credit: 0 + savings_credit: 892.22 + pension_credit: 892.22 + in_receipt_of_guarantee_credit: false + +- name: Case 5, pensioner couple claiming Pension Credit receives a guarantee credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 72 + state_pension_reported: 5_000 + person2: + age: 68 + state_pension_reported: 5_000 + benunits: + benunit: + members: [person1, person2] + would_claim_pc: true + households: + household: + members: [person1, person2] + output: + # 18,889 - 10,000 = 8,889. + guarantee_credit: 8_889 + pension_credit: 8_889 + in_receipt_of_guarantee_credit: true + +- name: Case 6, pensioner with income above the minimum guarantee receives no guarantee credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 12_500 + benunits: + benunit: + members: [person1] + would_claim_pc: true + households: + household: + members: [person1] + output: + # 12,500 exceeds 12,376, and a person who reached State Pension age after + # April 2016 cannot get Savings Credit. + guarantee_credit: 0 + pension_credit: 0 + in_receipt_of_guarantee_credit: false diff --git a/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py new file mode 100644 index 0000000000..3c2978065c --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py @@ -0,0 +1,242 @@ +"""Property-based tests for the Housing Benefit Guarantee Credit passport. + +"In the case of a claimant who is in receipt, or whose partner is in receipt, +of a guarantee credit, the whole of his capital and income shall be +disregarded." (SI 2006/214 reg 26; NI: SR 2006/406 reg 24.) The passport +follows in_receipt_of_guarantee_credit: Pension Credit is paid and includes a +guarantee credit. + +Invariants, for any generated population of families: + +1. Receipt: in_receipt_of_guarantee_credit is true exactly when pension_credit + and guarantee_credit are both positive, so it implies Pension Credit + eligibility and would_claim_pc, and every adult in a receiving family is + over State Pension age. +2. Passport: a receiving family's Housing Benefit applicable income, tariff + income and assessable capital are all zero. +3. Differential: where the family claims and is eligible for Pension Credit, + every Housing Benefit output equals the pre-fix formula (passport on a + computed guarantee credit). The two passports differ exactly for families + with someone over State Pension age and a computed guarantee credit that is + not paid; there the pre-fix formula passports and this one does not + (intended). Housing Benefit itself then differs only when the counted + capital is over the limit or the counted income exceeds the applicable + amount. +4. Metamorphic: not claiming Pension Credit never raises Housing Benefit. +5. Under the Pension Credit freeze, receipt is the baseline receipt, because + the frozen award is the baseline award. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import YEAR as YEAR_PERIOD +from policyengine_uk.model_api import BenUnit, Variable + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA", "RENT_PRIVATELY", "OWNED_OUTRIGHT"] +# State Pension age is 66 until 2026-27 finishes phasing up; 67 and over is +# unambiguously pension age and 60 and under unambiguously working age. +PENSION_AGE = st.integers(67, 100) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single_pension": [PENSION_AGE], + "couple_pension": [PENSION_AGE, PENSION_AGE], + "mixed_age": [PENSION_AGE, WORKING_AGE], + "single_working": [WORKING_AGE], +} +money = st.floats(0, 20_000, allow_nan=False, allow_infinity=False) + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(sorted(SHAPES))) + return dict( + ages=[draw(age) for age in SHAPES[shape]], + tenure=draw(st.sampled_from(TENURES)), + rent=draw(money), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + state_pension=draw(money), + private_pension=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + would_claim_pc=draw(st.booleans()), + hb_reported=draw(st.sampled_from([0.0, 1.0])), + ) + + +POPULATIONS = st.lists(families(), min_size=1, max_size=30) + + +def situation(units, would_claim_pc=None): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, age in enumerate(unit["ages"]): + name = f"p{i}_{j}" + person = {"age": {YEAR: age}} + if age >= 67: + person["state_pension_reported"] = {YEAR: unit["state_pension"]} + person["private_pension_income"] = {YEAR: unit["private_pension"]} + if j == 0 and unit["hb_reported"]: + person["housing_benefit_reported"] = {YEAR: unit["hb_reported"]} + people[name] = person + names.append(name) + claims = unit["would_claim_pc"] if would_claim_pc is None else would_claim_pc + benunits[f"b{i}"] = { + "members": names, + "would_claim_pc": {YEAR: claims}, + "would_claim_uc": {YEAR: False}, + } + households[f"h{i}"] = { + "members": names, + "rent": {YEAR: unit["rent"]}, + "tenure_type": {YEAR: unit["tenure"]}, + "savings": {YEAR: unit["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +class in_receipt_of_guarantee_credit(Variable): + # The passport test on main before this fix: a computed guarantee credit. + # The Housing Benefit formulas still require someone over State Pension + # age, so swapping this in reproduces the old behaviour exactly. + value_type = bool + entity = BenUnit + definition_period = YEAR_PERIOD + label = "computed guarantee credit (pre-fix passport test)" + + def formula(benunit, period, parameters): + return benunit("guarantee_credit", period) > 0 + + +VARIABLES = [ + "in_receipt_of_guarantee_credit", + "pension_credit", + "guarantee_credit", + "is_pension_credit_eligible", + "would_claim_pc", + "housing_benefit", + "housing_benefit_eligible", + "housing_benefit_applicable_income", + "housing_benefit_tariff_income", + "housing_benefit_assessable_capital", +] +HB_OUTPUTS = VARIABLES[5:] + + +def calculate(units, old_formula=False, **kwargs): + sim = Simulation(situation=situation(units, **kwargs)) + if old_formula: + sim.tax_benefit_system.update_variable(in_receipt_of_guarantee_credit) + values = {v: np.asarray(sim.calculate(v, YEAR)) for v in VARIABLES} + values["any_over_sp_age"] = ( + np.asarray(sim.calculate("is_SP_age", YEAR, map_to="benunit")) > 0 + ) + return values + + +def all_adults_over_sp_age(unit): + return all(age >= 67 for age in unit["ages"]) + + +@PROPERTY_SETTINGS +@given(POPULATIONS) +def test_receipt_means_a_paid_guarantee_credit_and_passports(units): + values = calculate(units) + receipt = values["in_receipt_of_guarantee_credit"].astype(bool) + paid = (values["pension_credit"] > 0) & (values["guarantee_credit"] > 0) + assert np.array_equal(receipt, paid) + assert not np.any(receipt & ~values["is_pension_credit_eligible"].astype(bool)) + assert not np.any(receipt & ~values["would_claim_pc"].astype(bool)) + for i, unit in enumerate(units): + if receipt[i]: + assert all_adults_over_sp_age(unit), unit + for variable in [ + "housing_benefit_applicable_income", + "housing_benefit_tariff_income", + "housing_benefit_assessable_capital", + ]: + assert np.all(values[variable][receipt] == 0), variable + + +@PROPERTY_SETTINGS +@given(POPULATIONS) +def test_matches_the_old_formula_when_pension_credit_is_claimed(units): + new = calculate(units) + old = calculate(units, old_formula=True) + claimed = new["would_claim_pc"].astype(bool) & new[ + "is_pension_credit_eligible" + ].astype(bool) + for variable in HB_OUTPUTS: + assert np.allclose(new[variable][claimed], old[variable][claimed], atol=0.01), ( + variable + ) + old_passport = old["any_over_sp_age"] & old["in_receipt_of_guarantee_credit"] + new_passport = new["any_over_sp_age"] & new["in_receipt_of_guarantee_credit"] + # Intended difference: a computed guarantee credit that is not paid. + computed_not_paid = ( + new["any_over_sp_age"] + & (new["guarantee_credit"] > 0) + & ~(new["pension_credit"] > 0) + ) + assert np.array_equal(old_passport & ~new_passport, computed_not_paid) + assert not np.any(new_passport & ~old_passport) + assert not np.any(computed_not_paid & claimed) + + +@PROPERTY_SETTINGS +@given(POPULATIONS) +def test_not_claiming_pension_credit_never_raises_housing_benefit(units): + claiming = calculate(units, would_claim_pc=True) + not_claiming = calculate(units, would_claim_pc=False) + assert not np.any(not_claiming["in_receipt_of_guarantee_credit"]) + assert np.all(not_claiming["housing_benefit"] <= claiming["housing_benefit"] + 0.01) + + +def test_pension_credit_freeze_keeps_the_baseline_receipt(): + # A pensioner with capital over £16,000 receives a guarantee credit of + # 12,376 - (10,000 + 1,040) = 1,336 in the baseline. A reform setting the + # single minimum guarantee to zero ends it; with Pension Credit frozen the + # baseline award, guarantee credit included, is still paid. + household = { + "people": { + "person": { + "age": {YEAR: 70}, + "state_pension_reported": {YEAR: 10_000}, + } + }, + "benunits": {"benunit": {"members": ["person"]}}, + "households": { + "household": { + "members": ["person"], + "rent": {YEAR: 6_240}, + "tenure_type": {YEAR: "RENT_FROM_COUNCIL"}, + "savings": {YEAR: 20_000}, + } + }, + } + no_minimum_guarantee = { + "gov.dwp.pension_credit.guarantee_credit.minimum_guarantee.SINGLE": 0 + } + frozen = Simulation( + situation=household, + reform={**no_minimum_guarantee, "gov.contrib.freeze_pension_credit": True}, + ) + assert frozen.calculate("guarantee_credit", YEAR)[0] == 0 + assert abs(frozen.calculate("pension_credit", YEAR)[0] - 1_336) < 0.01 + assert frozen.calculate("in_receipt_of_guarantee_credit", YEAR)[0] + assert frozen.calculate("housing_benefit_assessable_capital", YEAR)[0] == 0 + assert abs(frozen.calculate("housing_benefit", YEAR)[0] - 6_240) < 0.01 + + unfrozen = Simulation(situation=household, reform=no_minimum_guarantee) + assert unfrozen.calculate("pension_credit", YEAR)[0] == 0 + assert not unfrozen.calculate("in_receipt_of_guarantee_credit", YEAR)[0] + assert unfrozen.calculate("housing_benefit_assessable_capital", YEAR)[0] == 20_000 + assert unfrozen.calculate("housing_benefit", YEAR)[0] == 0 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 999da75d2a..b16ad02e3b 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 @@ -63,5 +63,6 @@ def formula(benunit, period, parameters): - disregard - childcare_element, ) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) - return where(guarantee_credit, 0, applicable_income) + # SI 2006/214 reg 26 (NI: SR 2006/406 reg 24). + passported = any_over_SP_age & benunit("in_receipt_of_guarantee_credit", period) + return where(passported, 0, applicable_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py index 70b6c8bbc4..d313761eff 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py @@ -12,7 +12,8 @@ class housing_benefit_tariff_income(Variable): def formula(benunit, period, parameters): capital = benunit("housing_benefit_assessable_capital", period) any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) + # SI 2006/214 reg 26 (NI: SR 2006/406 reg 24). + passported = any_over_SP_age & benunit("in_receipt_of_guarantee_credit", period) p = parameters(period).gov.dwp.housing_benefit.means_test.capital threshold = where( any_over_SP_age, @@ -32,4 +33,4 @@ def formula(benunit, period, parameters): excess_capital = max_(0, capital - threshold) steps = np.ceil(excess_capital / step) tariff_income = steps * amount * WEEKS_IN_YEAR - return where(guarantee_credit, 0, tariff_income) + return where(passported, 0, tariff_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py index c978c5f699..6b087a0842 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 @@ -29,5 +29,6 @@ def formula(benunit, period, parameters): household_capital * benunit_adults / adult_divisor, 0, ) - guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) - return where(guarantee_credit, 0, max_(0, household_capital_proxy)) + # SI 2006/214 reg 26 (NI: SR 2006/406 reg 24). + passported = any_over_SP_age & benunit("in_receipt_of_guarantee_credit", period) + return where(passported, 0, max_(0, household_capital_proxy)) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py new file mode 100644 index 0000000000..44d5e6d579 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class in_receipt_of_guarantee_credit(Variable): + label = "in receipt of Guarantee Credit" + documentation = ( + "Whether the claimant or partner in this benefit unit is in receipt of " + "a guarantee credit: the Pension Credit paid to the benefit unit " + "includes a Guarantee Credit. A Guarantee Credit computed for a family " + "that does not claim Pension Credit, or is not eligible for it (for " + "example a mixed-age couple), is not received. Under the Pension " + "Credit freeze the baseline receipt is kept, because the frozen award " + "is the baseline award." + ) + entity = BenUnit + definition_period = YEAR + value_type = bool + reference = ( + "https://www.legislation.gov.uk/uksi/2006/214/regulation/26", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/24", + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + ) + + def formula(benunit, period, parameters): + freeze = parameters(period).gov.contrib.freeze_pension_credit + baseline = benunit.simulation.baseline + if freeze and baseline is not None: + return baseline.populations["benunit"]( + "in_receipt_of_guarantee_credit", period + ) + pension_credit = benunit("pension_credit", period) + guarantee_credit = benunit("guarantee_credit", period) + return (pension_credit > 0) & (guarantee_credit > 0) From 2a507afba7db4408adc3a89d9309de7b7bb61060 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 08:49:47 -0400 Subject: [PATCH 2/5] Move the passport note to the Housing Benefit eligibility section The Local Housing Allowance fix rewrites the means-test line of the docs page, so a note appended there would conflict. Co-Authored-By: Claude Opus 5.5 --- docs/book/programs/gov/dwp/housing-benefit.ipynb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 818e39a521..682aab7259 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -26,6 +26,8 @@ "\n", "The pension-age test counts the same benunit members as Universal Credit's working-age test, and the continuing-award route requires not claiming Universal Credit, so no benunit receives both. Working-age awards outside specified or temporary accommodation ended on 1 July 2026 in Great Britain ([SI 2025/1148 art. 7](https://www.legislation.gov.uk/uksi/2025/1148/article/7)); PolicyEngine does not yet model that. Claims for specified or temporary accommodation, which remain open at any age, are not modelled.\n", "\n", + "**Guarantee Credit passport.** If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: the Pension Credit paid to the benunit includes a guarantee credit), the whole of their capital and income is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)). `housing_benefit_assessable_capital`, `housing_benefit_tariff_income` and `housing_benefit_applicable_income` are then zero, so the capital limit cannot bite and no income is tapered. A guarantee credit computed for a family that does not claim Pension Credit (`would_claim_pc`), or cannot get it (for example a mixed-age couple), does not count.\n", + "\n", "Take-up is handled by `would_claim_housing_benefit`, which is true for benunits that report Housing Benefit and, when no reported benefits are supplied (as in household calculations), for everyone. In the Enhanced FRS take-up therefore follows reported claims." ] }, @@ -40,7 +42,7 @@ "\n", "1. **Eligible rent** (`benunit_rent`): the contracted rent for the property, capped at the Local Housing Allowance rate where the LHA applies.\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", - "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household. If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: Pension Credit is paid and includes a guarantee credit), the whole of their income and capital is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)), so applicable income, tariff income and assessable capital are zero and the capital limit cannot bite. A guarantee credit computed for a family that does not claim Pension Credit, or cannot get it (such as a mixed-age couple), does not count.\n", + "3. **Means test** (`housing_benefit_entitlement`): the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is tapered against the eligible rent at the published `withdrawal_rate` (currently 0.65). The result is then reduced by `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household.\n", "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`." From 8bad05c9f02febfd8fcf46779e22bdf830016814 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 09:54:00 -0400 Subject: [PATCH 3/5] Use the Guarantee Credit receipt variable shared with the CTR fix PolicyEngine/policyengine-uk#1909 (pensioner Council Tax Reduction) adds the same in_receipt_of_guarantee_credit file. Both branches now carry it byte-identical, so either can merge first without a conflict. Its formula, is_pension_credit_eligible & would_claim_pc & (guarantee_credit > 0), equals the previous (pension_credit > 0) & (guarantee_credit > 0) whenever pension_credit is computed: pension_credit is (guarantee_credit + savings_credit) x is_pension_credit_eligible x would_claim_pc, and savings_credit is never negative. The Enhanced FRS stores neither pension_credit nor guarantee_credit, and a re-run gives arrays identical to the previous run. The Housing Benefit formulas keep their SI 2006/214 reg 26 comments. Co-Authored-By: Claude Opus 5.5 --- .../programs/gov/dwp/housing-benefit.ipynb | 2 +- .../in_receipt_of_guarantee_credit.py | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 682aab7259..0e6ae862eb 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -26,7 +26,7 @@ "\n", "The pension-age test counts the same benunit members as Universal Credit's working-age test, and the continuing-award route requires not claiming Universal Credit, so no benunit receives both. Working-age awards outside specified or temporary accommodation ended on 1 July 2026 in Great Britain ([SI 2025/1148 art. 7](https://www.legislation.gov.uk/uksi/2025/1148/article/7)); PolicyEngine does not yet model that. Claims for specified or temporary accommodation, which remain open at any age, are not modelled.\n", "\n", - "**Guarantee Credit passport.** If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: the Pension Credit paid to the benunit includes a guarantee credit), the whole of their capital and income is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)). `housing_benefit_assessable_capital`, `housing_benefit_tariff_income` and `housing_benefit_applicable_income` are then zero, so the capital limit cannot bite and no income is tapered. A guarantee credit computed for a family that does not claim Pension Credit (`would_claim_pc`), or cannot get it (for example a mixed-age couple), does not count.\n", + "**Guarantee Credit passport.** If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: the benunit is eligible for Pension Credit, claims it, and its guarantee credit is positive, so the Pension Credit paid includes a guarantee credit), the whole of their capital and income is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)). `housing_benefit_assessable_capital`, `housing_benefit_tariff_income` and `housing_benefit_applicable_income` are then zero, so the capital limit cannot bite and no income is tapered. A guarantee credit computed for a family that does not claim Pension Credit (`would_claim_pc`), or cannot get it (for example a mixed-age couple), does not count.\n", "\n", "Take-up is handled by `would_claim_housing_benefit`, which is true for benunits that report Housing Benefit and, when no reported benefits are supplied (as in household calculations), for everyone. In the Enhanced FRS take-up therefore follows reported claims." ] diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py index 44d5e6d579..69b5a7e090 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py @@ -5,20 +5,19 @@ class in_receipt_of_guarantee_credit(Variable): label = "in receipt of Guarantee Credit" documentation = ( "Whether the claimant or partner in this benefit unit is in receipt of " - "a guarantee credit: the Pension Credit paid to the benefit unit " - "includes a Guarantee Credit. A Guarantee Credit computed for a family " - "that does not claim Pension Credit, or is not eligible for it (for " - "example a mixed-age couple), is not received. Under the Pension " - "Credit freeze the baseline receipt is kept, because the frozen award " - "is the baseline award." + "a guarantee credit: the benefit unit is eligible for Pension Credit, " + "claims it, and its Guarantee Credit is positive. A Guarantee Credit " + "computed for a family that does not claim Pension Credit, or is not " + "eligible for it (for example a working-age or mixed-age family), is " + "not received. Under the Pension Credit freeze the baseline receipt is " + "kept, because the frozen award is the baseline award." ) entity = BenUnit definition_period = YEAR value_type = bool reference = ( - "https://www.legislation.gov.uk/uksi/2006/214/regulation/26", - "https://www.legislation.gov.uk/nisr/2006/406/regulation/24", "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/2002/16/section/2", ) def formula(benunit, period, parameters): @@ -28,6 +27,7 @@ def formula(benunit, period, parameters): return baseline.populations["benunit"]( "in_receipt_of_guarantee_credit", period ) - pension_credit = benunit("pension_credit", period) + eligible = benunit("is_pension_credit_eligible", period) + would_claim = benunit("would_claim_pc", period) guarantee_credit = benunit("guarantee_credit", period) - return (pension_credit > 0) & (guarantee_credit > 0) + return eligible & would_claim & (guarantee_credit > 0) From 58ee875c7200ffa47be1500661583c892d407840 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 10:25:18 -0400 Subject: [PATCH 4/5] =?UTF-8?q?Test=20the=20Housing=20Benefit=20capital=20?= =?UTF-8?q?limit=20at=20and=20just=20over=20=C2=A316,000?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pensioner not claiming Pension Credit with capital of exactly £16,000 keeps Housing Benefit, since SI 2006/214 reg 43 and SSCBA 1992 s.134(1) bar entitlement only where capital "exceeds" the prescribed amount; at £16,000.01 there is no entitlement. Both fail on main, where the computed guarantee credit passports the capital. The rulespec-uk companion tests use the same two cases. Co-Authored-By: Claude Opus 5.5 --- ...ing_benefit_guarantee_credit_passport.yaml | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml index 42740e2ab6..d5c6cd18a7 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml @@ -273,3 +273,58 @@ housing_benefit_assessable_capital: 20_000 housing_benefit_eligible: false housing_benefit: 0 + +- name: Case 9, pensioner not claiming Pension Credit with capital of exactly 16,000 is within the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: false + households: + household: + members: [person1] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 16_000 + output: + # (16,000 - 10,000) / 500 = 12 steps; 12 x 52 = 624. Computed guarantee + # credit 12,376 - (10,000 + 624) = 1,752, not paid. + guarantee_credit: 1_752 + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 16_000 + housing_benefit_tariff_income: 624 + # The limit bites only where capital "exceeds" 16,000. + housing_benefit_eligible: true + # Applicable income is at most 10,000 + 624 = 10,624, below 13,312, so + # no taper whatever the earnings disregard. + housing_benefit: 6_240 + +- name: Case 10, the same pensioner with capital just over 16,000 is not eligible. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + state_pension_reported: 10_000 + benunits: + benunit: + members: [person1] + would_claim_pc: false + households: + household: + members: [person1] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 16_000.01 + output: + in_receipt_of_guarantee_credit: false + housing_benefit_assessable_capital: 16_000.01 + housing_benefit_eligible: false + housing_benefit: 0 From d8c24d914531e755268a63428f78181cd555f86c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 30 Sep 2026 11:50:59 -0400 Subject: [PATCH 5/5] Say the mixed-age passport loss follows the model, not the law SI 2019/37 art 4 disapplies SPCA 2002 s.4(1A) for a mixed-age couple entitled to Pension Credit or Housing Benefit as that couple on 14 May 2019, and PolicyEngine gives Housing Benefit to a mixed-age couple only on a continuing award, which housing_benefit_eligible treats as standing in for that saving. is_pension_credit_eligible omits the saving, so it pays those couples no Pension Credit and the passport now drops for them, although in law they may be entitled to a guarantee credit. The mixed-age test cases, docs paragraph and changelog now say so instead of calling the change a legal correction. The two mixed-age cases no longer assert the computed guarantee credit, which PolicyEngine/policyengine-uk#1896 makes 0 for a family not eligible for Pension Credit; their passport and Housing Benefit assertions stay. in_receipt_of_guarantee_credit takes the shared v2 text also carried by #1909: documentation and references only (SSAA 1992 s.1, SI 2019/37 art 4). The three Housing Benefit call sites now cite reg 26, NI reg 24 and reg 2(5), under which "in receipt" includes a person who would be but for SPC Regs 2002 reg 13, which PolicyEngine does not model. The tariff-income passport, redundant with the zero assessable capital, is kept for parallelism with reg 26's "capital and income" and says so. The capital test's comment calls 11,536.20 PolicyEngine's 2025 parameter, not the minimum guarantee. Co-Authored-By: Claude Opus 5.5 --- changelog.d/hb-gc-passport-receipt.fixed.md | 2 +- .../programs/gov/dwp/housing-benefit.ipynb | 2 +- .../housing_benefit_capital.yaml | 5 +++-- ...ing_benefit_guarantee_credit_passport.yaml | 21 +++++++++++++------ .../in_receipt_of_guarantee_credit.yaml | 9 +++++--- ...it_guarantee_credit_passport_properties.py | 8 ++++--- .../housing_benefit_applicable_income.py | 8 ++++++- .../housing_benefit_tariff_income.py | 11 +++++++++- .../housing_benefit_assessable_capital.py | 8 ++++++- .../in_receipt_of_guarantee_credit.py | 14 +++++++++---- 10 files changed, 65 insertions(+), 23 deletions(-) diff --git a/changelog.d/hb-gc-passport-receipt.fixed.md b/changelog.d/hb-gc-passport-receipt.fixed.md index d6e5e21c18..b3ede03b6d 100644 --- a/changelog.d/hb-gc-passport-receipt.fixed.md +++ b/changelog.d/hb-gc-passport-receipt.fixed.md @@ -1 +1 @@ -Key the Housing Benefit Guarantee Credit passport on a guarantee credit that is received (SI 2006/214 reg 26; NI SR 2006/406 reg 24), through a new `in_receipt_of_guarantee_credit` variable. Income and capital were disregarded whenever `guarantee_credit` was computed, including for pensioners who do not claim Pension Credit and for mixed-age couples who cannot get it; those families now have their income and capital assessed, so capital over £16,000 ends their Housing Benefit. +Key the Housing Benefit Guarantee Credit passport on a guarantee credit that is received (SI 2006/214 reg 26; NI SR 2006/406 reg 24), through a new `in_receipt_of_guarantee_credit` variable. Income and capital were disregarded whenever `guarantee_credit` was computed, including for pensioners who do not claim Pension Credit and for mixed-age couples, whom `is_pension_credit_eligible` pays no Pension Credit; those families now have their income and capital assessed, so capital over £16,000 ends their Housing Benefit. `is_pension_credit_eligible` omits the SI 2019/37 art 4 saving, so mixed-age couples it saves, who may be entitled to Pension Credit in law, lose the passport until that saving is modelled. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 0e6ae862eb..398f4e6ad2 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -26,7 +26,7 @@ "\n", "The pension-age test counts the same benunit members as Universal Credit's working-age test, and the continuing-award route requires not claiming Universal Credit, so no benunit receives both. Working-age awards outside specified or temporary accommodation ended on 1 July 2026 in Great Britain ([SI 2025/1148 art. 7](https://www.legislation.gov.uk/uksi/2025/1148/article/7)); PolicyEngine does not yet model that. Claims for specified or temporary accommodation, which remain open at any age, are not modelled.\n", "\n", - "**Guarantee Credit passport.** If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: the benunit is eligible for Pension Credit, claims it, and its guarantee credit is positive, so the Pension Credit paid includes a guarantee credit), the whole of their capital and income is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)). `housing_benefit_assessable_capital`, `housing_benefit_tariff_income` and `housing_benefit_applicable_income` are then zero, so the capital limit cannot bite and no income is tapered. A guarantee credit computed for a family that does not claim Pension Credit (`would_claim_pc`), or cannot get it (for example a mixed-age couple), does not count.\n", + "**Guarantee Credit passport.** If the claimant or partner is in receipt of a guarantee credit (`in_receipt_of_guarantee_credit`: the benunit is eligible for Pension Credit, claims it, and its guarantee credit is positive, so the Pension Credit paid includes a guarantee credit), the whole of their capital and income is disregarded ([SI 2006/214 reg. 26](https://www.legislation.gov.uk/uksi/2006/214/regulation/26); Northern Ireland: [SR 2006/406 reg. 24](https://www.legislation.gov.uk/nisr/2006/406/regulation/24)). `housing_benefit_assessable_capital`, `housing_benefit_tariff_income` and `housing_benefit_applicable_income` are then zero, so the capital limit cannot bite and no income is tapered. A guarantee credit computed for a family that does not claim Pension Credit (`would_claim_pc`), or that PolicyEngine does not treat as eligible for it (`is_pension_credit_eligible`), does not count. `is_pension_credit_eligible` omits the saving in [SI 2019/37 art. 4](https://www.legislation.gov.uk/uksi/2019/37/article/4) for a mixed-age couple entitled to Pension Credit or Housing Benefit as that couple on 14 May 2019 (and to either ever since). Such a couple may be entitled to a guarantee credit in law and, if they receive it, are passported; PolicyEngine does not passport them until that saving is modelled.\n", "\n", "Take-up is handled by `would_claim_housing_benefit`, which is true for benunits that report Housing Benefit and, when no reported benefits are supplied (as in household calculations), for everyone. In the Enhanced FRS take-up therefore follows reported claims." ] diff --git a/policyengine_uk/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 b2670e9a92..257d1beb11 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 @@ -236,8 +236,9 @@ members: person # The passport needs a guarantee credit that is paid. Pension Credit # income here is 10,000 plus 80 x 52 = 4,160 deemed from capital, - # 14,160 in all, above the 2025 minimum guarantee of 11,536.20, so - # Pension Credit eligibility is set along with the credit. + # 14,160 in all, above PolicyEngine's 2025 parameter for the minimum + # guarantee (221.85 x 52 = 11,536.20), so Pension Credit eligibility + # is set along with the credit. guarantee_credit: 1 is_pension_credit_eligible: true would_claim_uc: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml index d5c6cd18a7..62641137e8 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml @@ -2,7 +2,8 @@ # whose partner is in receipt, of a guarantee credit, the whole of his capital # and income shall be disregarded." (SI 2006/214 reg 26; NI: SR 2006/406 # reg 24.) The passport follows the guarantee credit actually paid, not one -# computed for a family that does not claim Pension Credit or cannot get it. +# computed for a family that does not claim Pension Credit or that +# PolicyEngine does not treat as eligible for it. # # 2026 parameters: Pension Credit standard minimum guarantee 12,376 a year # single and 18,889 couple; Pension Credit and pension-age Housing Benefit @@ -150,7 +151,7 @@ housing_benefit_applicable_income: 0 housing_benefit: 7_800 -- name: Case 5, mixed-age couple with a continuing award is not passported. +- name: Case 5, mixed-age couple with a continuing award is not passported, because PolicyEngine does not model the SI 2019/37 art 4 saving. period: 2026 absolute_error_margin: 0.01 input: @@ -173,10 +174,18 @@ tenure_type: RENT_FROM_COUNCIL savings: 20_000 output: - # Computed guarantee credit 18,889 - (10,000 + 1,040) = 7,849, but a - # couple with a partner under State Pension age is not eligible for - # Pension Credit, so none is paid. - guarantee_credit: 7_849 + # PolicyEngine's is_pension_credit_eligible omits the SI 2019/37 art 4 + # saving, so it pays this couple no Pension Credit and there is no + # passport. In law a mixed-age couple saved under art 4(1)(b) (entitled + # to Housing Benefit as that couple on 14 May 2019, and to Pension Credit + # or Housing Benefit ever since, which the continuing award here may + # stand for) is not excluded by SPCA 2002 s.4(1A). It may then be + # entitled to a guarantee credit (18,889 - (10,000 + 1,040) = 7,849 on + # the 2026 parameters), and reg 26 would disregard its capital. This + # case pins the model, not the law, until that saving is modelled. The + # computed guarantee credit is not asserted, so the case does not depend + # on whether one is computed for a family PolicyEngine treats as + # ineligible. is_pension_credit_eligible: false pension_credit: 0 in_receipt_of_guarantee_credit: false diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml index 62698a21de..fe4b686a9e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml @@ -48,7 +48,7 @@ pension_credit: 0 in_receipt_of_guarantee_credit: false -- name: Case 3, mixed-age couple has a computed guarantee credit but is not eligible for Pension Credit. +- name: Case 3, mixed-age couple receives no guarantee credit, because PolicyEngine does not model the SI 2019/37 art 4 saving. period: 2026 absolute_error_margin: 0.01 input: @@ -66,8 +66,11 @@ household: members: [person1, person2] output: - # Couple minimum guarantee 18,889 - 10,000 = 8,889. - guarantee_credit: 8_889 + # PolicyEngine's is_pension_credit_eligible omits the SI 2019/37 art 4 + # saving, so no Pension Credit is paid. In law a mixed-age couple saved + # by art 4 may be entitled to it. The computed guarantee credit is not + # asserted, so the case does not depend on whether it is computed for a + # family PolicyEngine treats as ineligible. is_pension_credit_eligible: false pension_credit: 0 in_receipt_of_guarantee_credit: false diff --git a/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py b/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py index 3c2978065c..73f7d044e1 100644 --- a/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py @@ -19,9 +19,11 @@ computed guarantee credit). The two passports differ exactly for families with someone over State Pension age and a computed guarantee credit that is not paid; there the pre-fix formula passports and this one does not - (intended). Housing Benefit itself then differs only when the counted - capital is over the limit or the counted income exceeds the applicable - amount. + (intended). For a mixed-age couple saved by SI 2019/37 art 4, which + is_pension_credit_eligible omits, that is a known departure from the law + until the saving is modelled. Housing Benefit itself then differs only + when the counted capital is over the limit or the counted income exceeds + the applicable amount. 4. Metamorphic: not claiming Pension Credit never raises Housing Benefit. 5. Under the Pension Credit freeze, receipt is the baseline receipt, because the frozen award is the baseline award. 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 b16ad02e3b..b7cd7aaad5 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 @@ -63,6 +63,12 @@ def formula(benunit, period, parameters): - disregard - childcare_element, ) - # SI 2006/214 reg 26 (NI: SR 2006/406 reg 24). + # Guarantee Credit passport: SI 2006/214 reg 26 (NI: SR 2006/406 + # reg 24) disregards the whole of the capital and income of a + # claimant "in receipt, or whose partner is in receipt, of a + # guarantee credit". By reg 2(5) (NI: reg 2(5)), that includes a + # person who would be in receipt but for SPC Regs 2002 reg 13 (NI: + # SPC Regs (NI) 2003 reg 13), the small-amounts rule, which + # PolicyEngine does not model. passported = any_over_SP_age & benunit("in_receipt_of_guarantee_credit", period) return where(passported, 0, applicable_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py index d313761eff..a9453e909d 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_tariff_income.py @@ -12,7 +12,13 @@ class housing_benefit_tariff_income(Variable): def formula(benunit, period, parameters): capital = benunit("housing_benefit_assessable_capital", period) any_over_SP_age = benunit.any(benunit.members("is_SP_age", period)) - # SI 2006/214 reg 26 (NI: SR 2006/406 reg 24). + # Guarantee Credit passport: SI 2006/214 reg 26 (NI: SR 2006/406 + # reg 24) disregards the whole of the capital and income of a + # claimant "in receipt, or whose partner is in receipt, of a + # guarantee credit". By reg 2(5) (NI: reg 2(5)), that includes a + # person who would be in receipt but for SPC Regs 2002 reg 13 (NI: + # SPC Regs (NI) 2003 reg 13), the small-amounts rule, which + # PolicyEngine does not model. passported = any_over_SP_age & benunit("in_receipt_of_guarantee_credit", period) p = parameters(period).gov.dwp.housing_benefit.means_test.capital threshold = where( @@ -33,4 +39,7 @@ def formula(benunit, period, parameters): excess_capital = max_(0, capital - threshold) steps = np.ceil(excess_capital / step) tariff_income = steps * amount * WEEKS_IN_YEAR + # Redundant with the zero assessable capital of a passported + # family, but kept for parallelism with reg 26's "capital and + # income". return where(passported, 0, tariff_income) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py index 6b087a0842..f47ade2c91 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 @@ -29,6 +29,12 @@ def formula(benunit, period, parameters): household_capital * benunit_adults / adult_divisor, 0, ) - # SI 2006/214 reg 26 (NI: SR 2006/406 reg 24). + # Guarantee Credit passport: SI 2006/214 reg 26 (NI: SR 2006/406 + # reg 24) disregards the whole of the capital and income of a + # claimant "in receipt, or whose partner is in receipt, of a + # guarantee credit". By reg 2(5) (NI: reg 2(5)), that includes a + # person who would be in receipt but for SPC Regs 2002 reg 13 (NI: + # SPC Regs (NI) 2003 reg 13), the small-amounts rule, which + # PolicyEngine does not model. passported = any_over_SP_age & benunit("in_receipt_of_guarantee_credit", period) return where(passported, 0, max_(0, household_capital_proxy)) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py index 69b5a7e090..877ab3606b 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py @@ -7,10 +7,14 @@ class in_receipt_of_guarantee_credit(Variable): "Whether the claimant or partner in this benefit unit is in receipt of " "a guarantee credit: the benefit unit is eligible for Pension Credit, " "claims it, and its Guarantee Credit is positive. A Guarantee Credit " - "computed for a family that does not claim Pension Credit, or is not " - "eligible for it (for example a working-age or mixed-age family), is " - "not received. Under the Pension Credit freeze the baseline receipt is " - "kept, because the frozen award is the baseline award." + "computed for a family that does not claim Pension Credit, or that the " + "model does not treat as eligible for it (for example a working-age " + "family), is not received. The model's Pension Credit eligibility " + "omits the SI 2019/37 article 4 saving, so a mixed-age couple it saves " + "(entitled to Pension Credit or Housing Benefit as that couple on 14 " + "May 2019, and to either ever since) is not treated as in receipt. Under " + "the Pension Credit freeze the baseline receipt is kept, because the " + "frozen award is the baseline award." ) entity = BenUnit definition_period = YEAR @@ -18,6 +22,8 @@ class in_receipt_of_guarantee_credit(Variable): reference = ( "https://www.legislation.gov.uk/ukpga/2002/16/section/1", "https://www.legislation.gov.uk/ukpga/2002/16/section/2", + "https://www.legislation.gov.uk/ukpga/1992/5/section/1", + "https://www.legislation.gov.uk/uksi/2019/37/article/4", ) def formula(benunit, period, parameters):