From 0a6b36b81025b830afa8cb342bc4df8942fd4c9d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 17:56:21 -0400 Subject: [PATCH 1/4] Give CTR and Income Support their own pension contribution shares Council Tax Reduction and Income Support deducted a hard-coded 0.5 of pension contributions, while the CTR pensioner earnings disregards were capped at Housing Benefit net earnings, which read Housing Benefit's share. A reform of HB's share therefore moved the CTR disregard cap but not CTR income, and neither scheme could be reformed on its own. Each scheme's regulations state their own one-half and none refers to HB's, so each gets its own parameter at 0.5: - Income Support: SI 1987/1967 regs 36(3)(b) and 38. - CTR, by scheme: England pensioners (SI 2012/2885 Sch 1 paras 19, 29), the England default scheme (SI 2012/2886), Wales (WSI 2013/3029), Scotland (SSI 2012/319, SSI 2021/249) and the working-age schemes of Merton, Kingston upon Thames, Newham, Westminster and Oxford. council_tax_reduction_pension_contribution_deduction_rate picks the family's scheme's share. CTR applicable income reads it, and the pensioner disregards now cap at council_tax_reduction_net_earnings, which shares HB's earnings rules (factored into _net_earnings.py) at the scheme's own share. Current-law outputs are unchanged. Fixes #2515 Co-Authored-By: Claude Opus 5.5 --- changelog.d/2515.fixed.md | 1 + .../pension_contribution_deduction_rate.yaml | 13 + .../pension_contribution_deduction_rate.yaml | 17 + .../pension_contribution_deduction_rate.yaml | 13 + .../pension_contribution_deduction_rate.yaml | 11 + .../pension_contribution_deduction_rate.yaml | 11 + .../pension_contribution_deduction_rate.yaml | 11 + .../pension_contribution_deduction_rate.yaml | 13 + .../pension_contribution_deduction_rate.yaml | 19 + .../pension_contribution_deduction_rate.yaml | 21 + .../pension_contribution_deduction_rate.yaml | 15 + .../pension_contribution_deduction_rate.yaml | 151 ++++ .../pension_contribution_deduction_rate.yaml | 783 ++++++++++++++++++ ..._contribution_deduction_rate_properties.py | 243 ++++++ .../applicable_income/_net_earnings.py | 49 ++ .../housing_benefit_net_earnings.py | 39 +- .../dwp/income_support_applicable_income.py | 10 +- ...council_tax_reduction_applicable_income.py | 10 +- .../council_tax_reduction_net_earnings.py | 40 + ...ion_pension_contribution_deduction_rate.py | 87 ++ ..._reduction_pensioner_earnings_disregard.py | 10 +- 21 files changed, 1524 insertions(+), 43 deletions(-) create mode 100644 changelog.d/2515.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/default_scheme/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/kingston_upon_thames/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/merton/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/newham/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/parameters/gov/local_authorities/westminster/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml create mode 100644 policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/_net_earnings.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py diff --git a/changelog.d/2515.fixed.md b/changelog.d/2515.fixed.md new file mode 100644 index 0000000000..1154378a6b --- /dev/null +++ b/changelog.d/2515.fixed.md @@ -0,0 +1 @@ +- Council Tax Reduction and Income Support read their own pension contribution deduction shares (one-half in each scheme) instead of a hard-coded 0.5, and the Council Tax Reduction pensioner earnings disregards cap at net earnings with the scheme's own share, so a reform of Housing Benefit's share no longer moves them. diff --git a/policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..09b055db92 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/income_support/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,13 @@ +description: Income Support deducts this share of pension contributions when it calculates a claimant's net earnings and a self-employed earner's net profit. +values: + 1988-04-11: 0.5 +metadata: + period: year + unit: /1 + label: Income Support net earnings pension contribution deduction rate + economy: false + reference: + - title: The Income Support (General) Regulations 1987, regulation 36(3)(b) (employed earners) + href: https://www.legislation.gov.uk/uksi/1987/1967/regulation/36 + - title: The Income Support (General) Regulations 1987, regulation 38(1)(b)(ii), (3)(c) and (9)(b) (self-employed earners) + href: https://www.legislation.gov.uk/uksi/1987/1967/regulation/38 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/default_scheme/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/default_scheme/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..0e9ece3f08 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/default_scheme/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,17 @@ +description: The England Council Tax Reduction default scheme deducts this share of pension contributions when it calculates an applicant's net earnings and a self-employed earner's net profit, for pensioners and for applicants who are not pensioners and have no award of Universal Credit. The default scheme applies where a billing authority has made no scheme of its own. +values: + 2013-04-01: 0.5 +metadata: + period: year + unit: /1 + label: England Council Tax Reduction default scheme pension contribution deduction rate + economy: false + reference: + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012 Sch. para. 42(2)(b) and (c) and (5)(c) (employed earners who are pensioners) + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/42 + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012 Sch. para. 52(3)(b) and (c) and (6)(c) (employed earners who are not pensioners) + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/52 + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012 Sch. para. 61(1)(b)(ii) and (c)(ii), (3)(c) and (9)(b) (self-employed earners) + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/61 + - title: Local Government Finance Act 1992 Sch. 1A para. 4 (the default scheme applies where a billing authority has made no scheme) + href: https://www.legislation.gov.uk/ukpga/1992/14/schedule/1A/paragraph/4 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..0b4b2c2b9b --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,13 @@ +description: The England Council Tax Reduction scheme for pensioners deducts this share of pension contributions when it calculates an applicant's net earnings and a self-employed earner's net profit. +values: + 2013-04-01: 0.5 +metadata: + period: year + unit: /1 + label: England Council Tax Reduction pensioner pension contribution deduction rate + economy: false + reference: + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Sch. 1 para. 19(2)(b) and (c) and (5)(c) (employed earners) + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/19 + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Sch. 1 para. 29(1)(b)(ii), (2)(c) and (8)(b) (self-employed earners) + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/29 diff --git a/policyengine_uk/parameters/gov/local_authorities/kingston_upon_thames/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/kingston_upon_thames/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..213fa1d994 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/kingston_upon_thames/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,11 @@ +description: The Kingston upon Thames Council Tax Reduction scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant. +values: + 2026-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Kingston upon Thames Council Tax Reduction pension contribution deduction rate + economy: false + reference: + - title: Council Tax Reduction Scheme 2026 - 2027, paras. 52(3)(b) and (c) and (6)(c) (employed earners) and 61 (self-employed earners) + href: https://www.kingston.gov.uk/sites/default/files/2026-03/Council%20Tax%20Reduction%20Scheme%202026%20-%202027.pdf diff --git a/policyengine_uk/parameters/gov/local_authorities/merton/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/merton/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..e3dd4bc680 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/merton/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,11 @@ +description: The Merton Council Tax Support scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant. +values: + 2026-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Merton Council Tax Support pension contribution deduction rate + economy: false + reference: + - title: The London Borough of Merton Local Council Tax Support Scheme Rules 2026, paras. 52(3)(b) and (c) and (6)(c) (employed earners) and 61 (self-employed earners) + href: https://www.merton.gov.uk/sites/default/files/2026-03/Council%20Tax%20Reduction%20Scheme%20L%20B%20Merton%202026-27.pdf diff --git a/policyengine_uk/parameters/gov/local_authorities/newham/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/newham/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..3a898a844b --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/newham/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,11 @@ +description: The Newham Council Tax Reduction scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant. +values: + 2026-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Newham Council Tax Reduction pension contribution deduction rate + economy: false + reference: + - title: Council Tax Reduction Scheme 2026, paras. 52(3)(b) and (c) and (6)(c) (employed earners) and 61 (self-employed earners) + href: https://www.newham.gov.uk/downloads/file/10753/council-tax-reduction-scheme-2026 diff --git a/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..2856e8dc9c --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,13 @@ +description: The Oxford Council Tax Reduction scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant. +values: + 2026-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Oxford Council Tax Reduction pension contribution deduction rate + economy: false + reference: + - title: Oxford City Council Council Tax Reduction Scheme 2026 to 2027, Part 1 para. 18 (50% of contributions to occupational or personal pension schemes) + href: https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27 + - title: Oxford City Council Council Tax Reduction Scheme 2026 to 2027, Annex 5 (one half of personal pension scheme payments, self-employed earners) + href: https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27/3 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..0345f58a56 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,19 @@ +description: The Scotland Council Tax Reduction schemes deduct this share of pension contributions when they calculate an applicant's net earnings and a self-employed earner's net profit, for applicants of pension age and for applicants under pension age with no award of Universal Credit. +values: + 2013-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Scotland Council Tax Reduction pension contribution deduction rate + economy: false + reference: + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 33(2)(b) and (c) and (4)(c) (employed earners of pension age) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/33 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 36(1)(b)(ii), (2)(c) and (8)(b) (self-employed earners of pension age) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/36 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 50(3)(b) and (c) and (5)(c) (employed earners under pension age with no award of Universal Credit, from 1 April 2022) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/50 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 51(9) (self-employed earners under pension age with no award of Universal Credit, by SSI 2012/303 reg. 37) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/51 + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 35(3)(b) and (c) and (6)(c) and reg. 37(1)(b)(ii), (3)(c) and (9)(b) (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/contents/made diff --git a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..0c847d4b13 --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,21 @@ +description: The Wales Council Tax Reduction scheme deducts this share of pension contributions when it calculates an applicant's net earnings and a self-employed earner's net profit, for pensioners and for applicants who are not pensioners and have no award of Universal Credit. +values: + 2013-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Wales Council Tax Reduction pension contribution deduction rate + economy: false + reference: + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Sch. 1 para. 13(2)(b) and (c) and (5)(c) (employed earners who are pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/13 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Sch. 1 para. 23(1)(b)(ii), (2)(c) and (8)(b) (self-employed earners who are pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/23 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Sch. 6 para. 15(3)(b) and (c) and (6)(c) (employed earners who are not pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/15 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Sch. 6 para. 24(1)(b)(ii) and (c)(ii), (3)(c) and (9)(b) (self-employed earners who are not pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/24 + - title: The Council Tax Reduction Schemes (Default Scheme) (Wales) Regulations 2013 Sch. paras. 39, 49 and 58 + href: https://www.legislation.gov.uk/wsi/2013/3035/schedule + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 Sch. 1 paras. 13 and 23 and Sch. 6 paras. 15 and 24 (2013-14 schemes; revoked from 1 April 2014) + href: https://www.legislation.gov.uk/wsi/2012/3144/contents/made diff --git a/policyengine_uk/parameters/gov/local_authorities/westminster/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/westminster/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..93b9b5309f --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/westminster/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -0,0 +1,15 @@ +description: The Westminster Council Tax Support scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant. The council's published scheme says the default scheme forms the basis of its working-age scheme and does not restate this rule, so the default scheme's one-half is used. +values: + 2026-04-01: 0.5 +metadata: + period: year + unit: /1 + label: Westminster Council Tax Support pension contribution deduction rate + economy: false + reference: + - title: Council Tax Support scheme for 2026/27, section 4 (the Default Scheme Regulations 2012 form the basis of the working-age scheme) + href: https://www.westminster.gov.uk/sites/default/files/media/documents/council-tax-support-scheme-for-2026-27.pdf + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012 Sch. para. 52(3)(b) and (c) and (6)(c) + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/52 + - title: The Council Tax Reduction Schemes (Default Scheme) (England) Regulations 2012 Sch. para. 61 + href: https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/61 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/pension_contribution_deduction_rate.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..bf6c22ef1a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/pension_contribution_deduction_rate.yaml @@ -0,0 +1,151 @@ +# Income Support deducts one-half of pension contributions from net earnings +# (IS Regs 1987 reg 36(3)(b)) and net profit (reg 38(1)(b)(ii), (3)(c) and +# (9)(b)), with no reference to Housing Benefit's share. The rates below 0.5 +# are model reforms, not current law. +# +# Each person earns 10,000 a year, below the personal allowance and the primary +# threshold, so no income tax or National Insurance is due. The single +# claimant's earnings disregard is 5 a week (260 a year) and a couple's 10 a +# week (520 a year). + +- name: At current law Income Support deducts half of pension contributions + # 10,000 - 0.5 x 2,000 - 260 = 8,740. The same claimant without the + # contribution has 9,740 (means_test_income_members.yaml). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_contributions: 2_000 + income_tax: 0 + national_insurance: 0 + income_support_applicable_income: 8_740 + +- name: A reform of the Income Support share moves Income Support applicable income + # Model reform, not current law: 10,000 - 0.25 x 2,000 - 260 = 9,240, which is + # 500 more than at half: 2,000 x (0.5 - 0.25). + period: 2026 + absolute_error_margin: 0.01 + input: + gov.dwp.income_support.means_test.pension_contribution_deduction_rate: 0.25 + people: + claimant: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + income_support_applicable_income: 9_240 + # Housing Benefit keeps its own half: 10,000 - 0.5 x 2,000. + housing_benefit_net_earnings: 9_000 + +- name: A reform of the Housing Benefit share leaves Income Support applicable income + # Model reform, not current law. Housing Benefit net earnings become + # 10,000 - 0.25 x 2,000 = 9,500; Income Support keeps 8,740. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate: 0.25 + people: + claimant: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + housing_benefit_net_earnings: 9_500 + income_support_applicable_income: 8_740 + +- name: A reform of a Council Tax Reduction share leaves Income Support applicable income + # Model reform, not current law. The claimant lives in England outside the + # councils whose schemes are modelled, so Council Tax Reduction reads the + # England default scheme's share: 10,000 - 0.25 x 2,000 = 9,500. Income + # Support keeps 8,740. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.england.council_tax_reduction.default_scheme.means_test.pension_contribution_deduction_rate: 0.25 + people: + claimant: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: ENGLAND + output: + council_tax_reduction_applicable_income: 9_500 + income_support_applicable_income: 8_740 + +- name: A couple's contributions are summed at the Income Support share + # 20,000 - 0.5 x (2,000 + 1,000) - 520 = 17,980. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + partner: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 1_000 + benunits: + benunit: + members: [head, partner] + households: + household: + members: [head, partner] + output: + income_support_applicable_income: 17_980 + +- name: A couple's Income Support applicable income falls by contributions times the change in share + # Model reform, not current law: 20,000 - 0.25 x 3,000 - 520 = 18,730, which + # is 750 more than at half: 3,000 x (0.5 - 0.25). + period: 2026 + absolute_error_margin: 0.01 + input: + gov.dwp.income_support.means_test.pension_contribution_deduction_rate: 0.25 + people: + head: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + partner: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 1_000 + benunits: + benunit: + members: [head, partner] + households: + household: + members: [head, partner] + output: + income_support_applicable_income: 18_730 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml new file mode 100644 index 0000000000..9f34719d19 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml @@ -0,0 +1,783 @@ +# Each Council Tax Reduction scheme deducts its own share of pension +# contributions from earnings, one-half in every scheme modelled: +# - England pensioners: SI 2012/2885 Sch 1 paras 19(2)(b)-(c) and 29; +# - England default scheme: SI 2012/2886 Sch paras 42, 52 and 61; +# - Wales: WSI 2013/3029 Sch 1 paras 13 and 23 and Sch 6 paras 15 and 24; +# - Scotland: SSI 2012/319 regs 33 and 36 and SSI 2021/249 regs 50 and 51(9); +# - Merton, Kingston upon Thames and Newham: their schemes' paras 52 and 61; +# - Oxford: its scheme's para 18 and Annex 5; +# - Westminster: the default scheme, which its scheme says it is based on. +# None refers to Housing Benefit's share, so a reform of one scheme's share +# moves that scheme's families only, and a reform of Housing Benefit's moves +# none of them. The rates below 0.5 are model reforms, not current law. +# +# Every person earns 10,000 a year, below the personal allowance and the +# primary threshold, so no income tax or National Insurance is due, and pays +# 2,000 into a personal pension. Universal Credit and Pension Credit are not +# claimed. Applicable income is 10,000 - rate x 2,000, less the single +# pensioner's 5 a week (260 a year) earnings disregard (SI 2012/2885 Sch 4 +# para 8(a)) for the 70-year-old. Net earnings are 10,000 - rate x 2,000. + +- name: At current law every scheme deducts half of pension contributions + period: 2026 + absolute_error_margin: 0.01 + input: + people: + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + wales: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + england_pensioner: + age: 70 + employment_income: 10_000 + personal_pension_contributions: 2_000 + england_default: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + merton: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + kingston: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + newham: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + westminster: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + oxford: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + wales_family: + members: [wales] + would_claim_uc: false + would_claim_pc: false + england_pensioner_family: + members: [england_pensioner] + would_claim_uc: false + would_claim_pc: false + england_default_family: + members: [england_default] + would_claim_uc: false + would_claim_pc: false + merton_family: + members: [merton] + would_claim_uc: false + would_claim_pc: false + kingston_family: + members: [kingston] + would_claim_uc: false + would_claim_pc: false + newham_family: + members: [newham] + would_claim_uc: false + would_claim_pc: false + westminster_family: + members: [westminster] + would_claim_uc: false + would_claim_pc: false + oxford_family: + members: [oxford] + would_claim_uc: false + would_claim_pc: false + households: + scotland_household: + members: [scotland] + country: SCOTLAND + wales_household: + members: [wales] + country: WALES + england_pensioner_household: + members: [england_pensioner] + country: ENGLAND + england_default_household: + members: [england_default] + country: ENGLAND + merton_household: + members: [merton] + country: ENGLAND + local_authority: MERTON + kingston_household: + members: [kingston] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + newham_household: + members: [newham] + country: ENGLAND + local_authority: NEWHAM + westminster_household: + members: [westminster] + country: ENGLAND + local_authority: WESTMINSTER + oxford_household: + members: [oxford] + country: ENGLAND + local_authority: OXFORD + output: + # In order: Scotland and Wales at working age, the England pensioner, + # England working age outside the modelled councils, and working age in + # Merton, Kingston upon Thames, Newham, Westminster and Oxford. + pension_contributions: [2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000, 2000] + income_tax: [0, 0, 0, 0, 0, 0, 0, 0, 0] + national_insurance: [0, 0, 0, 0, 0, 0, 0, 0, 0] + council_tax_reduction_pensioner: [false, false, true, false, false, false, false, false, false] + council_tax_reduction_pension_contribution_deduction_rate: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] + # 10,000 - 0.5 x 2,000, less 260 for the pensioner. + council_tax_reduction_applicable_income: [9000, 9000, 8740, 9000, 9000, 9000, 9000, 9000, 9000] + council_tax_reduction_net_earnings: [9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000] + housing_benefit_net_earnings: [9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0, 260, 0, 0, 0, 0, 0, 0] + +- name: A reform of the Scotland share moves only that scheme's applicable income + # Model reform, not current law. + # Scotland, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, England working age outside the modelled councils: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.scotland.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + england_default: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + england_default_family: + members: [england_default] + would_claim_uc: false + would_claim_pc: false + households: + scotland_household: + members: [scotland] + country: SCOTLAND + england_default_household: + members: [england_default] + country: ENGLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Wales share moves only that scheme's applicable income + # Model reform, not current law. + # Wales, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, England working age outside the modelled councils: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.wales.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + wales: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + england_default: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + wales_family: + members: [wales] + would_claim_uc: false + would_claim_pc: false + england_default_family: + members: [england_default] + would_claim_uc: false + would_claim_pc: false + households: + wales_household: + members: [wales] + country: WALES + england_default_household: + members: [england_default] + country: ENGLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the England pensioner share moves only that scheme's applicable income + # Model reform, not current law. + # England pensioner scheme: 10,000 - 0.25 x 2,000 - 260 = 9,240. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.england.council_tax_reduction.pensioners.means_test.pension_contribution_deduction_rate: 0.25 + people: + england_pensioner: + age: 70 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + england_pensioner_family: + members: [england_pensioner] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + england_pensioner_household: + members: [england_pensioner] + country: ENGLAND + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9240, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [260, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the England default scheme share moves only that scheme's applicable income + # Model reform, not current law. + # England working age outside the modelled councils: 10,000 - 0.25 x 2,000 = 9,500. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.england.council_tax_reduction.default_scheme.means_test.pension_contribution_deduction_rate: 0.25 + people: + england_default: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + england_default_family: + members: [england_default] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + england_default_household: + members: [england_default] + country: ENGLAND + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Merton share moves only that scheme's applicable income + # Model reform, not current law. + # Merton, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.merton.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + merton: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + merton_family: + members: [merton] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + merton_household: + members: [merton] + country: ENGLAND + local_authority: MERTON + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Kingston upon Thames share moves only that scheme's applicable income + # Model reform, not current law. + # Kingston upon Thames, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.kingston_upon_thames.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + kingston: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + kingston_family: + members: [kingston] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + kingston_household: + members: [kingston] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Newham share moves only that scheme's applicable income + # Model reform, not current law. + # Newham, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.newham.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + newham: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + newham_family: + members: [newham] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + newham_household: + members: [newham] + country: ENGLAND + local_authority: NEWHAM + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Westminster share moves only that scheme's applicable income + # Model reform, not current law. + # Westminster, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.westminster.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + westminster: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + westminster_family: + members: [westminster] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + westminster_household: + members: [westminster] + country: ENGLAND + local_authority: WESTMINSTER + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Oxford share moves only that scheme's applicable income + # Model reform, not current law. + # Oxford, working age: 10,000 - 0.25 x 2,000 = 9,500. + # Control, Scotland, working age: 10,000 - 0.5 x 2,000 = 9,000. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.oxford.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + oxford: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + oxford_family: + members: [oxford] + would_claim_uc: false + would_claim_pc: false + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + households: + oxford_household: + members: [oxford] + country: ENGLAND + local_authority: OXFORD + scotland_household: + members: [scotland] + country: SCOTLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: [0.25, 0.5] + council_tax_reduction_applicable_income: [9500, 9000] + council_tax_reduction_net_earnings: [9500, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0] + housing_benefit_net_earnings: [9000, 9000] + +- name: A reform of the Housing Benefit share moves no Council Tax Reduction scheme + # Model reform, not current law. Housing Benefit net earnings become + # 10,000 - 0.25 x 2,000 = 9,500; every scheme's figures stay as at current law. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate: 0.25 + people: + scotland: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + wales: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + england_pensioner: + age: 70 + employment_income: 10_000 + personal_pension_contributions: 2_000 + england_default: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + merton: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + kingston: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + newham: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + westminster: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + oxford: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + benunits: + scotland_family: + members: [scotland] + would_claim_uc: false + would_claim_pc: false + wales_family: + members: [wales] + would_claim_uc: false + would_claim_pc: false + england_pensioner_family: + members: [england_pensioner] + would_claim_uc: false + would_claim_pc: false + england_default_family: + members: [england_default] + would_claim_uc: false + would_claim_pc: false + merton_family: + members: [merton] + would_claim_uc: false + would_claim_pc: false + kingston_family: + members: [kingston] + would_claim_uc: false + would_claim_pc: false + newham_family: + members: [newham] + would_claim_uc: false + would_claim_pc: false + westminster_family: + members: [westminster] + would_claim_uc: false + would_claim_pc: false + oxford_family: + members: [oxford] + would_claim_uc: false + would_claim_pc: false + households: + scotland_household: + members: [scotland] + country: SCOTLAND + wales_household: + members: [wales] + country: WALES + england_pensioner_household: + members: [england_pensioner] + country: ENGLAND + england_default_household: + members: [england_default] + country: ENGLAND + merton_household: + members: [merton] + country: ENGLAND + local_authority: MERTON + kingston_household: + members: [kingston] + country: ENGLAND + local_authority: KINGSTON_UPON_THAMES + newham_household: + members: [newham] + country: ENGLAND + local_authority: NEWHAM + westminster_household: + members: [westminster] + country: ENGLAND + local_authority: WESTMINSTER + oxford_household: + members: [oxford] + country: ENGLAND + local_authority: OXFORD + output: + housing_benefit_net_earnings: [9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500, 9500] + council_tax_reduction_pension_contribution_deduction_rate: [0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5] + council_tax_reduction_applicable_income: [9000, 9000, 8740, 9000, 9000, 9000, 9000, 9000, 9000] + council_tax_reduction_net_earnings: [9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000, 9000] + council_tax_reduction_pensioner_earnings_disregard: [0, 0, 260, 0, 0, 0, 0, 0, 0] + +- name: A pensioner's net earnings below the disregard are all disregarded at current law + # Net earnings are 300 - 0.5 x 200 = 200, below the single pensioner's 260, so + # the disregard is 200 (SI 2012/2885 Sch 4 para 8(a), capped at net earnings). + # Applicable income is the State Pension and earnings, 5,300, less 0.5 x 200 + # and the 200 disregard: 5,000. Income tax is nil (5,300 is below the + # personal allowance). + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + employment_income: 300 + personal_pension_contributions: 200 + state_pension: 5_000 + benunits: + benunit: + members: [pensioner] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + output: + income_tax: 0 + council_tax_reduction_pension_contribution_deduction_rate: 0.5 + housing_benefit_net_earnings: 200 + council_tax_reduction_net_earnings: 200 + council_tax_reduction_pensioner_earnings_disregard: 200 + council_tax_reduction_applicable_income: 5_000 + +- name: A reform of the England pensioner share moves the disregard cap with the deduction + # Model reform, not current law. Net earnings for Council Tax Reduction are + # 300 - 0.25 x 200 = 250, still below 260, so 250 is disregarded. Applicable + # income is 5,300 - 0.25 x 200 - 250 = 5,000: earnings wholly disregarded + # deduct nothing, whatever the share. Housing Benefit net earnings stay 200. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.england.council_tax_reduction.pensioners.means_test.pension_contribution_deduction_rate: 0.25 + people: + pensioner: + age: 70 + employment_income: 300 + personal_pension_contributions: 200 + state_pension: 5_000 + benunits: + benunit: + members: [pensioner] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: 0.25 + housing_benefit_net_earnings: 200 + council_tax_reduction_net_earnings: 250 + council_tax_reduction_pensioner_earnings_disregard: 250 + council_tax_reduction_applicable_income: 5_000 + +- name: A reform of the Housing Benefit share leaves the Council Tax Reduction disregard cap + # Model reform, not current law. Housing Benefit net earnings become + # 300 - 0.25 x 200 = 250, but the Council Tax Reduction cap stays at its own + # 300 - 0.5 x 200 = 200, so the disregard stays 200 and applicable income + # 5,000. Reading Housing Benefit's net earnings, the disregard was 250 and + # applicable income 5,300 - 0.5 x 200 - 250 = 4,950. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate: 0.25 + people: + pensioner: + age: 70 + employment_income: 300 + personal_pension_contributions: 200 + state_pension: 5_000 + benunits: + benunit: + members: [pensioner] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: 0.5 + housing_benefit_net_earnings: 250 + council_tax_reduction_net_earnings: 200 + council_tax_reduction_pensioner_earnings_disregard: 200 + council_tax_reduction_applicable_income: 5_000 + +- name: A couple's contributions are summed at the scheme's share + # Scotland, working age. Each partner earns 10,000 and they contribute 2,000 + # and 1,000. Applicable income is 20,000 - 0.5 x 3,000 = 18,500. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + partner: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 1_000 + benunits: + benunit: + members: [head, partner] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [head, partner] + country: SCOTLAND + output: + council_tax_reduction_applicable_income: 18_500 + council_tax_reduction_net_earnings: 18_500 + +- name: A couple's applicable income falls by their contributions times the change in share + # Model reform, not current law. At a quarter, 20,000 - 0.25 x 3,000 = 19,250: + # 750 more than at half, which is 3,000 x (0.5 - 0.25). + period: 2026 + absolute_error_margin: 0.01 + input: + gov.local_authorities.scotland.council_tax_reduction.means_test.pension_contribution_deduction_rate: 0.25 + people: + head: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 2_000 + partner: + age: 40 + employment_income: 10_000 + personal_pension_contributions: 1_000 + benunits: + benunit: + members: [head, partner] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [head, partner] + country: SCOTLAND + output: + council_tax_reduction_applicable_income: 19_250 + council_tax_reduction_net_earnings: 19_250 diff --git a/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py b/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py new file mode 100644 index 0000000000..45310d1403 --- /dev/null +++ b/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py @@ -0,0 +1,243 @@ +"""Property-based tests for the legacy means tests' pension contribution shares. + +Each scheme's regulations state their own share of pension contributions to +deduct from earnings, one-half in every scheme modelled, and none refers to +another scheme's: + +- Housing Benefit: SI 2006/213 regs 36(3)(b)-(c) and 38; SI 2006/214 regs + 36(2)(b)-(c) and 39 (gov.dwp.housing_benefit.means_test); +- Income Support: SI 1987/1967 regs 36(3)(b) and 38 (gov.dwp.income_support); +- Council Tax Reduction, each scheme under gov.local_authorities: + the England pensioner scheme (SI 2012/2885 Sch 1 paras 19 and 29), Wales + (WSI 2013/3029 Sch 1 paras 13 and 23, Sch 6 paras 15 and 24), Scotland (SSI + 2012/319 regs 33 and 36, SSI 2021/249 regs 50 and 51(9)), the working-age + schemes of Merton, Kingston upon Thames, Newham, Westminster and Oxford, and + the England default scheme (SI 2012/2886 Sch paras 42, 52 and 61) for every + other family. + +Invariants, for any generated population of families whose floors are slack +(earnings of at least 15,000 per adult and contributions of at most a fifth of +them, so no net-earnings, disregard or applicable-income floor binds, and no +Pension Credit or Universal Credit claim): + +1. Exactness: when each scheme's share moves from 0.5 to r, the scheme's + applicable income falls by exactly the family's contributions x (r - 0.5), + and so do the scheme's net earnings. +2. Locality: every share is drawn distinct, so a family that read another + scheme's share (or Housing Benefit's) would miss invariant 1. Pensioners in + Merton follow the England pensioner scheme, not Merton's, and families in + Northern Ireland, which has no Council Tax Reduction, read the England + default scheme's share. +3. The pensioner earnings disregards do not move when the disregard caps are + slack. +4. Differential: at current law Council Tax Reduction net earnings equal + Housing Benefit net earnings, as both deduct half. +5. Current law: every share is 0.5 in every financial year from 2015 (the + first year the model simulates) to 2030, Income Support's and Housing + Benefit's too. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.system import system + +PROPERTY_SETTINGS = settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + +CTR = "gov.local_authorities.{}.council_tax_reduction{}.means_test.pension_contribution_deduction_rate" +PARAMETERS = { + "england_pensioner": CTR.format("england", ".pensioners"), + "england_default": CTR.format("england", ".default_scheme"), + "scotland": CTR.format("scotland", ""), + "wales": CTR.format("wales", ""), + "merton": CTR.format("merton", ""), + "kingston_upon_thames": CTR.format("kingston_upon_thames", ""), + "newham": CTR.format("newham", ""), + "westminster": CTR.format("westminster", ""), + "oxford": CTR.format("oxford", ""), + "housing_benefit": ( + "gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate" + ), + "income_support": ( + "gov.dwp.income_support.means_test.pension_contribution_deduction_rate" + ), +} +# Each route: (country, local authority, pension age, the share it reads). +ROUTES = { + "england_pensioner": ("ENGLAND", "MAIDSTONE", True, "england_pensioner"), + "merton_pensioner": ("ENGLAND", "MERTON", True, "england_pensioner"), + "england_default": ("ENGLAND", "MAIDSTONE", False, "england_default"), + "scotland_pension_age": ("SCOTLAND", "GLASGOW_CITY", True, "scotland"), + "scotland_working_age": ("SCOTLAND", "GLASGOW_CITY", False, "scotland"), + "wales_pension_age": ("WALES", "CARDIFF", True, "wales"), + "wales_working_age": ("WALES", "CARDIFF", False, "wales"), + "merton": ("ENGLAND", "MERTON", False, "merton"), + "kingston_upon_thames": ( + "ENGLAND", + "KINGSTON_UPON_THAMES", + False, + "kingston_upon_thames", + ), + "newham": ("ENGLAND", "NEWHAM", False, "newham"), + "westminster": ("ENGLAND", "WESTMINSTER", False, "westminster"), + "oxford": ("ENGLAND", "OXFORD", False, "oxford"), + "northern_ireland": ("NORTHERN_IRELAND", "BELFAST", False, "england_default"), +} +# State Pension age is 66 to 67 over the years tested, so 68 and over is +# pension age and 55 and under working age. +WORKING_AGE = (25, 55) +PENSION_AGE = (68, 90) +# Shares other than 0.5, on a grid of twentieths so that each is exact in +# binary and every pair differs by at least 0.05. +SHARES = [i / 20 for i in range(21) if i != 10] +TOLERANCE = 0.05 + + +@st.composite +def families(draw): + route = draw(st.sampled_from(sorted(ROUTES))) + pension_age = ROUTES[route][2] + low, high = PENSION_AGE if pension_age else WORKING_AGE + head_age = draw(st.integers(low, high)) + adults = [] + for _ in range(draw(st.integers(1, 2))): + earnings = draw(st.floats(15_000, 60_000, allow_nan=False)) + share = draw(st.one_of(st.just(0.0), st.floats(0.01, 0.2))) + # Partners within five years of each other, so neither is presumed + # the other's child. + age = draw(st.integers(max(low, head_age - 5), min(high, head_age + 5))) + adults.append( + dict( + age=age, + employment_income=earnings, + personal_pension_contributions=round(earnings * share, 2), + ) + ) + return dict(route=route, adults=adults) + + +@st.composite +def reforms(draw): + shares = draw( + st.lists( + st.sampled_from(SHARES), + min_size=len(PARAMETERS), + max_size=len(PARAMETERS), + unique=True, + ) + ) + return dict(zip(PARAMETERS, shares)) + + +def situation(units, year): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + country, local_authority, _, _ = ROUTES[unit["route"]] + names = [] + for j, adult in enumerate(unit["adults"]): + name = f"a{i}_{j}" + people[name] = {key: {year: value} for key, value in adult.items()} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "would_claim_uc": {year: False}, + "would_claim_pc": {year: False}, + "income_support": {year: 0.0}, + "jsa_income": {year: 0.0}, + "esa_income": {year: 0.0}, + } + households[f"h{i}"] = { + "members": names, + "country": {year: country}, + "local_authority": {year: local_authority}, + "savings": {year: 0.0}, + } + return {"people": people, "benunits": benunits, "households": households} + + +VARIABLES = [ + "council_tax_reduction_pension_contribution_deduction_rate", + "council_tax_reduction_applicable_income", + "council_tax_reduction_net_earnings", + "council_tax_reduction_pensioner_earnings_disregard", + "council_tax_reduction_pensioner", + "income_support_applicable_income", + "housing_benefit_net_earnings", +] + + +def calculate(units, year, reform=None): + sim = Simulation(situation=situation(units, year), reform=reform) + return {v: np.asarray(sim.calculate(v, year), dtype=float) for v in VARIABLES} + + +def contributions(unit): + return sum(adult["personal_pension_contributions"] for adult in unit["adults"]) + + +@PROPERTY_SETTINGS +@given( + st.lists(families(), min_size=1, max_size=26), + reforms(), + st.sampled_from([2026, 2028, 2030]), +) +def test_each_scheme_moves_by_contributions_times_its_own_change_in_share( + units, shares, year +): + base = calculate(units, year) + reform = {PARAMETERS[key]: {str(year): share} for key, share in shares.items()} + moved = calculate(units, year, reform) + for i, unit in enumerate(units): + country, _, pension_age, scheme = ROUTES[unit["route"]] + c = contributions(unit) + case = (unit, shares, year) + # The route is the one intended: pension-age families in Great + # Britain are the schemes' pensioners. + in_great_britain = country != "NORTHERN_IRELAND" + pensioner = base["council_tax_reduction_pensioner"][i] + assert bool(pensioner) == (pension_age and in_great_britain), case + # Floors are slack: income stays positive on both sides. + for values in (base, moved): + assert values["council_tax_reduction_applicable_income"][i] > 0, case + assert values["income_support_applicable_income"][i] > 0, case + rate = "council_tax_reduction_pension_contribution_deduction_rate" + assert base[rate][i] == 0.5, case + assert moved[rate][i] == shares[scheme], case + # Invariant 1 (with 2): exactly contributions x the scheme's change. + for variable, share in ( + ("council_tax_reduction_applicable_income", shares[scheme]), + ("council_tax_reduction_net_earnings", shares[scheme]), + ("income_support_applicable_income", shares["income_support"]), + ("housing_benefit_net_earnings", shares["housing_benefit"]), + ): + change = base[variable][i] - moved[variable][i] + assert abs(change - c * (share - 0.5)) < TOLERANCE, (variable, case) + # Invariant 3: slack caps leave the disregards where they were. + disregard = "council_tax_reduction_pensioner_earnings_disregard" + assert abs(base[disregard][i] - moved[disregard][i]) < TOLERANCE, case + # Invariant 4: equal shares, equal net earnings. + assert ( + abs( + base["council_tax_reduction_net_earnings"][i] + - base["housing_benefit_net_earnings"][i] + ) + < TOLERANCE + ), case + + +def test_every_share_is_one_half_in_every_year(): + for year in range(2015, 2031): + # Read in each financial year (30 April, the model's convention). + parameters = system.parameters(f"{year}-04-30") + for key, path in PARAMETERS.items(): + node = parameters + for name in path.split("."): + node = getattr(node, name) + assert node == 0.5, (key, year) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/_net_earnings.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/_net_earnings.py new file mode 100644 index 0000000000..5ac9560f4d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/_net_earnings.py @@ -0,0 +1,49 @@ +from policyengine_uk.model_api import * + + +def person_net_earnings(person, period, pension_contribution_deduction_rate): + """A person's net earnings under the legacy means tests' earnings rules. + + Employment income plus self-employment profit (each floored at zero) plus + statutory sick and maternity pay, less the income tax attributed to them, + Class 1, 2 and 4 National Insurance and the scheme's share of pension + contributions, floored at zero. Housing Benefit + (housing_benefit_net_earnings) and the Council Tax Reduction pensioner + earnings disregards (council_tax_reduction_net_earnings) use these + deductions. Each scheme's regulations state their own share of pension + contributions, so each passes its own rate. + """ + employment_income = person("employment_income", period) + self_employment_income = person("self_employment_income", period) + # Statutory sick and maternity pay are earnings (HB Regs 2006 reg + # 35(1)(i); pension age reg 35(1)(h)). The model taxes them as employment income + # (employment_benefits) but leaves them out of total_income. + statutory_pay = person("employment_benefits", period) + earnings = ( + max_(employment_income, 0) + max_(self_employment_income, 0) + statutory_pay + ) + # Income tax is attributed by the earnings' share of total income, + # with statutory pay added and before any negative component (a + # loss), so that a loss set against other income does not drop or + # inflate the share of tax on these earnings. + income_before_losses = person("total_income", period) + statutory_pay + system = person.simulation.tax_benefit_system + for component in system.get_variable("total_income").adds: + income_before_losses += max_(-person(component, period), 0) + earnings_share = np.divide( + earnings, + income_before_losses, + out=np.zeros_like(earnings), + where=income_before_losses > 0, + ) + income_tax = person("income_tax", period) * min_(earnings_share, 1) + national_insurance = add( + person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] + ) + pension_contributions = ( + person("pension_contributions", period) * pension_contribution_deduction_rate + ) + return max_( + earnings - income_tax - national_insurance - pension_contributions, + 0, + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py index 8f8d97ac9a..c7b939c8f9 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py @@ -1,4 +1,7 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income._net_earnings import ( + person_net_earnings, +) class housing_benefit_net_earnings(Variable): @@ -46,40 +49,8 @@ class housing_benefit_net_earnings(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.means_test person = benunit.members - employment_income = person("employment_income", period) - self_employment_income = person("self_employment_income", period) - # Statutory sick and maternity pay are earnings (reg 35(1)(i); pension - # age reg 35(1)(h)). The model taxes them as employment income - # (employment_benefits) but leaves them out of total_income. - statutory_pay = person("employment_benefits", period) - earnings = ( - max_(employment_income, 0) + max_(self_employment_income, 0) + statutory_pay - ) - # Income tax is attributed by the earnings' share of total income, - # with statutory pay added and before any negative component (a - # loss), so that a loss set against other income does not drop or - # inflate the share of tax on these earnings. - income_before_losses = person("total_income", period) + statutory_pay - system = person.simulation.tax_benefit_system - for component in system.get_variable("total_income").adds: - income_before_losses += max_(-person(component, period), 0) - earnings_share = np.divide( - earnings, - income_before_losses, - out=np.zeros_like(earnings), - where=income_before_losses > 0, - ) - income_tax = person("income_tax", period) * min_(earnings_share, 1) - national_insurance = add( - person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] - ) - pension_contributions = ( - person("pension_contributions", period) - * p.pension_contribution_deduction_rate - ) - net_earnings = max_( - earnings - income_tax - national_insurance - pension_contributions, - 0, + net_earnings = person_net_earnings( + person, period, p.pension_contribution_deduction_rate ) # The claimant and partner: a child's or young person's earnings are # not the claimant's (reg 25(3); pension age reg 23(3)). diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index eea502562e..b0bad392d8 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -7,7 +7,11 @@ class income_support_applicable_income(Variable): label = "Relevant income for Income Support means test" definition_period = YEAR unit = GBP - reference = "https://www.legislation.gov.uk/uksi/1987/1967/schedule/8" + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/schedule/8", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/36", + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/38", + ) def formula(benunit, period, parameters): # Members whose income counts: the claimant and partner and, as the model did @@ -35,8 +39,10 @@ def formula(benunit, period, parameters): income += add_for_members(benunit, period, ["social_security_income"], members) income += benunit("income_support_tariff_income", period) income -= tax + # IS Regs 1987 reg 36(3)(b) and reg 38(1)(b)(ii), (3)(c) and (9)(b). income -= ( - add_for_members(benunit, period, ["pension_contributions"], members) * 0.5 + add_for_members(benunit, period, ["pension_contributions"], members) + * IS.means_test.pension_contribution_deduction_rate ) # Schedule 8 paras 5, 6 and 10 use mutually exclusive claimant types. mt = IS.means_test 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 55575e1987..65bef905a7 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 @@ -112,9 +112,13 @@ def formula(benunit, period, parameters): benunit, period, ["basic_income"], members ) - pension_contributions = ( - add_for_members(benunit, period, ["pension_contributions"], members) * 0.5 - ) + # The scheme's own share of pension contributions (one-half in each: + # SI 2012/2885 Sch 1 para 19(2)(b)-(c), WSI 2013/3029 Sch 1 para 13 + # and Sch 6 para 15, SSI 2012/319 reg 33 and SSI 2021/249 reg 50(3), + # and the councils' schemes). + pension_contributions = add_for_members( + benunit, period, ["pension_contributions"], members + ) * benunit("council_tax_reduction_pension_contribution_deduction_rate", period) tax = add_for_members( benunit, period, ["income_tax", "national_insurance"], members ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py new file mode 100644 index 0000000000..e76587f4bc --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_net_earnings.py @@ -0,0 +1,40 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income._net_earnings import ( + person_net_earnings, +) + + +class council_tax_reduction_net_earnings(Variable): + value_type = float + entity = BenUnit + label = "Council Tax Reduction net earnings" + documentation = ( + "Net earnings of the claimant and partner, from which the pensioner " + "Council Tax Reduction earnings disregards are taken. The earnings and " + "deductions are those of housing_benefit_net_earnings, except the " + "share of pension contributions, which is the scheme's own " + "(council_tax_reduction_pension_contribution_deduction_rate): " + "SI 2012/2885 Sch 1 para 19(2)(b)-(c), WSI 2013/3029 Sch 1 para " + "13(2)(b)-(c) and SSI 2012/319 reg 33(2)(b)-(c) each deduct one-half " + "and do not refer to Housing Benefit's share." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/19", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/29", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/13", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/23", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/33", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/36", + ) + + def formula(benunit, period, parameters): + person = benunit.members + rate = benunit( + "council_tax_reduction_pension_contribution_deduction_rate", period + ) + net_earnings = person_net_earnings(person, period, benunit.project(rate)) + # The claimant and partner, as for Housing Benefit. + claimant_or_partner = person("is_claimant_or_partner", period) + return benunit.sum(net_earnings * claimant_or_partner) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py new file mode 100644 index 0000000000..6b9b5372a6 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py @@ -0,0 +1,87 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.local_authorities.council_tax_reduction.config import ( + is_england_pensioner_scheme, + is_kingston_upon_thames_working_age, + is_merton_working_age, + is_newham_working_age, + is_oxford_working_age, + is_scotland_scheme, + is_wales_scheme, + is_westminster_working_age, +) + + +class council_tax_reduction_pension_contribution_deduction_rate(Variable): + value_type = float + entity = BenUnit + label = "Council Tax Reduction pension contribution deduction rate" + documentation = ( + "The share of pension contributions deducted from earnings in the " + "Council Tax Reduction means test, under the family's own scheme. " + "Each scheme states its own share, one-half in every scheme modelled: " + "the England pensioner scheme (SI 2012/2885 Sch 1 paras 19 and 29), " + "Wales (WSI 2013/3029 Sch 1 paras 13 and 23 and Sch 6 paras 15 and " + "24), Scotland (SSI 2012/319 regs 33 and 36; SSI 2021/249 regs 50 and " + "51(9)) and the working-age schemes of Merton, Kingston upon Thames, " + "Newham, Westminster and Oxford. None of them refers to Housing " + "Benefit's share, so a reform of one scheme's share moves only that " + "scheme. English working-age families outside those councils, whose " + "reductions the model takes as reported, and families outside Great " + "Britain, where there is no Council Tax Reduction, read the England " + "default scheme's share (SI 2012/2886 Sch paras 42, 52 and 61). In " + "Wales and Scotland an applicant under pension age with an award of " + "Universal Credit is assessed on the Universal Credit figures, which " + "deduct relievable pension contributions in full (WSI 2013/3029 Sch " + "6 para 9; SSI 2021/249 regs 49(6)(a) and 51(2)); the national " + "schemes' Universal Credit route is not modelled, so the share here " + "applies to them too." + ) + definition_period = YEAR + unit = "/1" + reference = ( + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/19", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/29", + "https://www.legislation.gov.uk/uksi/2012/2886/schedule/paragraph/52", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/13", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/15", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/33", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/50", + ) + + def formula(benunit, period, parameters): + la = parameters(period).gov.local_authorities + household = benunit.household + country = household("country", period) + local_authority = household("local_authority", period) + # The scheme follows the applicant's own family (SI 2012/2885 reg 3), + # as in simulated_council_tax_reduction_benunit. + has_pensioner = benunit("council_tax_reduction_pensioner", period) + + def rate(ctr): + return ctr.means_test.pension_contribution_deduction_rate + + return select( + [ + is_england_pensioner_scheme(country, has_pensioner), + is_scotland_scheme(country), + is_wales_scheme(country), + is_merton_working_age(local_authority, country, has_pensioner), + is_kingston_upon_thames_working_age( + local_authority, country, has_pensioner + ), + is_newham_working_age(local_authority, country, has_pensioner), + is_westminster_working_age(local_authority, country, has_pensioner), + is_oxford_working_age(local_authority, country, has_pensioner), + ], + [ + rate(la.england.council_tax_reduction.pensioners), + rate(la.scotland.council_tax_reduction), + rate(la.wales.council_tax_reduction), + rate(la.merton.council_tax_reduction), + rate(la.kingston_upon_thames.council_tax_reduction), + rate(la.newham.council_tax_reduction), + rate(la.westminster.council_tax_reduction), + rate(la.oxford.council_tax_reduction), + ], + default=rate(la.england.council_tax_reduction.default_scheme), + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py index 17e26734c0..2daf943eae 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pensioner_earnings_disregard.py @@ -17,9 +17,11 @@ class council_tax_reduction_pensioner_earnings_disregard(Variable): "disregards and the additional amount. The amounts and work " "conditions are those of the Housing Benefit pension-age schedule " "(SI 2006/214 Sch 4), and an employee's actual earnings have the same " - "deductions (SI 2012/2885 Sch 1 para 19, WSI 2013/3029 Sch 1 para " - "13, SSI 2012/319 reg 33), so the Housing Benefit net earnings and " - "work conditions are used. The notional tax on estimated earnings " + "kinds of deduction (SI 2012/2885 Sch 1 para 19, WSI 2013/3029 Sch 1 " + "para 13, SSI 2012/319 reg 33), so the Housing Benefit work " + "conditions and earnings rules are used, with the scheme's own share " + "of pension contributions (council_tax_reduction_net_earnings). The " + "notional tax on estimated earnings " "and on self-employed profit can differ (Scotland deducts only the " "personal allowance, SSI 2012/319 regs 33(4) and 37(1)); the model " "applies the Housing Benefit approximation to all of them. The earnings test leaves out " @@ -61,7 +63,7 @@ def national(amount): default=england[amount], ) - net_earnings = benunit("housing_benefit_net_earnings", period) + net_earnings = benunit("council_tax_reduction_net_earnings", period) # SI 2012/2885 Sch 4 paras 2 and 8; WSI 2013/3029 Sch 3 paras 2 and 8; # SSI 2012/319 Sch 2 paras 2 and 8. A lone parent has £25 in all, # not £25 plus para 8's £5, as for Housing Benefit. From cc86a337a958737465f389d0885669264a12ff13 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 22:03:34 -0400 Subject: [PATCH 2/4] Compare the float32 share within a tolerance in the property test The model holds council_tax_reduction_pension_contribution_deduction_rate as float32, so a 0.05 share reads back as 0.0500000007 and the exact comparison failed in CI (Test / Python 2/4). Compare within 1e-6 and drop the comment's wrong claim that twentieths are exact in binary. Co-Authored-By: Claude Opus 5.5 --- ...test_pension_contribution_deduction_rate_properties.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py b/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py index 45310d1403..51a613efbd 100644 --- a/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py +++ b/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py @@ -94,10 +94,12 @@ # pension age and 55 and under working age. WORKING_AGE = (25, 55) PENSION_AGE = (68, 90) -# Shares other than 0.5, on a grid of twentieths so that each is exact in -# binary and every pair differs by at least 0.05. +# Shares other than 0.5, on a grid of twentieths so that every pair differs +# by at least 0.05. The model holds the share as float32 (0.05 reads back as +# 0.0500000007), so shares are compared within RATE_TOLERANCE. SHARES = [i / 20 for i in range(21) if i != 10] TOLERANCE = 0.05 +RATE_TOLERANCE = 1e-6 @st.composite @@ -209,7 +211,7 @@ def test_each_scheme_moves_by_contributions_times_its_own_change_in_share( assert values["income_support_applicable_income"][i] > 0, case rate = "council_tax_reduction_pension_contribution_deduction_rate" assert base[rate][i] == 0.5, case - assert moved[rate][i] == shares[scheme], case + assert abs(moved[rate][i] - shares[scheme]) < RATE_TOLERANCE, case # Invariant 1 (with 2): exactly contributions x the scheme's change. for variable, share in ( ("council_tax_reduction_applicable_income", shares[scheme]), From d77037fc08bfe97337a095e4371e2320af6a6552 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:19:27 -0400 Subject: [PATCH 3/4] Address review round 1: reproducer, route coverage, UC caveats - Make the HB-reform disregard-cap reproducer read only variables that exist on main, so it fails there by value (disregard 250, not 200); the new variables move to a companion case. - Add a Hypothesis example with a contributing family on all 13 routes, so every route is tested whatever Hypothesis draws. - Extend the rate selector's UC caveat to Oxford, whose Appendix 3 assesses UC claimants on DWP's net earned income (#2520), and cite UC Regs 2013 reg 55(5)(a) for the full deduction. - Say that SSI 2021/249 reg 51(9) takes self-employed net profit from SSI 2012/303 regs 37 and 38, and that Oxford's share is for applicants who do not receive UC. - Describe HB net earnings as deducting HB's own share. Co-Authored-By: Claude Opus 5.5 --- .../pension_contribution_deduction_rate.yaml | 4 ++- .../pension_contribution_deduction_rate.yaml | 2 +- .../pension_contribution_deduction_rate.yaml | 31 ++++++++++++++++-- ..._contribution_deduction_rate_properties.py | 32 +++++++++++++++++-- .../housing_benefit_net_earnings.py | 5 +-- ...ion_pension_contribution_deduction_rate.py | 26 +++++++++------ 6 files changed, 81 insertions(+), 19 deletions(-) diff --git a/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml index 2856e8dc9c..ebac88d7f5 100644 --- a/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/oxford/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -1,4 +1,4 @@ -description: The Oxford Council Tax Reduction scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant. +description: The Oxford Council Tax Reduction scheme deducts this share of pension contributions when it calculates the net earnings and self-employed net profit of a working-age applicant who does not receive Universal Credit. An applicant who does is assessed on the net earned income DWP provides (Appendix 3), which the model does not yet do (issue 2520). values: 2026-04-01: 0.5 metadata: @@ -11,3 +11,5 @@ metadata: href: https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27 - title: Oxford City Council Council Tax Reduction Scheme 2026 to 2027, Annex 5 (one half of personal pension scheme payments, self-employed earners) href: https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27/3 + - title: Oxford City Council Council Tax Reduction Scheme 2026 to 2027, Appendix 3 (applicants who receive Universal Credit) + href: https://www.oxford.gov.uk/council-tax-reduction/council-tax-reduction-scheme-2026-27/4 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml index 0345f58a56..b3681ebec4 100644 --- a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/pension_contribution_deduction_rate.yaml @@ -13,7 +13,7 @@ metadata: href: https://www.legislation.gov.uk/ssi/2012/319/regulation/36 - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 50(3)(b) and (c) and (5)(c) (employed earners under pension age with no award of Universal Credit, from 1 April 2022) href: https://www.legislation.gov.uk/ssi/2021/249/regulation/50 - - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 51(9) (self-employed earners under pension age with no award of Universal Credit, by SSI 2012/303 reg. 37) + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 51(9) (self-employed earners under pension age with no award of Universal Credit, whose net profit reg. 51(9) takes from SSI 2012/303 regs. 37 and 38) href: https://www.legislation.gov.uk/ssi/2021/249/regulation/51 - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 35(3)(b) and (c) and (6)(c) and reg. 37(1)(b)(ii), (3)(c) and (9)(b) (persons under pension age, to 31 March 2022) href: https://www.legislation.gov.uk/ssi/2012/303/contents/made diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml index 9f34719d19..38fc3f1288 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/pension_contribution_deduction_rate.yaml @@ -698,7 +698,8 @@ # 300 - 0.25 x 200 = 250, but the Council Tax Reduction cap stays at its own # 300 - 0.5 x 200 = 200, so the disregard stays 200 and applicable income # 5,000. Reading Housing Benefit's net earnings, the disregard was 250 and - # applicable income 5,300 - 0.5 x 200 - 250 = 4,950. + # applicable income 5,300 - 0.5 x 200 - 250 = 4,950. The outputs use only + # variables that exist before this change, so the case fails there by value. period: 2026 absolute_error_margin: 0.01 input: @@ -719,12 +720,36 @@ members: [pensioner] country: ENGLAND output: - council_tax_reduction_pension_contribution_deduction_rate: 0.5 housing_benefit_net_earnings: 250 - council_tax_reduction_net_earnings: 200 council_tax_reduction_pensioner_earnings_disregard: 200 council_tax_reduction_applicable_income: 5_000 +- name: Under a reform of the Housing Benefit share Council Tax Reduction net earnings keep the scheme's share + # Model reform, not current law. The same pensioner: the England pensioner + # scheme's share stays 0.5, so its net earnings stay 300 - 0.5 x 200 = 200. + period: 2026 + absolute_error_margin: 0.01 + input: + gov.dwp.housing_benefit.means_test.pension_contribution_deduction_rate: 0.25 + people: + pensioner: + age: 70 + employment_income: 300 + personal_pension_contributions: 200 + state_pension: 5_000 + benunits: + benunit: + members: [pensioner] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + output: + council_tax_reduction_pension_contribution_deduction_rate: 0.5 + council_tax_reduction_net_earnings: 200 + - name: A couple's contributions are summed at the scheme's share # Scotland, working age. Each partner earns 10,000 and they contribute 2,000 # and 1,000. Applicable income is 20,000 - 0.5 x 3,000 = 18,500. diff --git a/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py b/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py index 51a613efbd..27b066f7db 100644 --- a/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py +++ b/policyengine_uk/tests/test_pension_contribution_deduction_rate_properties.py @@ -27,7 +27,8 @@ scheme's share (or Housing Benefit's) would miss invariant 1. Pensioners in Merton follow the England pensioner scheme, not Merton's, and families in Northern Ireland, which has no Council Tax Reduction, read the England - default scheme's share. + default scheme's share. An explicit example puts a contributing family on + every route, so each is tested whatever else Hypothesis draws. 3. The pensioner earnings disregards do not move when the disregard caps are slack. 4. Differential: at current law Council Tax Reduction net earnings equal @@ -38,7 +39,7 @@ """ import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st from policyengine_uk import Simulation @@ -184,12 +185,39 @@ def contributions(unit): return sum(adult["personal_pension_contributions"] for adult in unit["adults"]) +def every_route(): + """One contributing family on each route, a couple on one of them, so that + every route is tested whatever Hypothesis draws.""" + units = [] + for route in sorted(ROUTES): + age = 70 if ROUTES[route][2] else 40 + adult = dict( + age=age, employment_income=20_000.0, personal_pension_contributions=2_000.0 + ) + units.append(dict(route=route, adults=[adult])) + # A partner two years older, in the same age band as the head. + partner_age = units[0]["adults"][0]["age"] + 2 + units[0]["adults"].append( + dict( + age=partner_age, + employment_income=30_000.0, + personal_pension_contributions=1_500.0, + ) + ) + return units + + @PROPERTY_SETTINGS @given( st.lists(families(), min_size=1, max_size=26), reforms(), st.sampled_from([2026, 2028, 2030]), ) +@example( + units=every_route(), + shares=dict(zip(PARAMETERS, SHARES)), + year=2026, +) def test_each_scheme_moves_by_contributions_times_its_own_change_in_share( units, shares, year ): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py index c7b939c8f9..d060fcf3c0 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_net_earnings.py @@ -15,8 +15,9 @@ class housing_benefit_net_earnings(Variable): "in one employment is not set against another) plus statutory sick " "and maternity pay (regulation 35(1)(i); pension age regulation " "35(1)(h)), less income tax, " - "Class 1, 2 and 4 National Insurance, and half of pension " - "contributions, floored at zero. For an employee the regulations " + "Class 1, 2 and 4 National Insurance, and Housing Benefit's share of " + "pension contributions (pension_contribution_deduction_rate, one-half " + "in law), floored at zero. For an employee the regulations " "deduct the income tax deducted from the earnings (regulation 36(3); " "pension age regulation 36(2)); for the self-employed, a notional " "basic-rate tax on the profit alone less personal reliefs (regulation " diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py index 6b9b5372a6..883a78c9c8 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py @@ -18,23 +18,28 @@ class council_tax_reduction_pension_contribution_deduction_rate(Variable): documentation = ( "The share of pension contributions deducted from earnings in the " "Council Tax Reduction means test, under the family's own scheme. " - "Each scheme states its own share, one-half in every scheme modelled: " + "Each scheme sets its own share, one-half in every scheme modelled: " "the England pensioner scheme (SI 2012/2885 Sch 1 paras 19 and 29), " "Wales (WSI 2013/3029 Sch 1 paras 13 and 23 and Sch 6 paras 15 and " - "24), Scotland (SSI 2012/319 regs 33 and 36; SSI 2021/249 regs 50 and " - "51(9)) and the working-age schemes of Merton, Kingston upon Thames, " + "24), Scotland (SSI 2012/319 regs 33 and 36; SSI 2021/249 reg 50, and " + "for the self-employed reg 51(9), which applies SSI 2012/303 regs 37 " + "and 38) and the working-age schemes of Merton, Kingston upon Thames, " "Newham, Westminster and Oxford. None of them refers to Housing " "Benefit's share, so a reform of one scheme's share moves only that " "scheme. English working-age families outside those councils, whose " "reductions the model takes as reported, and families outside Great " "Britain, where there is no Council Tax Reduction, read the England " - "default scheme's share (SI 2012/2886 Sch paras 42, 52 and 61). In " - "Wales and Scotland an applicant under pension age with an award of " - "Universal Credit is assessed on the Universal Credit figures, which " - "deduct relievable pension contributions in full (WSI 2013/3029 Sch " - "6 para 9; SSI 2021/249 regs 49(6)(a) and 51(2)); the national " - "schemes' Universal Credit route is not modelled, so the share here " - "applies to them too." + "default scheme's share (SI 2012/2886 Sch paras 42, 52 and 61). " + "A working-age applicant with an award of Universal Credit is " + "assessed instead on the Secretary of State's Universal Credit " + "figures, which deduct relievable pension contributions in full " + "(UC Regs 2013 reg 55(5)(a)): in Wales (WSI 2013/3029 Sch 6 para 9), " + "Scotland (SSI 2021/249 regs 49(6)(a) and 51(2)) and Oxford (its " + "scheme's Appendix 3). The model has no such route for them yet " + "(#1962 for Wales and Scotland, #2520 for Oxford), so the share here " + "reaches those applicants too. Merton, Kingston upon Thames, Newham " + "and Westminster already assess them on Universal Credit income " + "(_legacy.py)." ) definition_period = YEAR unit = "/1" @@ -46,6 +51,7 @@ class council_tax_reduction_pension_contribution_deduction_rate(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/15", "https://www.legislation.gov.uk/ssi/2012/319/regulation/33", "https://www.legislation.gov.uk/ssi/2021/249/regulation/50", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/55", ) def formula(benunit, period, parameters): From daa3291a8fa4b0a3090b1eef6a0420440a768779 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 19 Oct 2026 23:29:24 -0400 Subject: [PATCH 4/4] Cite UC Regs reg 57(2) step 4 for self-employed UC earnings Review round 2 (finding A): reg 55(5)(a) covers employed earnings only; the full deduction of relievable pension contributions from self-employed earnings is reg 57(2) step 4. Documentation only. Co-Authored-By: Claude Opus 5.5 --- ...uncil_tax_reduction_pension_contribution_deduction_rate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py index 883a78c9c8..d18898ecfa 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_contribution_deduction_rate.py @@ -33,7 +33,8 @@ class council_tax_reduction_pension_contribution_deduction_rate(Variable): "A working-age applicant with an award of Universal Credit is " "assessed instead on the Secretary of State's Universal Credit " "figures, which deduct relievable pension contributions in full " - "(UC Regs 2013 reg 55(5)(a)): in Wales (WSI 2013/3029 Sch 6 para 9), " + "(UC Regs 2013 reg 55(5)(a) for employed earnings and reg 57(2) step 4 " + "for self-employed earnings): in Wales (WSI 2013/3029 Sch 6 para 9), " "Scotland (SSI 2021/249 regs 49(6)(a) and 51(2)) and Oxford (its " "scheme's Appendix 3). The model has no such route for them yet " "(#1962 for Wales and Scotland, #2520 for Oxford), so the share here " @@ -52,6 +53,7 @@ class council_tax_reduction_pension_contribution_deduction_rate(Variable): "https://www.legislation.gov.uk/ssi/2012/319/regulation/33", "https://www.legislation.gov.uk/ssi/2021/249/regulation/50", "https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/57", ) def formula(benunit, period, parameters):