diff --git a/changelog.d/ctr-income-prescribed-benefits.fixed.md b/changelog.d/ctr-income-prescribed-benefits.fixed.md new file mode 100644 index 0000000000..8591acdafc --- /dev/null +++ b/changelog.d/ctr-income-prescribed-benefits.fixed.md @@ -0,0 +1 @@ +Council Tax Reduction income now counts the applicant's and partner's industrial injuries disablement benefit, severe disablement allowance and incapacity benefit in every scheme (SI 2012/2885 Sch 1 para 16; SI 2012/2886 Sch paras 39 and 54; WSI 2013/3029 Sch 1 para 10 and Sch 6 para 17; SSI 2012/319 reg 27; SSI 2021/249 reg 57). diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml new file mode 100644 index 0000000000..ea63047128 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_prescribed_benefits.yaml @@ -0,0 +1,606 @@ +# Industrial injuries disablement benefit (IIDB), severe disablement allowance +# (SDA), incapacity benefit (IB) and war pensions in Council Tax Reduction +# income. +# +# Every scheme counts IIDB, SDA and IB: +# - England, pensioners: SI 2012/2885 Sch 1 para 16(1)(j), social security +# benefits other than those listed; (j)(v) excepts only IIDB's increases +# under SSCBA 1992 ss.104 and 105. +# - English working-age schemes: gross income (SI 2012/2886 Sch para 54(1)) +# less Sch 8, which disregards none of the three (para 14 disregards +# "attendance allowance", defined to include the ss.104 and 105 increases). +# Kingston upon Thames, Merton and Newham copy the default scheme; Oxford's +# para 24 counts income other than earnings unless its Appendix 5 lists it. +# - Wales: WSI 2013/3029 Sch 1 para 10(1)(j) and Sch 6 para 17(1) with Sch 9. +# - Scotland: SSI 2012/319 reg 27(1)(j) (pension age) and SSI 2021/249 reg +# 57(1)(b)(ix) to (xi) (working age, a closed list). +# +# War pensions and AFCS payments (afcs, war_widows_pension): +# - Scotland counts none of them: SSI 2012/319 reg 31(10) and Sch 3 para 1 +# disregard each payment in full, and SSI 2021/249 reg 57(1) does not name +# them. +# - England and Wales prescribe a £10 a week disregard (SI 2012/2885 Sch 5 +# para 1; WSI 2013/3029 Sch 4 para 1 and Sch 9 para 20) that councils may +# exceed (WSI 2013/3029 reg 34(5); LGFA 1992 Sch 1A para 2(11) and +# s.13A(1)(c)). The model takes the full disregard where it cannot see the +# council's choice. +# - Of the councils whose schemes the model reads, Westminster disregards war +# pensions in full and Oxford ignores them for applicants without Universal +# Credit. Newham's scheme keeps the £10, but its discretionary reduction +# policy (para 2.1) automatically adds what a full disregard would give. +# Kingston upon Thames's schedules keep the £10 while its para 36(6) speaks +# of a full disregard, and Merton's "in full" disregard is "subject to +# paragraph 40" (a £20 a week limit). The model approximates all of them +# with the full disregard. +# - Only the applicant's and partner's IIDB, SDA and IB count (SI 2012/2885 +# Sch 1 para 11; SSI 2021/249 reg 36(1)), not a dependant's. +# +# Weekly rates, 2026-27 (SI 2026/148 Sch 1, restating SSCBA 1992 Sch 4): +# disablement pension at 100% 233.90, 50% 116.95, 20% 46.78; long-term +# incapacity benefit 146.60; severe disablement allowance 103.85 plus the +# higher age related addition 15.50, 119.35 in all. Full new State Pension +# 241.30 (SI 2026/148 art 6(1)), 12,547.60 a year, below the 12,570 personal +# allowance. IIDB, SDA and war pensions are not taxed in the model; State +# Pension and IB are. Pension Credit standard minimum guarantee 238.00 +# (single). CTR applicable amounts: personal allowance 256.00 for a single +# person who reached pensionable age before 1 April 2021 (SI 2012/2885 Sch 2 +# para 1, as uprated by SI 2026/27 reg 6; SSI 2026/56 reg 6 in Scotland), so +# the pensioners with an award asserted below are 80; and 95.55 for a single +# person aged 25 or over in Scotland's working-age scheme (SSI 2026/56 reg +# 14). The reduction is withdrawn at 20% of income above the applicable +# amount. Council tax is 2,000 throughout. + +- name: IIDB counts as pension-age Council Tax Reduction income in England + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 80 + state_pension: 241.30 * 52 + iidb_reported: 116.95 * 52 + benunits: + benunit: + members: [pensioner] + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: true + income_tax: 0 + # 12,547.60 + 6,081.40 + council_tax_reduction_applicable_income: 241.30 * 52 + 116.95 * 52 + council_tax_reduction_applicable_amount: 256.00 * 52 + # 2,000 - 0.2 x (18,629.00 - 13,312.00) = 936.60 + council_tax_benefit: 2_000 - 0.2 * ((241.30 + 116.95 - 256.00) * 52) + +- name: IIDB counts as pension-age Council Tax Reduction income in Wales + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 241.30 * 52 + iidb_reported: 233.90 * 52 + benunits: + benunit: + members: [pensioner] + would_claim_pc: false + households: + household: + members: [pensioner] + country: WALES + region: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + income_tax: 0 + # 12,547.60 + 12,162.80 + council_tax_reduction_applicable_income: 241.30 * 52 + 233.90 * 52 + +- name: IIDB counts as pension-age Council Tax Reduction income in Scotland + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 80 + state_pension: 241.30 * 52 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [pensioner] + would_claim_pc: false + households: + household: + members: [pensioner] + country: SCOTLAND + region: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + income_tax: 0 + # 12,547.60 + 2,432.56 + council_tax_reduction_applicable_income: 241.30 * 52 + 46.78 * 52 + council_tax_reduction_applicable_amount: 256.00 * 52 + # 2,000 - 0.2 x (14,980.16 - 13,312.00) = 1,666.37 + council_tax_benefit: 2_000 - 0.2 * ((241.30 + 46.78 - 256.00) * 52) + +- name: SDA counts as working-age Council Tax Reduction income under the Westminster scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 55 + sda_reported: 1 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: ENGLAND + region: LONDON + local_authority: WESTMINSTER + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: false + # 103.85 + 15.50 + sda: 119.35 * 52 + council_tax_reduction_applicable_income: 119.35 * 52 + +- name: IB counts as working-age Council Tax Reduction income in Wales + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 55 + incapacity_benefit_reported: 146.60 * 52 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: WALES + region: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + # 7,623.20 is below the personal allowance. + income_tax: 0 + council_tax_reduction_applicable_income: 146.60 * 52 + +- name: IIDB and IB count as working-age Council Tax Reduction income in Scotland + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 50 + iidb_reported: 46.78 * 52 + incapacity_benefit_reported: 146.60 * 52 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: SCOTLAND + region: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: false + income_tax: 0 + # 2,432.56 + 7,623.20 + council_tax_reduction_applicable_income: (46.78 + 146.60) * 52 + council_tax_reduction_applicable_amount: 95.55 * 52 + # 2,000 - 0.2 x (10,055.76 - 4,968.60) = 982.57 + council_tax_benefit: 2_000 - 0.2 * ((46.78 + 146.60 - 95.55) * 52) + +- name: Taxable IB counts net of income tax under the Westminster scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 55 + incapacity_benefit_reported: 146.60 * 52 + private_pension_income: 10_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: ENGLAND + region: LONDON + local_authority: WESTMINSTER + council_tax: 2_000 + savings: 0 + output: + # 0.2 x (7,623.20 + 10,000 - 12,570) = 1,010.64 + income_tax: 0.2 * (146.60 * 52 + 10_000 - 12_570) + # 17,623.20 - 1,010.64 = 16,612.56 + council_tax_reduction_applicable_income: 146.60 * 52 + 10_000 - 0.2 * (146.60 * 52 + 10_000 - 12_570) + +- name: War pensions do not count as pension-age Council Tax Reduction income in England + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 75 + state_pension: 241.30 * 52 + afcs_reported: 5_000 + war_widows_pension_reported: 3_000 + benunits: + benunit: + members: [pensioner] + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + income_tax: 0 + council_tax_reduction_applicable_income: 241.30 * 52 + +- name: War pensions do not count as working-age Council Tax Reduction income in Wales + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + afcs_reported: 5_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: WALES + region: WALES + local_authority: CARDIFF + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_applicable_income: 0 + +- name: War pensions do not count as pension-age Council Tax Reduction income in Scotland + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 80 + state_pension: 241.30 * 52 + war_widows_pension_reported: 3_000 + benunits: + benunit: + members: [pensioner] + would_claim_pc: false + households: + household: + members: [pensioner] + country: SCOTLAND + region: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_applicable_income: 241.30 * 52 + +- name: War pensions do not count as working-age Council Tax Reduction income in Scotland + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 45 + afcs_reported: 5_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: SCOTLAND + region: SCOTLAND + local_authority: GLASGOW_CITY + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_applicable_income: 0 + +- name: The Westminster working-age scheme disregards war pensions in full + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + afcs_reported: 5_000 + war_widows_pension_reported: 3_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: ENGLAND + region: LONDON + local_authority: WESTMINSTER + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_applicable_income: 0 + +- name: War pensions do not count under the Kingston upon Thames working-age scheme + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + afcs_reported: 5_000 + war_widows_pension_reported: 3_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: ENGLAND + region: LONDON + local_authority: KINGSTON_UPON_THAMES + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_applicable_income: 0 + +- name: War pensions do not count for a pensioner in Newham + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + afcs_reported: 5_000 + benunits: + benunit: + members: [pensioner] + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + region: LONDON + local_authority: NEWHAM + council_tax: 2_000 + savings: 0 + output: + council_tax_reduction_pensioner: true + council_tax_reduction_applicable_income: 0 + +- name: A dependant's IIDB does not count as Council Tax Reduction income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 80 + state_pension: 241.30 * 52 + dependant: + age: 17 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [pensioner, dependant] + would_claim_pc: false + households: + household: + members: [pensioner, dependant] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # Only the applicant's and partner's income counts (Sch 1 para 11); + # pensioner schemes also exclude Child Benefit. + council_tax_reduction_applicable_income: 241.30 * 52 + +- name: Where a household head applies alone, another couple's IIDB in her benefit unit does not count + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + mother: + age: 45 + is_parent: true + is_household_head: false + iidb_reported: 233.90 * 52 + father: + age: 46 + is_parent: true + is_household_head: false + iidb_reported: 233.90 * 52 + child: + age: 10 + is_household_head: false + benunits: + family: + members: [grandmother, mother, father, child] + would_claim_uc: false + would_claim_pc: false + would_claim_child_benefit: false + households: + home: + members: [grandmother, mother, father, child] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + # The grandmother is the applicant and has no partner here (SI 2012/2885 + # Sch 1 para 11); the parents are not her partners. + council_tax_reduction_head_applies_alone: true + is_council_tax_reduction_applicant_or_partner: [true, false, false, false] + council_tax_reduction_applicable_income: 0 + +- name: Where a household head applies alone, her own IIDB counts + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + iidb_reported: 116.95 * 52 + mother: + age: 45 + is_parent: true + is_household_head: false + father: + age: 46 + is_parent: true + is_household_head: false + child: + age: 10 + is_household_head: false + benunits: + family: + members: [grandmother, mother, father, child] + would_claim_uc: false + would_claim_pc: false + would_claim_child_benefit: false + households: + home: + members: [grandmother, mother, father, child] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_head_applies_alone: true + council_tax_reduction_applicable_income: 116.95 * 52 + +- name: Where a household head applies alone, her own IIDB counts with her State Pension, and the father's does not + # Only the merged code counts both: the head's State Pension through the + # applicant's member mask, her IIDB through the applicant-and-partner mask. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandmother: + age: 80 + is_household_head: true + state_pension: 241.30 * 52 + iidb_reported: 46.78 * 52 + mother: + age: 45 + is_parent: true + is_household_head: false + father: + age: 46 + is_parent: true + is_household_head: false + iidb_reported: 233.90 * 52 + child: + age: 10 + is_household_head: false + benunits: + family: + members: [grandmother, mother, father, child] + would_claim_uc: false + would_claim_pc: false + would_claim_child_benefit: false + households: + home: + members: [grandmother, mother, father, child] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 1_800 + savings: 0 + output: + council_tax_reduction_head_applies_alone: true + is_council_tax_reduction_applicant_or_partner: [true, false, false, false] + council_tax_reduction_pensioner: true + # State Pension 12,547.60, below the 12,570 personal allowance. + income_tax: [0, 0, 0, 0] + # State Pension 241.30 * 52 = 12,547.60, plus her IIDB at 20% + # 46.78 * 52 = 2,432.56: 14,980.16. The father's 12,162.80 IIDB is not + # hers (SI 2012/2885 Sch 1 para 11). + council_tax_reduction_applicable_income: 14_980.16 + # A single pensioner who reached pensionable age before April 2021: + # 256.00 * 52 = 13,312.00. + council_tax_reduction_applicable_amount: 13_312 + # 1,800 - 0.2 x (14,980.16 - 13,312.00) = 1,800 - 333.632 = 1,466.368. + # The couple are in her benefit unit, so the model charges no + # non-dependant deduction for them (#2017). + council_tax_benefit: 1_466.368 + +- name: A guarantee credit recipient with IIDB has nil Council Tax Reduction income + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 150 * 52 + iidb_reported: 46.78 * 52 + benunits: + benunit: + members: [pensioner] + would_claim_pc: true + households: + household: + members: [pensioner] + country: ENGLAND + region: SOUTH_EAST + local_authority: MAIDSTONE + council_tax: 2_000 + savings: 0 + output: + # 7,800 + 2,432.56 = 10,232.56, or 196.78 a week, below the 238.00 + # minimum guarantee (SPC Regs 2002 reg 15(1) counts IIDB). + pension_credit_income: (150 + 46.78) * 52 + in_receipt_of_guarantee_credit: true + # SI 2012/2885 Sch 1 para 13: the whole income is disregarded. + council_tax_reduction_applicable_income: 0 + council_tax_benefit: 2_000 diff --git a/policyengine_uk/tests/test_council_tax_reduction_prescribed_benefits_properties.py b/policyengine_uk/tests/test_council_tax_reduction_prescribed_benefits_properties.py new file mode 100644 index 0000000000..3bfc7883ef --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_prescribed_benefits_properties.py @@ -0,0 +1,466 @@ +"""IIDB, SDA, incapacity benefit and war pensions in Council Tax Reduction income. + +Council Tax Reduction income left out industrial injuries disablement benefit +(IIDB), severe disablement allowance (SDA) and incapacity benefit (IB), which +every scheme the model runs counts: + +- England, pensioners: SI 2012/2885 Sch 1 para 16(1)(j), social security + benefits other than those it lists; (j)(v) excepts only IIDB's increases + under SSCBA 1992 ss.104 and 105. +- English working-age schemes: gross income (SI 2012/2886 Sch para 54(1)) + less Sch 8, which disregards none of the three; Kingston upon Thames, Merton + and Newham copy it, and Oxford counts income other than earnings unless its + Appendix 5 lists it. +- Wales: WSI 2013/3029 Sch 1 para 10(1)(j) and Sch 6 para 17(1) with Sch 9. +- Scotland: SSI 2012/319 reg 27(1)(j) and SSI 2021/249 reg 57(1)(b)(ix)-(xi). + +Pension Credit counts them too (SPC Regs 2002 reg 15(1)), so once the +guarantee credit passport ends, Council Tax Reduction was assessed on less +income than Pension Credit and Housing Benefit. + +Only the applicant's and partner's benefits count (SI 2012/2885 Sch 1 para 11; +SSI 2021/249 reg 36(1)), not a dependant's. + +War pensions and Armed Forces Compensation Scheme payments stay uncounted. +Scotland disregards or omits them (SSI 2012/319 Sch 3 para 1; SSI 2021/249 reg +57(1)), and English and Welsh councils may relieve more than the prescribed +£10 a week (WSI 2013/3029 reg 34(5); LGFA 1992 Sch 1A para 2(11) and +s.13A(1)(c)). The councils whose schemes the model reads mostly do: Westminster +and Oxford disregard them in full, Newham's discretionary policy adds what a +full disregard would give, and Kingston upon Thames's and Merton's texts point +both ways. The model approximates all of them with the full disregard. + +Invariants, in 2026, for single people and couples not claiming Universal +Credit (whose award would move with the benefits), with no children except in +invariant 4: + +1. Differential: Council Tax Reduction and Pension Credit assess the same + income. For pension-age families under the national pensioner schemes in + England, Wales and Scotland, with State Pension, private pension, IIDB, IB + and SDA, no earnings, no pension contributions and savings of at most + £10,000 (so neither scheme's deemed income from capital applies): + Council Tax Reduction income is nil where guarantee credit is received + (SI 2012/2885 Sch 1 para 13 and equivalents), Pension Credit income plus + the savings credit where the award is savings credit only (para 14), and + Pension Credit income otherwise. +2. Pound for pound, every scheme: raising IIDB or IB by d, or awarding SDA, + raises Council Tax Reduction income plus the family's income tax and + National Insurance by exactly the added amount, wherever the general rules + apply before and after and the zero floor does not bind. The reduction + never rises where the route (general rules, savings credit only, guarantee + credit) is unchanged; in the national schemes, where both awards are + strictly between nil and the liability, it falls by 20% of the rise in + income. +3. War pensions: adding war pensions and Armed Forces Compensation Scheme + payments leaves Council Tax Reduction income unchanged under the general + rules, in every scheme. +4. Dependants: giving a dependant aged 16 to 19 IIDB, IB within the personal + allowance, or SDA leaves Council Tax Reduction income unchanged under the + general rules, in every scheme. + +These compare the model's own measures. Invariant 1 relies on both measures +deducting the family's whole income tax and National Insurance, an existing +approximation in each. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +WEEKS = 52 +# 2026-27 weekly rates (SI 2026/148 Sch 1). +IIDB_FULL = 233.90 * WEEKS +LONG_TERM_IB = 146.60 * WEEKS +SDA = (103.85 + 15.50) * WEEKS +WITHDRAWAL_RATE = 0.2 +PRIVATE_PENSIONS = np.arange(0, 20_001, 1_000) +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Areas of the national pensioner schemes, and of the working-age schemes. +PENSION_AGE_AREAS = { + "MAIDSTONE": ("ENGLAND", "SOUTH_EAST"), + "KINGSTON_UPON_THAMES": ("ENGLAND", "LONDON"), + "NEWHAM": ("ENGLAND", "LONDON"), + "CARDIFF": ("WALES", "WALES"), + "GLASGOW_CITY": ("SCOTLAND", "SCOTLAND"), +} +WORKING_AGE_AREAS = { + "WESTMINSTER": ("ENGLAND", "LONDON"), + "MERTON": ("ENGLAND", "LONDON"), + "KINGSTON_UPON_THAMES": ("ENGLAND", "LONDON"), + "NEWHAM": ("ENGLAND", "LONDON"), + "OXFORD": ("ENGLAND", "SOUTH_EAST"), + "CARDIFF": ("WALES", "WALES"), + "GLASGOW_CITY": ("SCOTLAND", "SCOTLAND"), +} +NATIONAL_SCHEME_AREAS = {"MAIDSTONE", "CARDIFF", "GLASGOW_CITY"} +BENUNIT_VARIABLES = [ + "council_tax_reduction_applicable_income", + "council_tax_benefit", + "council_tax_reduction_pensioner", + "council_tax_reduction_relevant_income_based_benefit", + "in_receipt_of_guarantee_credit", + "in_receipt_of_savings_credit_only", + "pension_credit_income", + "pension_credit", +] +PERSON_TOTALS = ["income_tax", "national_insurance"] + + +def money(high): + return st.floats(0, high, allow_nan=False, allow_infinity=False) + + +@st.composite +def benefits(draw): + """IIDB at a 20% to 100% assessment, IB up to the long-term rate, SDA.""" + return dict( + iidb=draw(st.one_of(st.just(0.0), money(IIDB_FULL))), + incapacity_benefit=draw(st.one_of(st.just(0.0), money(LONG_TERM_IB))), + sda=draw(st.booleans()), + ) + + +@st.composite +def pension_age_family(draw, areas=tuple(PENSION_AGE_AREAS)): + adults = draw(st.integers(1, 2)) + return dict( + ages=[draw(st.integers(67, 95)) for _ in range(adults)], + state_pensions=[draw(money(15_000)) for _ in range(adults)], + earnings=[0.0] * adults, + benefits=[draw(benefits()) for _ in range(adults)], + property_income=draw(st.one_of(st.just(0.0), money(5_000))), + area=draw(st.sampled_from(areas)), + council_tax=draw(st.floats(500, 4_000)), + savings=draw(st.one_of(st.just(0.0), money(10_000))), + would_claim_pc=draw(st.booleans()), + ) + + +@st.composite +def working_age_family(draw): + adults = draw(st.integers(1, 2)) + return dict( + ages=[draw(st.integers(25, 60)) for _ in range(adults)], + state_pensions=[0.0] * adults, + earnings=[draw(st.one_of(st.just(0.0), money(30_000))) for _ in range(adults)], + benefits=[draw(benefits()) for _ in range(adults)], + property_income=draw(st.one_of(st.just(0.0), money(5_000))), + area=draw(st.sampled_from(tuple(WORKING_AGE_AREAS))), + council_tax=draw(st.floats(500, 4_000)), + savings=draw(st.one_of(st.just(0.0), money(5_000))), + would_claim_pc=False, + ) + + +any_family = st.one_of(pension_age_family(), working_age_family()) + +# A single pensioner off Pension Credit with every benefit and State Pension +# below the personal allowance, so the general rules apply in every cell. +GENERAL_RULES_PENSIONER = dict( + ages=[75], + state_pensions=[9_000.0], + earnings=[0.0], + benefits=[dict(iidb=IIDB_FULL * 0.5, incapacity_benefit=LONG_TERM_IB, sda=True)], + property_income=0.0, + area="MAIDSTONE", + council_tax=2_000.0, + savings=0.0, + would_claim_pc=False, +) +# A pensioner whose reduction is partial before and after each change, so the +# 20% taper in invariant 2 is checked. +PARTIAL_AWARD_PENSIONER = dict( + ages=[70], + state_pensions=[241.30 * WEEKS], + earnings=[0.0], + benefits=[dict(iidb=0.0, incapacity_benefit=0.0, sda=False)], + property_income=0.0, + area="MAIDSTONE", + council_tax=2_000.0, + savings=0.0, + would_claim_pc=False, +) +# A working-age couple on the Kingston upon Thames scheme. +KINGSTON_COUPLE = dict( + ages=[40, 40], + state_pensions=[0.0, 0.0], + earnings=[8_000.0, 0.0], + benefits=[dict(iidb=IIDB_FULL * 0.3, incapacity_benefit=0.0, sda=False)] * 2, + property_income=0.0, + area="KINGSTON_UPON_THAMES", + council_tax=2_000.0, + savings=0.0, + would_claim_pc=False, +) + + +# A working-age lone adult in Westminster, close in age to a dependant. +YOUNG_PARENT = dict( + ages=[32], + state_pensions=[0.0], + earnings=[12_000.0], + benefits=[dict(iidb=0.0, incapacity_benefit=0.0, sda=False)], + property_income=0.0, + area="WESTMINSTER", + council_tax=2_000.0, + savings=0.0, + would_claim_pc=False, +) + + +def area_of(fam): + if fam["ages"][0] >= 67: + return PENSION_AGE_AREAS[fam["area"]] + return WORKING_AGE_AREAS[fam["area"]] + + +def situation(variants): + """One benefit unit and household per (family, private pension, change). + + variants: list of (family, private pension of the first adult, extra + person inputs for the first adult). + """ + people, benunits, households = {}, {}, {} + for i, (fam, private_pension, extra) in enumerate(variants): + country, region = area_of(fam) + names = [] + for j, age in enumerate(fam["ages"]): + name = f"p{i}_{j}" + b = fam["benefits"][j] + person = { + "age": {YEAR: age}, + # The generated adults are the claimant and partner, and any + # dependant is not: say so for everyone, so the model's + # presumption (a member under 20 and 16 or more years younger + # than the claimant is their child) cannot read a dependant + # close in age to the claimant as a partner. + "is_claimant_or_partner": {YEAR: True}, + "state_pension": {YEAR: fam["state_pensions"][j]}, + "employment_income": {YEAR: fam["earnings"][j]}, + "private_pension_income": {YEAR: private_pension * (j == 0)}, + "property_income": {YEAR: fam["property_income"] * (j == 0)}, + "iidb_reported": {YEAR: b["iidb"]}, + "incapacity_benefit_reported": {YEAR: b["incapacity_benefit"]}, + "sda_reported": {YEAR: float(b["sda"])}, + } + for variable, value in extra.get(j, {}).items(): + person[variable] = {YEAR: value} + people[name] = person + names.append(name) + if fam.get("dependant_age") is not None: + j = len(fam["ages"]) + name = f"p{i}_{j}" + people[name] = { + "age": {YEAR: fam["dependant_age"]}, + "is_claimant_or_partner": {YEAR: False}, + } + for variable, value in extra.get(j, {}).items(): + people[name][variable] = {YEAR: value} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "would_claim_pc": {YEAR: fam["would_claim_pc"]}, + "would_claim_uc": {YEAR: False}, + } + households[f"h{i}"] = { + "members": names, + "country": {YEAR: country}, + "region": {YEAR: region}, + "local_authority": {YEAR: fam["area"]}, + "tenure_type": {YEAR: "OWNED_OUTRIGHT"}, + "council_tax": {YEAR: fam["council_tax"]}, + "savings": {YEAR: fam["savings"]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(variants): + simulation = Simulation(situation=situation(variants)) + values = {v: np.asarray(simulation.calculate(v, YEAR)) for v in BENUNIT_VARIABLES} + values["tax"] = sum( + np.asarray(simulation.calculate(v, YEAR, map_to="benunit")) + for v in PERSON_TOTALS + ) + values["liability"] = np.asarray( + simulation.calculate("council_tax_reduction_maximum_eligible_liability", YEAR) + ) + return values + + +def route(values): + """0: general rules; 1: savings credit only; 2: guarantee credit.""" + gc = values["in_receipt_of_guarantee_credit"].astype(bool) + sc_only = values["in_receipt_of_savings_credit_only"].astype(bool) + return np.where(gc, 2, np.where(sc_only, 1, 0)) + + +@PROPERTY_SETTINGS +@given(st.lists(pension_age_family(), min_size=1, max_size=4)) +@example([GENERAL_RULES_PENSIONER]) +def test_council_tax_reduction_and_pension_credit_assess_the_same_income( + families, +): + variants = [(fam, p, {}) for fam in families for p in PRIVATE_PENSIONS] + v = calculate(variants) + pensioner = v["council_tax_reduction_pensioner"].astype(bool) + assert np.all(pensioner) + ctr_income = v["council_tax_reduction_applicable_income"] + pc_income = v["pension_credit_income"] + r = route(v) + expected = np.select( + [r == 2, r == 1], [0.0, pc_income + v["pension_credit"]], default=pc_income + ) + assert np.allclose(ctr_income, expected, atol=0.01), [ + (variants[k][0], variants[k][1], ctr_income[k], expected[k], r[k]) + for k in np.flatnonzero(~np.isclose(ctr_income, expected, atol=0.01))[:3] + ] + if families == [GENERAL_RULES_PENSIONER]: + # Not vacuous: the general rules apply and count every benefit. + assert np.all(r == 0) + b = GENERAL_RULES_PENSIONER["benefits"][0] + counted = 9_000 + b["iidb"] + b["incapacity_benefit"] + SDA + assert np.isclose(ctr_income[0], counted - v["tax"][0], atol=0.01) + + +CHANGES = { + "iidb": lambda b: {"iidb_reported": b["iidb"] + 0.2 * IIDB_FULL}, + "incapacity_benefit": lambda b: { + "incapacity_benefit_reported": b["incapacity_benefit"] + 0.3 * LONG_TERM_IB + }, + "sda": lambda b: {"sda_reported": 1.0}, +} +ADDED = { + "iidb": lambda b: 0.2 * IIDB_FULL, + "incapacity_benefit": lambda b: 0.3 * LONG_TERM_IB, + "sda": lambda b: 0.0 if b["sda"] else SDA, +} + + +@PROPERTY_SETTINGS +@given( + st.lists(any_family, min_size=1, max_size=6), + st.sampled_from(sorted(CHANGES)), +) +@example( + [GENERAL_RULES_PENSIONER, KINGSTON_COUPLE, PARTIAL_AWARD_PENSIONER], + "incapacity_benefit", +) +@example([PARTIAL_AWARD_PENSIONER], "iidb") +def test_each_benefit_counts_pound_for_pound_in_every_scheme(families, change): + variants = [] + for fam in families: + b = fam["benefits"][0] + variants.append((fam, 5_000.0, {})) + variants.append((fam, 5_000.0, {0: CHANGES[change](b)})) + v = calculate(variants) + base, plus = slice(0, None, 2), slice(1, None, 2) + added = np.array([ADDED[change](fam["benefits"][0]) for fam in families]) + income = v["council_tax_reduction_applicable_income"] + with_tax = income + v["tax"] + r = route(v) + passported = v["council_tax_reduction_relevant_income_based_benefit"].astype(bool) + general = (r[base] == 0) & (r[plus] == 0) & ~passported[base] & ~passported[plus] + unfloored = general & (income[base] > 0) + assert np.allclose( + (with_tax[plus] - with_tax[base])[unfloored], added[unfloored], atol=0.01 + ), (change, [f for f, u in zip(families, unfloored) if u]) + ctr = v["council_tax_benefit"] + same_route = r[base] == r[plus] + assert np.all((ctr[plus] <= ctr[base] + 0.01)[same_route]), change + national = np.array([fam["area"] in NATIONAL_SCHEME_AREAS for fam in families]) + liability = v["liability"] + partial = ( + general + & national + & (ctr[base] > 0.01) + & (ctr[plus] > 0.01) + & (ctr[base] < liability[base] - 0.01) + & (ctr[plus] < liability[plus] - 0.01) + ) + assert np.allclose( + (ctr[base] - ctr[plus])[partial], + (WITHDRAWAL_RATE * (income[plus] - income[base]))[partial], + atol=0.01, + ), change + if PARTIAL_AWARD_PENSIONER in families: + assert partial[families.index(PARTIAL_AWARD_PENSIONER)] + + +@st.composite +def war_pensions(draw): + return { + "afcs_reported": draw(money(15_000)), + "war_widows_pension_reported": draw(st.one_of(st.just(0.0), money(8_000))), + } + + +@PROPERTY_SETTINGS +@given(st.data()) +def test_war_pensions_never_count(data): + # Every draw includes the Kingston couple, so a council whose schedules + # keep the £10 is always exercised. + families = data.draw(st.lists(any_family, min_size=1, max_size=6)) + families = families + [KINGSTON_COUPLE] + payments = [[data.draw(war_pensions()) for _ in fam["ages"]] for fam in families] + payments[-1] = [ + {"afcs_reported": 5_000.0, "war_widows_pension_reported": 3_000.0} + ] * 2 + variants = [] + for fam, pay in zip(families, payments): + variants.append((fam, 5_000.0, {})) + variants.append((fam, 5_000.0, dict(enumerate(pay)))) + v = calculate(variants) + base, plus = slice(0, None, 2), slice(1, None, 2) + income = v["council_tax_reduction_applicable_income"] + r = route(v) + general = (r[base] == 0) & (r[plus] == 0) + assert general[-1] + rise = income[plus] - income[base] + assert np.allclose(rise[general], 0, atol=0.01), [ + (families[k]["area"], payments[k], rise[k]) + for k in np.flatnonzero(general & ~np.isclose(rise, 0, atol=0.01))[:3] + ] + + +DEPENDANT_CHANGES = { + "iidb": {"iidb_reported": 0.5 * IIDB_FULL}, + "incapacity_benefit": {"incapacity_benefit_reported": LONG_TERM_IB}, + "sda": {"sda_reported": 1.0}, +} + + +@PROPERTY_SETTINGS +@given( + st.lists(any_family, min_size=1, max_size=6), + st.sampled_from(sorted(DEPENDANT_CHANGES)), + st.integers(16, 19), +) +@example([GENERAL_RULES_PENSIONER, KINGSTON_COUPLE], "iidb", 17) +# Found by CI: a dependant of 19 is under 16 years younger than a claimant of +# 32, so without the declared roles the model read them as a partner. +@example([YOUNG_PARENT], "iidb", 19) +def test_dependants_benefits_never_count(families, change, dependant_age): + families = [{**fam, "dependant_age": dependant_age} for fam in families] + variants = [] + for fam in families: + dependant = len(fam["ages"]) + variants.append((fam, 5_000.0, {})) + variants.append((fam, 5_000.0, {dependant: DEPENDANT_CHANGES[change]})) + v = calculate(variants) + base, plus = slice(0, None, 2), slice(1, None, 2) + income = v["council_tax_reduction_applicable_income"] + r = route(v) + general = (r[base] == 0) & (r[plus] == 0) + rise = income[plus] - income[base] + assert np.allclose(rise[general], 0, atol=0.01), [ + (families[k]["area"], families[k]["ages"], change, rise[k]) + for k in np.flatnonzero(general & ~np.isclose(rise, 0, atol=0.01))[:3] + ] diff --git a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py index 67448845bd..cbdfaa5d61 100644 --- a/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py +++ b/policyengine_uk/tests/test_pension_credit_prescribed_benefits_properties.py @@ -54,6 +54,14 @@ household net income, excluding the TV licence fee, does not fall. The free TV licence for over-75s on Pension Credit is a genuine statutory cliff, so it is excluded, as in test_pension_credit_carer_income_properties.py. + Intended exception: Scottish pension-age Council Tax Reduction counts + Carer Support Payment before the CSP Regs reg 16(2) overlapping-benefit + reduction (SSI 2012/319 reg 27(3) and (5); see + council_tax_reduction_applicable_income), while Pension Credit counts it + after (SPC Regs reg 15(4)(g)). Where an overlapping benefit cuts the + payment, the passport ends on more Council Tax Reduction income than + Pension Credit counted, so the step can fall. Those families are left out + until that reading is decided. """ import numpy as np @@ -332,13 +340,20 @@ def test_net_income_does_not_fall_where_guarantee_credit_ends(families): "tv_licence", "housing_benefit", "council_tax_benefit", + "carer_support_payment_pre_overlap", + "carer_support_payment", ], ) step = np.diff(g["household_net_income"] + g["tv_licence"], axis=1) gc = g["guarantee_credit"] ends = (gc[:, :-1] > 0) & (gc[:, 1:] <= 0) + # The intended exception in invariant 4: Carer Support Payment cut by an + # overlapping benefit. + csp_cut = g["carer_support_payment_pre_overlap"] > g["carer_support_payment"] + 0.01 for i, fam in enumerate(families): for k in np.flatnonzero(ends[i]): + if csp_cut[i, k] or csp_cut[i, k + 1]: + continue assert step[i, k] >= -0.01, ( fam, f"private pension {PENSIONS[k]} -> {PENSIONS[k + 1]}", diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index a0be20927c..2f5010c9bf 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -26,7 +26,13 @@ class council_tax_reduction_applicable_income(Variable): "awards (Child Benefit, Income Support, income-based JSA, " "income-related ESA, Universal Credit, tax credits, Pension Credit) " "and its children belong to its claimant and partner, not to the " - "head, so none of them counts." + "head, so none of them counts. Industrial injuries disablement " + "benefit, severe disablement allowance and incapacity benefit count " + "in every scheme, for the applicant and partner only, not for " + "children or young persons. War pensions and Armed Forces " + "Compensation Scheme payments are not counted: Scotland disregards or " + "omits them, and English and Welsh councils may relieve them beyond " + "the prescribed £10 a week, so the model takes the full disregard." ) definition_period = YEAR unit = GBP @@ -49,6 +55,15 @@ class council_tax_reduction_applicable_income(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/5", "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/7", "https://www.legislation.gov.uk/ssi/2021/249/regulation/36", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/5/paragraph/1", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/39", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/54", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/34", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/17", + "https://www.legislation.gov.uk/ssi/2012/319/schedule/3/paragraph/1", + "https://www.legislation.gov.uk/ukpga/1992/14/schedule/1A/paragraph/2", + "https://www.legislation.gov.uk/ukpga/1992/14/section/13A", + "https://www.newham.gov.uk/downloads/file/209/discretionary-council-tax-reductions-for-individual-council-tax-payers", ] def formula(benunit, period, parameters): @@ -104,6 +119,57 @@ def formula(benunit, period, parameters): "statutory_maternity_pay", "ssmg", ] + # Every scheme the model runs counts these three benefits. At pension + # age they are social security benefits that the income lists do not + # except: SI 2012/2885 Sch 1 para 16(1)(j), the default scheme SI + # 2012/2886 Sch para 39(1)(j), WSI 2013/3029 Sch 1 para 10(1)(j) and + # SSI 2012/319 reg 27(1)(j). At working age England and Wales count + # gross income (SI 2012/2886 Sch para 54(1); WSI 2013/3029 Sch 6 para + # 17(1)) less disregards that do not list them (their Schs 8 and 9), + # and Scotland's closed list names them (SSI 2021/249 reg 57(1)(b)(ix) + # to (xi)). Each scheme leaves out IIDB's constant attendance and + # exceptionally severe disablement increases (SSCBA 1992 ss.104 and + # 105), by name or as "attendance allowance" (Sch 8 and Sch 9 para + # 14), but iidb is the whole reported payment and cannot separate + # them. Only the applicant's and partner's count (Sch 1 para 11; SSI + # 2021/249 reg 36(1)): the Council Tax Reduction applicant and partner, + # which is the household head alone where the head applies alone, and + # never the dependants that the wider mask above still reaches. + applicant_and_partner_benefits = ["iidb", "sda", "incapacity_benefit"] + applicant_and_partner = person( + "is_council_tax_reduction_applicant_or_partner", period + ) + # War disablement pensions, war widow's or widower's pensions and + # Armed Forces Compensation Scheme payments (afcs, war_widows_pension). + # Scotland counts none of them: SSI 2012/319 Sch 3 para 1 disregards + # each in full, and SSI 2021/249 reg 57(1), a closed list, does not + # name them. England and Wales prescribe a £10 a week disregard (SI + # 2012/2885 Sch 5 para 1; SI 2012/2886 Sch 6 para 1 and Sch 8 para 20; + # WSI 2013/3029 Sch 4 para 1 and Sch 9 para 20), which a council may + # exceed: Wales makes it a minimum for war disablement and war widow's + # or widower's pensions (WSI 2013/3029 reg 34(5)), an English scheme + # may make provision equivalent to the council tax benefit rules (LGFA + # 1992 Sch 1A para 2(11)), and any council may reduce council tax + # further (s.13A(1)(c)). The model takes the full disregard, as + # Housing Benefit does for local schemes under SSAA 1992 s.134(8)(a). + # The councils whose schemes the model reads mostly go further: + # Westminster's working-age and pensioner schemes disregard war + # pensions in full, and Oxford's Appendix 5 ignores war pensions for + # applicants without Universal Credit. Newham's scheme keeps the £10 + # (Sch 6 para 1, Sch 8 para 19), but since 2013 it adds automatically + # the reduction a full disregard of war compensation payments would + # give (its s.13A(1)(c) policy, para 2.1). Kingston upon Thames's + # schedules keep the £10 (Sch 6 para 1, Sch 8 para 20) while its para + # 36(6) speaks of "the higher amount of full disregard" of the same + # payments. Merton's Sch 8 para 20 disregards them "in full" but + # "subject to paragraph 40", whose £20 a week limit, read literally, + # caps that at working age. These are approximations of the councils' + # texts. The full disregard also over-disregards guaranteed income + # payments in Wales, which reg 34(5) does not cover, and wherever a + # scheme keeps the prescribed £10 without further relief; afcs does + # not separate guaranteed income payments from war disablement + # pensions, nor either from the constant attendance and severe + # disablement additions that every scheme disregards in full. income_components = [ "employment_income", "self_employment_income", @@ -137,6 +203,9 @@ def formula(benunit, period, parameters): personal_benefit_income = add_for_members( benunit, period, personal_benefits, members ) + personal_benefit_income += add_for_members( + benunit, period, applicant_and_partner_benefits, applicant_and_partner + ) credits = applicant_family * add(benunit, period, ["tax_credits"]) increased_income = income + personal_benefit_income + credits + benefits