diff --git a/changelog.d/hb-savings-credit-only-income.fixed.md b/changelog.d/hb-savings-credit-only-income.fixed.md new file mode 100644 index 0000000000..33f5e0a58b --- /dev/null +++ b/changelog.d/hb-savings-credit-only-income.fixed.md @@ -0,0 +1 @@ +Use the Pension Credit assessment of income and capital, plus the savings credit payable, for Housing Benefit where the Pension Credit award is savings credit only (HB(SPC) Regs 2006 reg 27). For pensioners without earnings, while the award remains savings credit only, a rise in private pension no longer lowers net income (tested for council tenants in Great Britain); losing Pension Credit passports, such as the free TV licence at 75, still applies when savings credit ends. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml new file mode 100644 index 0000000000..1c45251f4f --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml @@ -0,0 +1,327 @@ +# Savings-credit-only cases: "In determining the income and capital of a +# claimant who has, or whose partner has, an award of state pension credit +# comprising only the savings credit, the relevant authority shall ... use the +# calculation or estimate of the claimant's or, as the case may be, the +# claimant's partner's income and capital made by the Secretary of State for +# the purpose of determining that award." (SI 2006/214 reg 27(1); NI: SR +# 2006/406 reg 25(1).) The authority modifies that net income only for the +# items in reg 27(4), the first being "the amount of the savings credit +# payable"; Housing Benefit's own income rules (regs 29 to 49) do not apply to +# it (reg 27(5)); and capital is the Secretary of State's calculation, with the +# £16,000 limit applied to it (reg 27(6)). +# +# 2026 parameters: Pension Credit standard minimum guarantee £238.00 a week +# single (12,376 a year) and £363.25 couple (18,889); savings credit threshold +# £208.07 single (10,819.64) and £329.75 couple (17,147); savings credit +# phase-in 60% and withdrawal 40%, so the maximum savings credit is +# 0.6 x (12,376 - 10,819.64) = 933.82 single and 0.6 x (18,889 - 17,147) = +# 1,045.20 couple; the Pension Credit child addition £69.98 (3,638.96) for a +# child born after 5 April 2017; +# Pension Credit deemed income and pension-age Housing Benefit tariff income +# both £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); taper 65%; income tax personal allowance 12,570 and basic rate 20%. +# Every Pension Credit claimant and partner is over State Pension age and +# reached it before April 2016, so can have savings credit, and pays no +# National Insurance. +# +# Robustness to the other open Housing Benefit fixes: the tenants rent from +# the council, so the Local Housing Allowance order cannot apply; every case +# sets housing_benefit_applicable_income_disregard to the value the earnings +# disregard fix (PolicyEngine/policyengine-uk#1908) gives, 0 for a family with +# no earnings; and no claimant has a disability benefit, so the severe +# disability premium fix (#1938) does not change the applicable amount. + +- name: Case 1, savings credit only, the savings credit payable is added to the Pension Credit income. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 11_000 + private_pension_income: 2_000 + benunits: + benunit: + members: [person1] + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 0 + output: + # (13,000 - 12,570) x 0.2 = 86. + income_tax: 86 + # 13,000 - 86 = 12,914, above the minimum guarantee of 12,376. + pension_credit_income: 12_914 + guarantee_credit: 0 + # 933.82 - 0.4 x (12,914 - 12,376) = 933.82 - 215.20 = 718.62. + savings_credit: 718.62 + pension_credit: 718.62 + in_receipt_of_savings_credit_only: true + # Reg 27(4)(a): 12,914 + 718.62 = 13,632.62. + housing_benefit_savings_credit_only_income: 13_632.62 + housing_benefit_applicable_income: 13_632.62 + housing_benefit_applicable_amount: 13_312 + # 7,800 - 0.65 x (13,632.62 - 13,312) = 7,800 - 208.40 = 7,591.60. On + # the Housing Benefit rules the income, 12,914, is below the applicable + # amount, and Housing Benefit would be the full 7,800. + housing_benefit: 7_591.60 + +- name: Case 2, savings credit only with capital, the Pension Credit deemed income is counted once. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 11_000 + private_pension_income: 2_000 + benunits: + benunit: + members: [person1] + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 12_300 + output: + # (12,300 - 10,000) / 500 = 4.6, so 5 steps; 5 x 52 = 260. + pension_credit_assessable_capital: 12_300 + pension_credit_deemed_income: 260 + # 13,000 - 86 + 260 = 13,174. + pension_credit_income: 13_174 + # 933.82 - 0.4 x (13,174 - 12,376) = 933.82 - 319.20 = 614.62. + savings_credit: 614.62 + in_receipt_of_savings_credit_only: true + # Reg 27(6): the Pension Credit assessment of capital. + housing_benefit_assessable_capital: 12_300 + # Reg 27(5): no Housing Benefit tariff income on top of the deemed income. + housing_benefit_tariff_income: 0 + # 13,174 + 614.62 = 13,788.62. + housing_benefit_applicable_income: 13_788.62 + # 7,800 - 0.65 x (13,788.62 - 13,312) = 7,800 - 309.80 = 7,490.20. + housing_benefit: 7_490.20 + +- name: Case 3, savings credit only couple with a non-dependant, the 16,000 limit applies to the Pension Credit capital. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 9_000 + person2: + age: 78 + state_pension: 9_000 + person3: + age: 40 + employment_income: 20_000 + benunits: + benunit1: + members: [person1, person2] + housing_benefit_applicable_income_disregard: 0 + benunit2: + members: [person3] + households: + household: + members: [person1, person2, person3] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 20_000 + output: + # Pension Credit splits household capital across the two pension-age + # adults only, so the couple has all 20,000: 20 steps, 20 x 52 = 1,040 of + # deemed income. Each State Pension is below the personal allowance. + pension_credit_assessable_capital: [20_000, 0] + # 18,000 + 1,040 = 19,040, above the couple minimum guarantee of 18,889. + # The non-dependant's is computed but he cannot have Pension Credit: + # 20,000 - 1,486 income tax - 594.40 National Insurance = 17,919.60. + pension_credit_income: [19_040, 17_919.60] + guarantee_credit: [0, 0] + # 1,045.20 - 0.4 x (19,040 - 18,889) = 1,045.20 - 60.40 = 984.80. + savings_credit: [984.80, 0] + in_receipt_of_savings_credit_only: [true, false] + # Reg 27(6): the couple's capital is the Pension Credit's 20,000, not the + # household adult share of 20,000 x 2 / 3 = 13,333.33 used otherwise; the + # non-dependant keeps the adult share of 6,666.67. + housing_benefit_assessable_capital: [20_000, 6_666.67] + # 20,000 is over the 16,000 limit (reg 43, applied by reg 27(6)). + housing_benefit_eligible: [false, false] + housing_benefit: [0, 0] + +- name: Case 4, savings credit only with earnings, the earnings disregard is deducted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 11_000 + employment_income: 2_000 + benunits: + benunit: + members: [person1] + # £5 a week for a single claimant with earnings (Sch 4 para 7(a)), + # the same as the Pension Credit disregard (SPC Regs 2002 Sch VI + # para 5(a)); 5 x 52 = 260. Pension Credit income in the model + # disregards no earnings, so deducting it gives the Secretary of + # State's figure. + housing_benefit_applicable_income_disregard: 260 + households: + household: + members: [person1] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 0 + output: + # 13,000 - 86 = 12,914, as in case 1. + pension_credit_income: 12_914 + savings_credit: 718.62 + in_receipt_of_savings_credit_only: true + # 12,914 + 718.62 - 260 = 13,372.62. + housing_benefit_applicable_income: 13_372.62 + # 7,800 - 0.65 x (13,372.62 - 13,312) = 7,800 - 39.40 = 7,760.60. + housing_benefit: 7_760.60 + +- name: Case 5, savings credit only lone parent in work, childcare charges and the lone parent and additional earnings disregards are deducted. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 78 + state_pension: 11_000 + employment_income: 7_000 + # 20 hours a week, so remunerative work (reg 6) and the reg 31 + # condition for deducting childcare charges are met. + hours_worked: 1_040 + person2: + age: 8 + childcare_expenses: 2_600 + benunits: + benunit: + members: [person1, person2] + # Reg 27(4)(c)(i): the higher amount disregarded for a lone parent's + # earnings, £25 a week (Sch 4 para 2) against Pension Credit's £20 + # (SPC Regs 2002 Sch VI para 1); and (d): the additional £17.10 (Sch + # 4 para 9(1)) for a lone parent working 16 hours or more whose net + # earnings cover the disregards and childcare. Pension Credit income + # in the model disregards no earnings, so the whole of both is + # deducted: (25 + 17.10) x 52 = 2,189.20, the value #1908 gives. + housing_benefit_applicable_income_disregard: 2_189.20 + households: + household: + members: [person1, person2] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 0 + output: + # (18,000 - 12,570) x 0.2 = 1,086; 18,000 - 1,086 = 16,914. + pension_credit_income: 16_914 + # 12,376 + 3,638.96 = 16,014.96. + minimum_guarantee: 16_014.96 + guarantee_credit: 0 + # 933.82 - 0.4 x (16,914 - 16,014.96) = 933.82 - 359.62 = 574.20. + savings_credit: 574.20 + in_receipt_of_savings_credit_only: true + # Reg 27(4)(b): 2,600 of childcare (£50 a week), below the weekly + # maximum and the earnings. + housing_benefit_applicable_income_childcare_element: 2_600 + # 16,914 + 574.20 - 2,600 - 2,189.20 = 12,699. + housing_benefit_savings_credit_only_income: 12_699 + housing_benefit_applicable_income: 12_699 + +- name: Case 6, guarantee credit and savings credit together, the award is not savings credit only and the guarantee credit passport applies. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 12_000 + benunits: + benunit: + members: [person1] + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 0 + output: + # 12,000 is below the personal allowance, so no income tax. + pension_credit_income: 12_000 + # 12,376 - 12,000 = 376. + guarantee_credit: 376 + # min(933.82, 0.6 x (12,000 - 10,819.64)) = 708.22. + savings_credit: 708.22 + in_receipt_of_savings_credit_only: false + housing_benefit_applicable_income: 0 + housing_benefit: 7_800 + +- name: Case 7, no Pension Credit, the Housing Benefit rules apply. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 11_000 + private_pension_income: 6_000 + benunits: + benunit: + members: [person1] + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 12_300 + output: + # (17,000 - 12,570) x 0.2 = 886; 17,000 - 886 + 260 = 16,374. + pension_credit_income: 16_374 + # 933.82 - 0.4 x (16,374 - 12,376) is negative. + savings_credit: 0 + in_receipt_of_savings_credit_only: false + housing_benefit_assessable_capital: 12_300 + housing_benefit_tariff_income: 260 + # 17,000 - 886 + 260 = 16,374. + housing_benefit_applicable_income: 16_374 + # 7,800 - 0.65 x (16,374 - 13,312) = 7,800 - 1,990.30 = 5,809.70. + housing_benefit: 5_809.70 + +- name: Case 8, savings credit computed but Pension Credit not claimed, the Housing Benefit rules apply. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + state_pension: 11_000 + private_pension_income: 2_000 + benunits: + benunit: + members: [person1] + would_claim_pc: false + housing_benefit_applicable_income_disregard: 0 + households: + household: + members: [person1] + rent: 7_800 + tenure_type: RENT_FROM_COUNCIL + savings: 0 + output: + savings_credit: 718.62 + pension_credit: 0 + # There is no award, so reg 27 does not apply. + in_receipt_of_savings_credit_only: false + # 12,914, below the applicable amount of 13,312. + housing_benefit_applicable_income: 12_914 + housing_benefit: 7_800 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml index 34bb2652a1..fee4343f7b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_severe_disability_conditions.yaml @@ -1,12 +1,12 @@ # HB(SPC) Regs 2006 Sch 3 para 6, with the non-dependant definition in # reg 3: these cases assert final awards without a Guarantee Credit passport. # The final HB and CTR awards include the model's current income rules, -# including two known defects outside this PR: no Savings Credit in HB -# income for a savings-credit-only claimant (#2112), and no non-dependant -# deduction exemption for a claimant on Attendance Allowance (#2007). The -# premium and applicable-amount assertions do not depend on them. Each HB -# award is the rent less 65% of income after tax above the applicable -# amount; State Pension gets no earnings disregard (#1908). +# including a known defect outside this file: no non-dependant deduction +# exemption for a claimant on Attendance Allowance (#2007). The premium and +# applicable-amount assertions do not depend on it. Each HB award is the +# rent less 65% of income after tax, plus the savings credit where the award +# is savings credit only (HB (SPC) Regs 2006 reg 27(4)(a)), above the +# applicable amount; State Pension gets no earnings disregard (#1908). - name: One qualifying pensioner and a non-blind partner get no HB or CTR severe premium period: 2026 @@ -63,7 +63,10 @@ output: guarantee_credit: 0 pension_age_severe_disability_premium: 172.10 * 52 - housing_benefit: 6_146.01 + # Savings credit only (569.28): HB income is Pension Credit income plus + # the savings credit (HB (SPC) Regs 2006 reg 27(4)(a)), 0.65 x 569.28 = + # 370.03 below the figure without it. + housing_benefit: 5_775.98 council_tax_benefit: 1_857.22 - name: A qualifying pensioner with a blind non-qualifying partner gets one HB and CTR rate @@ -92,7 +95,10 @@ output: guarantee_credit: 0 pension_age_severe_disability_premium: 86.05 * 52 - housing_benefit: 5_317.52 + # Savings credit only (59.44): HB income is Pension Credit income plus + # the savings credit (HB (SPC) Regs 2006 reg 27(4)(a)), 0.65 x 59.44 = + # 38.64 below the figure without it. + housing_benefit: 5_278.88 council_tax_benefit: 1_704.27 - name: A non-ignored adult resident blocks the pensioner's HB and CTR severe premium @@ -163,7 +169,10 @@ has_non_dependant_for_severe_disability_premium: [true, false] pension_age_severe_disability_premium: [0, 0] housing_benefit_applicable_amount: [13_312, 0] - housing_benefit: [3_898.70, 0] + # Savings credit only (908.46): HB income is Pension Credit income plus + # the savings credit (HB (SPC) Regs 2006 reg 27(4)(a)), 0.65 x 908.46 = + # 590.50 below the figure without it. + housing_benefit: [3_308.20, 0] council_tax_benefit: [1_097.91, 0] - name: A qualifying-benefit resident in another benefit unit can still be the pensioner's paid carer diff --git a/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py b/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py new file mode 100644 index 0000000000..6d19738615 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_savings_credit_only_properties.py @@ -0,0 +1,419 @@ +"""Housing Benefit where the Pension Credit award is savings credit only. + +HB(SPC) Regs 2006 (SI 2006/214) reg 27 (NI: SR 2006/406 reg 25): the +authority uses the Secretary of State's assessment of income and capital for +the award, modified to take account of "the amount of the savings credit +payable" (reg 27(4)(a)) and the other items reg 27(4) lists; Housing +Benefit's own income rules, including tariff income, do not apply to it (reg +27(5)); and the £16,000 limit applies to the Secretary of State's capital (reg +27(6)). + +Before this change Housing Benefit counted none of the savings credit. On a +rise in private pension, savings credit fell by 40% of the rise in net income, +Housing Benefit by 65% of the whole rise, and council tax reduction, which +counts the savings credit (PolicyEngine/policyengine-uk#1909), by 20% of the +rise net of the savings credit withdrawn, or 12%: 117% in all, so a family +lost more than it gained. A single person aged 80 with no State Pension whose +private pension rose from £13,500 to £13,750 in 2026 lost £34 of net income. +Counting the savings credit, Housing Benefit falls by 65% of the rise net of +the savings credit withdrawn, or 39%, and the combined withdrawal is 91%. + +Invariants, for single people and couples aged 80 or over (so State Pension +age was reached before April 2016 and savings credit is possible), who rent +from the council in England, Wales or Scotland and have no children, carers +or earnings, with private pension on a £250 grid: + +1. Monotonicity: on every step on which the family has a savings-credit-only + award at both ends, household net income, excluding the TV licence, does + not fall. +2. Boundary: on the step where savings credit ends, household net income does + not fall, excluding the TV licence, the Winter Fuel Payment and the + Scottish Pension Age Winter Heating Payment, which the model makes depend + on receiving Pension Credit (the free licence at 75, the Scottish payment's + Pension Credit rates, and the Winter Fuel Payment above £35,000 of taxable + income). Housing Benefit switches from the Secretary of State's figure to + its own rules there. +3. Differential: a savings-credit-only family's Housing Benefit income is the + Pension Credit income, recomputed here from pensions, income tax and the + deemed income on savings, plus the savings credit, less the earnings + disregard; its tariff income is zero and its capital is its savings. Other + families keep the Housing Benefit rules: zero income with a guarantee + credit, otherwise pensions less income tax plus tariff income less the + earnings disregard. + +Carers are not drawn. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +# Wide enough that every drawn family starts with a guarantee credit or savings +# credit and ends with no Pension Credit: a couple both on Attendance Allowance +# with no State Pension leaves savings credit at about £35,000. +PENSIONS = np.arange(0, 40_001, 250) +PROPERTY_SETTINGS = settings( + max_examples=12, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Local authorities without a modelled local council tax reduction scheme, so +# each country's national scheme applies. +COUNTRIES = { + "ENGLAND": "MAIDSTONE", + "WALES": "CARDIFF", + "SCOTLAND": "GLASGOW_CITY", +} +DISABILITY = st.sampled_from( + [ + {}, + {}, + {"aa_category": "LOWER"}, + {"aa_category": "HIGHER"}, + {"dla_sc_category": "MIDDLE"}, + {"pip_dl_category": "ENHANCED"}, + ] +) +# Pension Credit deemed income and pension-age Housing Benefit tariff income: +# £1 a week for each £500, or part, of capital over £10,000. +CAPITAL_THRESHOLD = 10_000 +CAPITAL_STEP = 500 +WEEKS = 52 + + +def money(low, high): + return st.floats(low, high, allow_nan=False, allow_infinity=False) + + +@st.composite +def adult(draw): + return dict( + # Aged 80 or over in 2026, so State Pension age was reached before + # 6 April 2016 and savings credit is possible. (The model tests this + # against the current State Pension age of 66, so in 2026 it admits + # only people aged 77 or over and excludes qualifying people aged 76 + # or under.) + age=draw(st.integers(80, 100)), + disability=draw(DISABILITY), + blind=draw(st.booleans()), + # At most £10,000 each, so a couple without disability starts below + # the top of the savings credit range (about £21,500 of income). + state_pension=draw(money(0, 10_000)), + ) + + +@st.composite +def family(draw): + return dict( + adults=[draw(adult()) for _ in range(draw(st.integers(1, 2)))], + country=draw(st.sampled_from(sorted(COUNTRIES))), + rent=draw(money(3_000, 12_000)), + council_tax=draw(money(500, 3_000)), + savings=draw(st.one_of(st.just(0.0), money(0, 16_000))), + ) + + +def situation(families): + """Each family once per private pension on the PENSIONS grid.""" + people, benunits, households = {}, {}, {} + for i, fam in enumerate(families): + for k, pension in enumerate(PENSIONS): + names = [] + for j, a in enumerate(fam["adults"]): + name = f"p{i}_{k}_{j}" + person = { + "age": {YEAR: a["age"]}, + "state_pension": {YEAR: a["state_pension"]}, + # All private pension goes to the first adult. + "private_pension_income": {YEAR: float(pension) * (j == 0)}, + "is_blind": {YEAR: a["blind"]}, + } + for variable, value in a["disability"].items(): + person[variable] = {YEAR: value} + people[name] = person + names.append(name) + benunits[f"b{i}_{k}"] = {"members": names} + households[f"h{i}_{k}"] = { + "members": names, + "country": {YEAR: fam["country"]}, + "local_authority": {YEAR: COUNTRIES[fam["country"]]}, + "tenure_type": {YEAR: "RENT_FROM_COUNCIL"}, + "rent": {YEAR: fam["rent"]}, + "council_tax": {YEAR: fam["council_tax"]}, + "savings": {YEAR: fam["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +BENUNIT_VARIABLES = [ + "guarantee_credit", + "savings_credit", + "pension_credit", + "minimum_guarantee", + "housing_benefit", + "housing_benefit_applicable_income", + "housing_benefit_applicable_income_disregard", + "housing_benefit_assessable_capital", + "housing_benefit_tariff_income", + "council_tax_benefit", +] +HOUSEHOLD_VARIABLES = [ + "household_net_income", + "tv_licence", + "winter_fuel_allowance", + "pawhp", +] + + +def grid(families): + simulation = Simulation(situation=situation(families)) + shape = (len(families), len(PENSIONS)) + values = { + variable: np.asarray(simulation.calculate(variable, YEAR)).reshape(shape) + for variable in BENUNIT_VARIABLES + HOUSEHOLD_VARIABLES + } + for variable in ["state_pension", "private_pension_income", "income_tax"]: + values[variable] = np.asarray( + simulation.calculate(variable, YEAR, map_to="benunit") + ).reshape(shape) + # An award of savings credit only: Pension Credit is paid and none of it + # is guarantee credit. + values["savings_credit_only"] = (values["pension_credit"] > 0) & ( + values["guarantee_credit"] <= 0 + ) + values["guarantee_credit_paid"] = (values["pension_credit"] > 0) & ( + values["guarantee_credit"] > 0 + ) + return values + + +def describe(fam, g, i, k): + return ( + fam, + f"private pension {PENSIONS[k]} -> {PENSIONS[k + 1]}", + f"savings credit {g['savings_credit'][i, k]:.2f} -> " + f"{g['savings_credit'][i, k + 1]:.2f}", + f"HB {g['housing_benefit'][i, k]:.2f} -> {g['housing_benefit'][i, k + 1]:.2f}", + f"CTR {g['council_tax_benefit'][i, k]:.2f} -> " + f"{g['council_tax_benefit'][i, k + 1]:.2f}", + ) + + +@PROPERTY_SETTINGS +@given(st.lists(family(), min_size=1, max_size=4)) +def test_net_income_rises_with_private_pension_on_savings_credit_only(families): + g = grid(families) + net = g["household_net_income"] + g["tv_licence"] + step = np.diff(net, axis=1) + sc_only = g["savings_credit_only"] + both_ends = sc_only[:, :-1] & sc_only[:, 1:] + # Every family is on savings credit only for part of the grid. + assert both_ends.any(axis=1).all(), families + for i, fam in enumerate(families): + for k in np.flatnonzero(both_ends[i]): + assert step[i, k] >= -0.01, ( + *describe(fam, g, i, k), + f"net income change {step[i, k]:.2f}", + ) + + +@PROPERTY_SETTINGS +@given(st.lists(family(), min_size=1, max_size=4)) +def test_net_income_does_not_fall_where_savings_credit_ends(families): + g = grid(families) + net = ( + g["household_net_income"] + + g["tv_licence"] + - g["winter_fuel_allowance"] + - g["pawhp"] + ) + step = np.diff(net, axis=1) + ends = g["savings_credit_only"][:, :-1] & (g["pension_credit"][:, 1:] <= 0) + assert ends.any(axis=1).all(), families + for i, fam in enumerate(families): + for k in np.flatnonzero(ends[i]): + assert step[i, k] >= -0.01, ( + *describe(fam, g, i, k), + f"net income change {step[i, k]:.2f}", + ) + + +def deemed_income(capital): + steps = np.ceil(np.maximum(0, capital - CAPITAL_THRESHOLD) / CAPITAL_STEP) + return steps * WEEKS + + +@PROPERTY_SETTINGS +@given(st.lists(family(), min_size=1, max_size=4)) +def test_income_follows_the_route_for_the_award(families): + g = grid(families) + savings = np.array([fam["savings"] for fam in families])[:, None] + capital_income = deemed_income(savings) + pensions_after_tax = ( + g["state_pension"] + g["private_pension_income"] - g["income_tax"] + ) + disregard = g["housing_benefit_applicable_income_disregard"] + sc_only = g["savings_credit_only"] + gc_paid = g["guarantee_credit_paid"] + other = ~sc_only & ~gc_paid + income = g["housing_benefit_applicable_income"] + capital = np.broadcast_to(savings, income.shape) + capital_income = np.broadcast_to(capital_income, income.shape) + + # Savings credit only: the Secretary of State's income (pensions after + # tax plus deemed income on savings) plus the savings credit paid. + pension_credit_income = pensions_after_tax + capital_income + expected = np.maximum(0, pension_credit_income + g["pension_credit"] - disregard) + assert np.allclose(income[sc_only], expected[sc_only], atol=0.01) + assert np.all(g["housing_benefit_tariff_income"][sc_only] == 0) + assert np.allclose( + g["housing_benefit_assessable_capital"][sc_only], capital[sc_only], atol=0.01 + ) + # Guarantee credit: the whole income is disregarded. + assert np.all(income[gc_paid] == 0) + # Otherwise the Housing Benefit rules, with tariff income on savings. + expected = np.maximum(0, pensions_after_tax + capital_income - disregard) + assert np.allclose(income[other], expected[other], atol=0.01) + assert np.allclose( + g["housing_benefit_tariff_income"][other], capital_income[other], atol=0.01 + ) + + +def single_pensioner(**person): + return { + "people": {"pensioner": person}, + "benunits": {"benunit": {"members": ["pensioner"]}}, + "households": { + "household": { + "members": ["pensioner"], + "country": {YEAR: "ENGLAND"}, + "local_authority": {YEAR: "MAIDSTONE"}, + "region": {YEAR: "NORTH_WEST"}, + "tenure_type": {YEAR: "RENT_FROM_COUNCIL"}, + "rent": {YEAR: 11_117.22}, + "council_tax": {YEAR: 2_782.16}, + "savings": {YEAR: 0.0}, + } + }, + } + + +def test_attendance_allowance_case_no_longer_loses_net_income(): + # The counterexample that found the gap: single, aged 88, blind, on + # Attendance Allowance at the higher rate, State Pension 11,609.88. + def calculate(private_pension): + simulation = Simulation( + situation=single_pensioner( + age={YEAR: 88}, + is_blind={YEAR: True}, + aa_category={YEAR: "HIGHER"}, + state_pension={YEAR: 11_609.88}, + private_pension_income={YEAR: private_pension}, + ) + ) + return { + variable: float(simulation.calculate(variable, YEAR).sum()) + for variable in [ + "savings_credit", + "housing_benefit_applicable_income", + "housing_benefit_applicable_income_disregard", + "housing_benefit", + "household_net_income", + "tv_licence", + ] + } + + before, after = calculate(9_000), calculate(9_500) + # 400 more after income tax; savings credit falls by 0.4 x 183.22 to nil. + assert abs(before["savings_credit"] - 73.29) < 0.01 + assert after["savings_credit"] == 0 + # 19,001.90 of Pension Credit income + 73.29 of savings credit, less the + # earnings disregard, which applies on both routes. + assert ( + abs( + before["housing_benefit_applicable_income"] + + before["housing_benefit_applicable_income_disregard"] + - 19_075.19 + ) + < 0.01 + ) + # 0.65 x (19,401.90 - 19,075.19) = 212.36. + assert abs(before["housing_benefit"] - after["housing_benefit"] - 212.36) < 0.01 + # Excluding the TV licence (free for over-75s on Pension Credit), net + # income rises. Before Housing Benefit and council tax reduction counted + # the savings credit, it fell by 13.29. + change = (after["household_net_income"] + after["tv_licence"]) - ( + before["household_net_income"] + before["tv_licence"] + ) + assert change > 0 + + +def test_savings_credit_only_award_counts_the_frozen_savings_credit(): + # A savings credit rate rise raises the reform's savings credit from + # 718.62 to 1,029.89, but the Pension Credit freeze keeps paying the + # baseline 718.62, and that is the savings credit payable. + data = single_pensioner( + age={YEAR: 80}, + state_pension={YEAR: 11_000}, + private_pension_income={YEAR: 2_000}, + ) + data["benunits"]["benunit"]["housing_benefit_applicable_income_disregard"] = { + YEAR: 0 + } + period = f"{YEAR}-01-01.{YEAR}-12-31" + rate_rise = {"gov.dwp.pension_credit.savings_credit.rate.phase_in": {period: 0.8}} + frozen = Simulation( + situation=data, + reform={**rate_rise, "gov.contrib.freeze_pension_credit": {period: True}}, + ) + unfrozen = Simulation(situation=data, reform=rate_rise) + assert abs(frozen.calculate("savings_credit", YEAR)[0] - 1_029.89) < 0.01 + assert abs(frozen.calculate("pension_credit", YEAR)[0] - 718.62) < 0.01 + assert frozen.calculate("in_receipt_of_savings_credit_only", YEAR)[0] + # 12,914 of Pension Credit income + 718.62 paid. + frozen_income = frozen.calculate("housing_benefit_applicable_income", YEAR)[0] + assert abs(frozen_income - 13_632.62) < 0.01 + # Without the freeze the higher savings credit is paid and counted. + unfrozen_income = unfrozen.calculate("housing_benefit_applicable_income", YEAR)[0] + assert abs(unfrozen_income - 13_943.89) < 0.01 + + +def test_private_pension_rise_on_savings_credit_only_raises_net_income(): + # The property's shrunk counterexample on main: single, aged 80, no State + # Pension, council rent 3,000, private pension 13,500 -> 13,750. The award + # is savings credit only at both ends, so the TV licence is free at both. + def calculate(private_pension): + data = single_pensioner( + age={YEAR: 80}, + state_pension={YEAR: 0}, + private_pension_income={YEAR: private_pension}, + ) + data["households"]["household"]["rent"] = {YEAR: 3_000} + data["households"]["household"]["council_tax"] = {YEAR: 500} + simulation = Simulation(situation=data) + return { + variable: float(simulation.calculate(variable, YEAR).sum()) + for variable in [ + "savings_credit", + "housing_benefit", + "council_tax_benefit", + "household_net_income", + "tv_licence", + ] + } + + before, after = calculate(13_500), calculate(13_750) + change = {variable: after[variable] - before[variable] for variable in before} + # 250 more, 200 after basic-rate income tax; savings credit -0.4 x 200. + assert abs(change["savings_credit"] + 80) < 0.01 + # Housing Benefit -0.65 x (200 - 80); main took -0.65 x 200 = -130. + assert abs(change["housing_benefit"] + 78) < 0.01 + # Council tax reduction -0.2 x (200 - 80). + assert abs(change["council_tax_benefit"] + 24) < 0.01 + # 200 - 80 - 78 - 24 = +18; on main 200 - 80 - 130 - 24 = -34. + assert after["tv_licence"] == before["tv_licence"] == 0 + assert abs(change["household_net_income"] - 18) < 0.01 diff --git a/policyengine_uk/tests/test_pension_credit_carer_income_properties.py b/policyengine_uk/tests/test_pension_credit_carer_income_properties.py index ac8cd72622..b0d4fcf75c 100644 --- a/policyengine_uk/tests/test_pension_credit_carer_income_properties.py +++ b/policyengine_uk/tests/test_pension_credit_carer_income_properties.py @@ -23,7 +23,10 @@ 1. Differential: Pension Credit and pension-age Housing Benefit assess the same income. Where Guarantee Credit is not paid, Housing Benefit - applicable income plus its income disregard equals Pension Credit income. + applicable income plus its income disregard equals Pension Credit income + plus any Pension Credit paid, which is then savings credit: a + savings-credit-only award's Housing Benefit income is the Pension Credit + income plus the savings credit payable (HB (SPC) Regs 2006 reg 27(4)(a)). The draws have no earnings, no pension contributions and capital of at most £10,000, so neither tariff income nor the earnings rules apply. 2. Boundary: on the private-pension step where Guarantee Credit ends, @@ -32,7 +35,10 @@ cliff, so it is excluded. 3. Metamorphic: setting the Scottish Carer Supplement to zero changes Pension Credit income and Housing Benefit applicable income only through - income tax: each plus income tax is unchanged. This encodes the model's + income tax: each plus income tax is unchanged, Housing Benefit income + after taking off the Pension Credit paid (a savings-credit-only award's + savings credit, which reg 27(4)(a) adds and which itself moves with the + tax on the supplement). This encodes the model's current behaviour, not the law's. The law disregards tax only on income taken into account (SPC Regs reg 17(10); HB (SPC) Regs 2006 reg 33(12)), so the supplement should leave both measures unchanged outright, while @@ -181,6 +187,7 @@ def by_benunit(variable): "tv_licence", "guarantee_credit", "pension_credit_income", + "pension_credit", "housing_benefit_eligible", "housing_benefit_applicable_income", "housing_benefit_applicable_income_disregard", @@ -221,7 +228,10 @@ def test_pension_credit_and_housing_benefit_assess_the_same_carer_income(familie before_disregard = ( hb_income + g["housing_benefit_applicable_income_disregard"][i] ) - pc_income = g["pension_credit_income"][i] + # Pension Credit paid without Guarantee Credit is savings credit only, + # which HB (SPC) Regs 2006 reg 27(4)(a) adds to the Pension Credit + # income. + pc_income = g["pension_credit_income"][i] + g["pension_credit"][i] assert np.allclose( before_disregard[compared], pc_income[compared], atol=0.01 ), (fam, before_disregard[compared], pc_income[compared]) @@ -266,6 +276,11 @@ def test_scottish_carer_supplement_is_not_means_tested_income(families): for variable in ["pension_credit_income", "housing_benefit_applicable_income"]: with_supplement = g[variable][i] + g["income_tax"][i] without_supplement = without[variable][i] + without["income_tax"][i] + if variable == "housing_benefit_applicable_income": + # Reg 27(4)(a) adds a savings-credit-only award's savings + # credit, which moves with the tax on the supplement. + with_supplement = with_supplement - g["pension_credit"][i] + without_supplement = without_supplement - without["pension_credit"][i] # Only where the zero floor does not bind in either run. compared = (g[variable][i] > 0) & (without[variable][i] > 0) assert np.allclose( diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 7d522ac645..7474b01981 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,12 +10,20 @@ class housing_benefit_applicable_income(Variable): "nil for a family in receipt of Income Support, income-based " "Jobseeker's Allowance or income-related Employment and Support " "Allowance, whose whole income is disregarded, and for a pension-age " - "family in receipt of the Pension Credit guarantee credit." + "family with a positive 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 charges and the earnings disregards. " + "Otherwise it is the family's income under the Housing Benefit rules." ) reference = ( "https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4", "https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4", "https://www.legislation.gov.uk/uksi/2006/213/regulation/5", + "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", ) definition_period = YEAR unit = GBP @@ -86,13 +94,22 @@ def formula(benunit, period, parameters): childcare_element = benunit( "housing_benefit_applicable_income_childcare_element", period ) - applicable_income = max_( + income_under_general_rules = max_( 0, increased_income_reduced_by_tax_and_pensions + tariff_income - disregard - childcare_element, ) + # SI 2006/214 reg 27 (NI: SR 2006/406 reg 25): where the award of + # Pension Credit is savings credit only, the Secretary of State's + # assessment of income is used instead, plus the savings credit. + savings_credit_only = benunit("in_receipt_of_savings_credit_only", period) + applicable_income = where( + savings_credit_only, + benunit("housing_benefit_savings_credit_only_income", period), + income_under_general_rules, + ) guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) # SI 2006/213 Sch 5 para 4 (NI: SR 2006/405 Sch 6 para 4) disregards # "the whole of his income" where a claimant is on income support, an diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py new file mode 100644 index 0000000000..74fb57f8e4 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py @@ -0,0 +1,85 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_savings_credit_only_income(Variable): + value_type = float + entity = BenUnit + label = ( + "Housing Benefit income where the Pension Credit award is savings credit only" + ) + documentation = ( + "Income for the Housing Benefit means test of a claimant who, or whose " + "partner, has a Pension Credit award of savings credit only. The " + "authority takes the Secretary of State's assessment of net income for " + "that award and modifies it only for the listed items. Modelled: the " + "savings credit payable is added, and childcare charges and the " + "Housing Benefit earnings disregards are deducted. Pension Credit " + "income in the model disregards no earnings, whereas the Secretary of " + "State's figure is after the Pension Credit disregards (£20 for a lone " + "parent, £5 single or £10 couple otherwise). Deducting the Housing " + "Benefit disregards (£25 for a lone parent, £5 or £10 otherwise, and " + "the additional £17.10) therefore gives that figure less the higher " + "Housing Benefit amounts the regulation allows: £5 more for a lone " + "parent, and the additional disregard. This holds while neither " + "programme's £20 disregards for disabled people, carers and some " + "occupations are modelled; for those earners the Secretary of State's " + "figure is after £20, so the omission changes the income assessed, the " + "savings credit payable and possibly whether the award is savings " + "credit only. Childcare charges are deducted as in the general " + "route, without the work condition or the cap at earnings. Housing " + "Benefit's own income and tariff income rules do not apply to the " + "Secretary of State's figure, which already counts Pension Credit " + "deemed income from capital. Not modelled: the " + "maintenance disregards (neither income definition counts " + "maintenance), the exempt work disregard, the income and capital of a " + "partner treated as a household member but not in the Pension Credit " + "assessment, income of a non-dependant treated as the claimant's, and " + "war pensions (Great Britain: local schemes under section 134(8) of " + "the Social Security Administration Act 1992; Northern Ireland: war " + "pension income above £10 disregarded under Schedule 6 paragraph 1); " + "the model has no war pension income." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/214/regulation/27", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/4", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/25", + "https://www.legislation.gov.uk/nisr/2006/406/schedule/5", + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI", + ) + + def formula(benunit, period, parameters): + # SI 2006/214 reg 27(1) and (3) (NI: SR 2006/406 reg 25): the + # Secretary of State's assessment of net income for the award. + pension_credit_income = benunit("pension_credit_income", period) + # Reg 27(4)(a): the savings credit payable. A savings-credit-only + # award pays only the savings credit, so this is the Pension Credit + # paid (under the Pension Credit freeze, the frozen amount). + savings_credit_payable = benunit("pension_credit", period) + # Reg 27(4)(b): childcare charges taken into account under reg + # 30(1)(c), here as the general route deducts them; the reg 31 work + # condition and the reg 30(1)(c) cap at earnings are not modelled. + childcare = benunit( + "housing_benefit_applicable_income_childcare_element", period + ) + # Reg 27(4)(c)(i) (Sch 4 para 2: £25 for a lone parent, against SPC + # Regs 2002 Sch VI para 1's £20) and (d) (Sch 4 para 9(1), the + # additional £17.10; the para 5A exempt work disregard is not + # modelled). NI: reg 25(4)(c)(i) and (d), Sch 5 paras 2 and 9(1). + # The model's Pension Credit income applies no Sch VI disregard, so + # deducting the whole Housing Benefit disregard also takes off the Sch + # VI amounts already inside the Secretary of State's figure: £20 for a + # lone parent (para 1) and £5 or £10 otherwise (para 5), which match + # Sch 4 paras 2 and 7 but for the lone parent's extra £5. Neither + # schedule's £20 disregards (Sch VI paras 2 to 4; Sch 4 paras 3 to 5) + # are modelled. If Sch 4 paras 3 to 5 are added to the disregard + # variable, this route needs the Sch VI grounds instead, as reg 27(4) + # does not let Housing Benefit's own £20 grounds replace them. + # Reg 27(5): Housing Benefit's own income rules, including tariff + # income from capital, do not apply to the Secretary of State's figure. + disregard = benunit("housing_benefit_applicable_income_disregard", period) + return max_( + 0, + pension_credit_income + savings_credit_payable - childcare - disregard, + ) 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..c15edabc14 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 @@ -32,4 +32,10 @@ def formula(benunit, period, parameters): excess_capital = max_(0, capital - threshold) steps = np.ceil(excess_capital / step) tariff_income = steps * amount * WEEKS_IN_YEAR + # SI 2006/214 reg 27(5) (NI: SR 2006/406 reg 25(5)): where the award of + # Pension Credit is savings credit only, Housing Benefit's tariff + # income rule does not apply; the Secretary of State's assessment of + # income already counts Pension Credit deemed income from capital. + savings_credit_only = benunit("in_receipt_of_savings_credit_only", period) + tariff_income = where(savings_credit_only, 0, tariff_income) return where(guarantee_credit, 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 5e68e5a8be..757f427c16 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_assessable_capital.py @@ -13,10 +13,12 @@ class housing_benefit_assessable_capital(Variable): "no weight. Person-level sources, such as a Lifetime ISA, count only for " "the holder's own benunit, and only when the holder is its claimant or " "partner (is_claimant_or_partner): a dependant's capital is not the " - "claimant's. It is nil for a family in receipt of Income Support, " - "income-based Jobseeker's Allowance or income-related Employment and " - "Support Allowance, and for a pension-age family in receipt of the " - "Pension Credit guarantee credit." + "claimant's. Where the Pension Credit award is savings credit only, the " + "Pension Credit assessment of capital is used instead. It is nil for a " + "family in receipt of Income Support, income-based Jobseeker's " + "Allowance or income-related Employment and Support Allowance, and for " + "a pension-age family with a positive guarantee credit (the guarantee " + "credit passport)." ) definition_period = YEAR unit = GBP @@ -25,6 +27,8 @@ class housing_benefit_assessable_capital(Variable): "https://www.legislation.gov.uk/uksi/2006/213/regulation/25", "https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5", "https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/27", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/25", ) def formula(benunit, period, parameters): @@ -52,6 +56,17 @@ def formula(benunit, period, parameters): / claimant_partner_divisor, 0, ) + # SI 2006/214 reg 27(6) (NI: SR 2006/406 reg 25(6)): where the award of + # Pension Credit is savings credit only, the Secretary of State's + # calculation of capital is used, and the £16,000 limit applies to it. + # The recalculation when capital rises above £16,000 during an + # assessed income period (reg 27(7) and (8)) is not modelled. + savings_credit_only = benunit("in_receipt_of_savings_credit_only", period) + capital = where( + savings_credit_only, + benunit("pension_credit_assessable_capital", period), + household_capital_proxy + person_capital, + ) guarantee_credit = any_over_SP_age & (benunit("guarantee_credit", period) > 0) # SI 2006/213 Sch 6 para 5 (NI: SR 2006/405 Sch 7 para 5) disregards # "the whole of his capital" where a claimant is on income support, an @@ -66,5 +81,5 @@ def formula(benunit, period, parameters): return where( guarantee_credit | on_income_related_benefit, 0, - max_(0, household_capital_proxy + person_capital), + max_(0, capital), )