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..76a2ede844 --- /dev/null +++ b/changelog.d/hb-gc-passport-receipt.fixed.md @@ -0,0 +1 @@ +- Disregard Housing Benefit income and capital on receipt of Guarantee Credit, so pensioners who do not claim Pension Credit retain the ordinary means test. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 39d1e4fbbd..704cabf965 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 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. The passport applies within the pension-age regulations (`housing_benefit_pension_age_regulations_apply`). `is_pension_credit_eligible` retains mixed-age couples covered by the [SI 2019/37 art. 4](https://www.legislation.gov.uk/uksi/2019/37/article/4) saving through `meets_pension_credit_age_conditions`; these couples keep the passport when they receive Guarantee Credit. When Pension Credit is frozen, receipt follows the baseline award, including its Guarantee Credit component. Savings Credit alone follows the separate assessment in [reg. 27](https://www.legislation.gov.uk/uksi/2006/214/regulation/27), including its capital limit, and does not give this passport.\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 af28ec2ca0..1bc2a40511 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,7 +236,13 @@ 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 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 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..dc92ef6b46 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_guarantee_credit_passport.yaml @@ -0,0 +1,393 @@ +# 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 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 +# 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 whose older member did not qualify by May 2019 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: + # The older member, aged 70 in 2026, had not reached the qualifying + # age by 14 May 2019. Current reported Housing Benefit therefore does + # not establish the SI 2019/37 art 4 saving. No Pension Credit is paid, + # so SI 2006/214 reg 26 does not disregard this couple's capital. + has_mixed_age_couple_pension_credit_saving: false + 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: Entitled pension-age nonclaimant receives no maximum HB passport while the claimant does in 2025. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + nonclaimant: + age: 70 + state_pension_reported: 10_000 + claimant: + age: 70 + state_pension_reported: 10_000 + benunits: + not_claiming: + members: [nonclaimant] + would_claim_pc: false + would_claim_housing_benefit: true + claiming: + members: [claimant] + would_claim_pc: true + would_claim_housing_benefit: true + households: + not_claiming_household: + members: [nonclaimant] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + claiming_household: + members: [claimant] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + # Pension Credit capital income: SI 2002/1792 reg 15(6), and the repo's + # pension_credit.income.capital parameters, give £1/week per £500 above + # £10,000: ceil((20,000 - 10,000) / 500) x 1 x 52 = £1,040/year. + # The 2025 SINGLE minimum_guarantee parameter is £221.85/week: + # 221.85 x 52 = £11,536.20; 11,536.20 - (10,000 + 1,040) = £496.20. + # Both are entitled; only the claimant receives that Guarantee Credit. + pension_credit_deemed_income: [1_040, 1_040] + minimum_guarantee: [11_536.20, 11_536.20] + guarantee_credit: [496.20, 496.20] + pension_credit: [0, 496.20] + in_receipt_of_guarantee_credit: [false, true] + # SI 2006/214 reg 26 disregards income and capital only on receipt: + # https://www.legislation.gov.uk/uksi/2006/214/regulation/26 + housing_benefit_assessable_capital: [20_000, 0] + # Reg 29(2) and housing_benefit.means_test.capital.pension_age give + # the same £1/week tariff: 20 x 52 = £1,040 for the nonclaimant. + # https://www.legislation.gov.uk/uksi/2006/214/regulation/29 + housing_benefit_tariff_income: [1_040, 0] + housing_benefit_applicable_income: [11_040, 0] + # Reg 43 and the pension_age.limit parameter exclude capital over + # £16,000: 20,000 > 16,000, but the claimant's assessable capital is 0. + # https://www.legislation.gov.uk/uksi/2006/214/regulation/43 + housing_benefit_eligible: [false, true] + # The nonclaimant gets £0. The claimant has no counted income and no + # non-dependants; council tenure has no LHA cap, so HB is the full rent, + # £120/week x 52 = £6,240/year (SI 2006/214 regs 50 and 55). + # https://www.legislation.gov.uk/uksi/2006/214/regulation/50 + housing_benefit: [0, 6_240] + +- 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 + +- 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 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..c3f89fcbb8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.yaml @@ -0,0 +1,189 @@ +# 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 without the SI 2019/37 art 4 saving receives no guarantee 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: + # Neither an existing award nor the SI 2019/37 art 4 saving is supplied, + # and the older member had not reached the qualifying age by May 2019. + # The mixed-age exclusion therefore applies and no Pension Credit is + # paid. A computed Guarantee Credit is not receipt. + 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 + +- name: Mixed-age couple with the SI 2019/37 art 4 saving receives Guarantee Credit and passports HB. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 75 + state_pension_reported: 10_000 + partner: + age: 60 + benunits: + benunit: + members: [pensioner, partner] + # Known entitlement as this couple on 14 May 2019 and continuously + # since: SI 2019/37 art 4 preserves the Pension Credit age route. + # https://www.legislation.gov.uk/uksi/2019/37/article/4 + has_mixed_age_couple_pension_credit_saving: true + would_claim_pc: true + would_claim_housing_benefit: true + households: + household: + members: [pensioner, partner] + rent: 6_240 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + is_pension_credit_eligible: true + # Couple minimum guarantee: £363.25/week x 52 = £18,889. Capital + # deemed income: ceil((20,000 - 10,000) / 500) x 52 = £1,040. + # 18,889 - (10,000 + 1,040) = £7,849 of Guarantee Credit, paid. + guarantee_credit: 7_849 + pension_credit: 7_849 + in_receipt_of_guarantee_credit: true + # SI 2006/214 reg 26 disregards all capital and income on receipt; + # the younger partner does not prevent the saved couple's passport. + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + housing_benefit: 6_240 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..aed68a8008 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_guarantee_credit_passport_properties.py @@ -0,0 +1,261 @@ +"""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. Its claimant and partner have reached the + qualifying age or have the SI 2019/37 art 4 mixed-age couple saving. +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 + under the pension-age regulations and a computed guarantee credit that is + not paid; there the pre-fix formula passports and this one does not + (intended). The SI 2019/37 art 4 saving applies to qualifying mixed-age + couples. Housing Benefit itself then differs only + when the counted capital is over the limit or the counted income exceeds + the applicable amount. +4. Metamorphic: for Guarantee Credit recipients, not claiming Pension Credit + never raises Housing Benefit. Savings-credit-only awards use reg 27's + separate income rule, which counts the savings credit payable. +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}" + # The shapes describe claimants and partners, including young + # partners who would otherwise be presumed dependent children. + person = { + "age": {YEAR: age}, + "is_claimant_or_partner": {YEAR: True}, + } + 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 the pension-age regulations + # to apply, so swapping this in reproduces main's entitlement passport. + 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["pension_age_regulations"] = np.asarray( + sim.calculate("housing_benefit_pension_age_regulations_apply", YEAR) + ) + values["mixed_age_saving"] = np.asarray( + sim.calculate("has_mixed_age_couple_pension_credit_saving", YEAR) + ) + 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) or values["mixed_age_saving"][i], 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["pension_age_regulations"] & old["in_receipt_of_guarantee_credit"] + ) + new_passport = ( + new["pension_age_regulations"] & new["in_receipt_of_guarantee_credit"] + ) + # Intended difference: a computed guarantee credit that is not paid. + computed_not_paid = ( + new["pension_age_regulations"] + & (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_guarantee_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"]) + guarantee_credit_recipients = claiming["in_receipt_of_guarantee_credit"] + assert np.all( + not_claiming["housing_benefit"][guarantee_credit_recipients] + <= claiming["housing_benefit"][guarantee_credit_recipients] + 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 6119abf2e1..d215f59dc4 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 @@ -10,7 +10,7 @@ class housing_benefit_applicable_income(Variable): "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), whose whole " - "income is disregarded, and for a pension-age family with a positive " + "income is disregarded, and for a pension-age family receiving a " "guarantee credit (the guarantee credit passport). Where the Pension " "Credit award is savings credit only, it is the Secretary of State's " "assessment of income plus the savings credit payable, less childcare " @@ -118,10 +118,14 @@ def formula(benunit, period, parameters): benunit("housing_benefit_savings_credit_only_income", period), income_under_general_rules, ) - # Pension HB reg 26 disregards "the whole of his capital and income" - # for guarantee-credit recipients within that regulation set. - guarantee_credit = pension_age_regulations & ( - benunit("guarantee_credit", period) > 0 + # 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), receipt includes awards withheld + # solely under SPC Regs 2002 reg 13 (small amounts), which the + # model does not withhold. Entitlement without a claim is not receipt. + passported = pension_age_regulations & benunit( + "in_receipt_of_guarantee_credit", period ) # 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 universal credit, @@ -142,4 +146,4 @@ def formula(benunit, period, parameters): on_passporting_benefit = benunit( "housing_benefit_on_passporting_benefit", period ) - return where(guarantee_credit | on_passporting_benefit, 0, applicable_income) + return where(passported | on_passporting_benefit, 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 9280adbd13..6526375fe8 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 @@ -21,8 +21,14 @@ def formula(benunit, period, parameters): pension_age_regulations = benunit( "housing_benefit_pension_age_regulations_apply", period ) - guarantee_credit = pension_age_regulations & ( - benunit("guarantee_credit", period) > 0 + # 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), receipt includes awards withheld + # solely under SPC Regs 2002 reg 13 (small amounts), which the + # model does not withhold. Entitlement without a claim is not receipt. + passported = pension_age_regulations & benunit( + "in_receipt_of_guarantee_credit", period ) p = parameters(period).gov.dwp.housing_benefit.means_test.capital threshold = where( @@ -53,4 +59,4 @@ def formula(benunit, period, parameters): "in_receipt_of_savings_credit_only", period ) tariff_income = where(savings_credit_only, 0, tariff_income) - 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 59dd042318..2af26611c4 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 @@ -18,7 +18,7 @@ class housing_benefit_assessable_capital(Variable): "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 with a positive guarantee credit (the guarantee credit " + "family receiving a guarantee credit (the guarantee credit " "passport)." ) definition_period = YEAR @@ -29,7 +29,9 @@ class housing_benefit_assessable_capital(Variable): "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", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/26", "https://www.legislation.gov.uk/uksi/2006/214/regulation/27", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/24", "https://www.legislation.gov.uk/nisr/2006/406/regulation/25", ) @@ -75,10 +77,14 @@ def formula(benunit, period, parameters): benunit("pension_credit_assessable_capital", period), household_capital_proxy + person_capital, ) - # Pension HB reg 26 disregards "the whole of his capital and income" - # for guarantee-credit recipients within that regulation set. - guarantee_credit = pension_age_regulations & ( - benunit("guarantee_credit", period) > 0 + # 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), receipt includes awards withheld + # solely under SPC Regs 2002 reg 13 (small amounts), which the + # model does not withhold. Entitlement without a claim is not receipt. + passported = pension_age_regulations & benunit( + "in_receipt_of_guarantee_credit", period ) # 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 universal credit, @@ -91,7 +97,7 @@ def formula(benunit, period, parameters): "housing_benefit_on_passporting_benefit", period ) return where( - guarantee_credit | on_passporting_benefit, + passported | on_passporting_benefit, 0, max_(0, capital), )