diff --git a/changelog.d/ctr-pension-credit-disregard.fixed.md b/changelog.d/ctr-pension-credit-disregard.fixed.md new file mode 100644 index 0000000000..790fa13606 --- /dev/null +++ b/changelog.d/ctr-pension-credit-disregard.fixed.md @@ -0,0 +1 @@ +Disregard the whole income and capital of Pension Credit guarantee credit recipients in the England pensioner, Wales and Scotland council tax reduction schemes, so they get the maximum reduction less non-dependant deductions whatever their income or savings (SI 2012/2885 Sch 1 para 13; WSI 2013/3029 Sch 1 para 7; SSI 2012/319 reg 24). For savings-credit-only recipients, use the Pension Credit assessment of income plus the savings credit paid, and of capital (para 14; para 8; reg 25). Their reduction can fall, because the savings credit counts as income and the capital limit applies to all the capital Pension Credit counts, not only savings. diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/maximum_support_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/maximum_support_rate.yaml index bb0c523676..4e8503b317 100644 --- a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/maximum_support_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/maximum_support_rate.yaml @@ -6,5 +6,5 @@ metadata: period: year label: England Council Tax Reduction pensioner maximum support rate reference: - - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 - href: https://www.legislation.gov.uk/uksi/2012/2885/contents/made + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Sch. 1 para. 7(1) + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/7 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/capital_limit.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/capital_limit.yaml index c6543dad07..597a702def 100644 --- a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/capital_limit.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/capital_limit.yaml @@ -6,5 +6,5 @@ metadata: period: year label: England Council Tax Reduction pensioner capital limit reference: - - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 - href: https://www.legislation.gov.uk/uksi/2012/2885/contents/made + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 reg. 11(2) + href: https://www.legislation.gov.uk/uksi/2012/2885/regulation/11 diff --git a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/withdrawal_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/withdrawal_rate.yaml index c8ca6101ad..dc50fd0a2e 100644 --- a/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/withdrawal_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/england/council_tax_reduction/pensioners/means_test/withdrawal_rate.yaml @@ -6,5 +6,5 @@ metadata: period: year label: England Council Tax Reduction pensioner withdrawal rate reference: - - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 - href: https://www.legislation.gov.uk/uksi/2012/2885/contents/made + - title: The Council Tax Reduction Schemes (Prescribed Requirements) (England) Regulations 2012 Sch. 1 para. 3(f)(ii) (2 6/7 per cent a day of weekly excess income) + href: https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/3 diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/maximum_support_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/maximum_support_rate.yaml index d2042b6106..63af8384b7 100644 --- a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/maximum_support_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/maximum_support_rate.yaml @@ -6,5 +6,9 @@ metadata: period: year label: Scotland Council Tax Reduction maximum support rate reference: - - title: The Council Tax Reduction (Scotland) Regulations 2012 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 47(1) (pensioners) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/47 + - title: The Council Tax Reduction (Scotland) Regulations 2021 (persons under pension age) + href: https://www.legislation.gov.uk/ssi/2021/249/contents + - title: The Council Tax Reduction (Scotland) Regulations 2012 (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/scotland/council_tax_reduction/means_test/capital_limit.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/capital_limit.yaml index 8a659bb8ac..1600248c9f 100644 --- a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/capital_limit.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/capital_limit.yaml @@ -6,5 +6,9 @@ metadata: period: year label: Scotland Council Tax Reduction capital limit reference: - - title: The Council Tax Reduction (Scotland) Regulations 2012 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 40 (pensioners) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/40 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 66 (persons under pension age) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/66 + - title: The Council Tax Reduction (Scotland) Regulations 2012 (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/scotland/council_tax_reduction/means_test/withdrawal_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/withdrawal_rate.yaml index 21e1648704..0f1b238542 100644 --- a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/withdrawal_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/means_test/withdrawal_rate.yaml @@ -6,5 +6,9 @@ metadata: period: year label: Scotland Council Tax Reduction withdrawal rate reference: - - title: The Council Tax Reduction (Scotland) Regulations 2012 + - title: The Council Tax Reduction (State Pension Credit) (Scotland) Regulations 2012 reg. 14(5)(b) (pensioners; 2 6/7 per cent a day of weekly excess income) + href: https://www.legislation.gov.uk/ssi/2012/319/regulation/14 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 13 (persons under pension age) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/13 + - title: The Council Tax Reduction (Scotland) Regulations 2012 (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/maximum_support_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/maximum_support_rate.yaml index 377bc0caf7..6419af075e 100644 --- a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/maximum_support_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/maximum_support_rate.yaml @@ -6,5 +6,9 @@ metadata: period: year label: Wales Council Tax Reduction maximum support rate reference: - - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Sch. 1 para. 2(1) (pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/2 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 Sch. 6 para. 4(1) (persons who are not pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/schedule/6/paragraph/4 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (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/wales/council_tax_reduction/means_test/capital_limit.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/capital_limit.yaml index c3e4c1f7cc..db4a41bc50 100644 --- a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/capital_limit.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/capital_limit.yaml @@ -6,5 +6,7 @@ metadata: period: year label: Wales Council Tax Reduction capital limit reference: - - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 reg. 30 + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/30 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (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/wales/council_tax_reduction/means_test/withdrawal_rate.yaml b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/withdrawal_rate.yaml index 620e0f32b9..e8b6dc6b9a 100644 --- a/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/withdrawal_rate.yaml +++ b/policyengine_uk/parameters/gov/local_authorities/wales/council_tax_reduction/means_test/withdrawal_rate.yaml @@ -6,5 +6,9 @@ metadata: period: year label: Wales Council Tax Reduction withdrawal rate reference: - - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 reg. 23(f)(ii) (pensioners; 2 6/7 per cent a day of weekly excess income) + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/23 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2013 reg. 25(f)(ii) (persons who are not pensioners) + href: https://www.legislation.gov.uk/wsi/2013/3029/regulation/25 + - title: The Council Tax Reduction Schemes and Prescribed Requirements (Wales) Regulations 2012 (2013-14 schemes; revoked from 1 April 2014) href: https://www.legislation.gov.uk/wsi/2012/3144/contents/made diff --git a/policyengine_uk/programs.yaml b/policyengine_uk/programs.yaml index f83e4acdd9..46d253f103 100644 --- a/policyengine_uk/programs.yaml +++ b/policyengine_uk/programs.yaml @@ -521,7 +521,7 @@ programs: variable: council_tax_reduction parameter_prefix: gov.local_authorities verified_start_year: 2013 - notes: First-pass national support for Scotland, Wales, and England pensioner schemes, with reported CTB/CTR retained for unsupported English working-age local schemes. + notes: First-pass national support for Scotland, Wales, and England pensioner schemes (including the Pension Credit guarantee credit disregard and savings-credit-only rules), with reported CTB/CTR retained for unsupported English working-age local schemes. # --- Treasury programs --- - id: cost_of_living_support diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml new file mode 100644 index 0000000000..ce21f7d8a4 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_credit.yaml @@ -0,0 +1,615 @@ +# Pension Credit recipients under the pensioner council tax reduction schemes. +# Guarantee credit: the whole of the applicant's income and capital is +# disregarded (SI 2012/2885 Sch 1 para 13; WSI 2013/3029 Sch 1 para 7; SSI +# 2012/319 reg 24), so the award is the maximum reduction less non-dependant +# deductions, whatever the income, and the capital limit cannot apply (SI +# 2012/2885 reg 11(3); WSI 2013/3029 reg 30(2)). Savings credit only: the +# Secretary of State's assessment of income and capital is used, adjusted for +# the savings credit payable (SI 2012/2885 Sch 1 para 14; WSI 2013/3029 Sch 1 +# para 8; SSI 2012/319 reg 25). +# +# A minimum_guarantee input of 18,500 stands in for an earnings-linked +# guarantee above the 13,312 CTR applicable amount, as on the uk-triple-lock +# central path in 2039-40, where the State Pension figures below come from. + +- name: England guarantee credit recipient with income above the applicable amount gets maximum CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 18_712 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + minimum_guarantee: 18_500 + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + income_tax: 1_228.40 + guarantee_credit: 1_016.40 + in_receipt_of_guarantee_credit: true + council_tax_reduction_applicable_amount: 13_312 + # Before the disregard: 17,483.60 and a tapered CTR of 1,165.68. + council_tax_reduction_applicable_income: 0 + council_tax_reduction_assessable_capital: 0 + simulated_council_tax_reduction_benunit: 2_000 + council_tax_reduction: 2_000 + +- name: England guarantee credit recipient's CTR does not rise when the State Pension falls by 72.70 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 18_639.30 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + minimum_guarantee: 18_500 + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + # 72.70 less State Pension: income tax -14.54, guarantee credit +58.16. + income_tax: 1_213.86 + guarantee_credit: 1_074.56 + # Before the disregard CTR rose by 11.63 to 1,177.31. + council_tax_reduction: 2_000 + +- name: Entitled to guarantee credit but not claiming Pension Credit keeps the taper + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 18_712 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + minimum_guarantee: 18_500 + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + guarantee_credit: 1_016.40 + pension_credit: 0 + in_receipt_of_guarantee_credit: false + council_tax_reduction_applicable_income: 17_483.60 + # 2,000 - 0.2 x (17,483.60 - 13,312) + council_tax_reduction: 1_165.68 + +- name: England pensioner without Pension Credit keeps the taper + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 18_712 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + guarantee_credit: 0 + in_receipt_of_guarantee_credit: false + in_receipt_of_savings_credit_only: false + council_tax_reduction_applicable_income: 17_483.60 + council_tax_reduction: 1_165.68 + +- name: England guarantee credit recipient with capital above 16,000 gets maximum CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 20_000 + output: + # 12,376 - 9,000 - 1,040 deemed income from capital. + guarantee_credit: 2_336 + in_receipt_of_guarantee_credit: true + council_tax_reduction_assessable_capital: 0 + # Before the disregard the 16,000 capital limit gave 0. + council_tax_reduction: 2_000 + +- name: Capital limit still applies to a pensioner entitled to guarantee credit who does not claim it + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 20_000 + output: + guarantee_credit: 2_336 + in_receipt_of_guarantee_credit: false + council_tax_reduction_assessable_capital: 20_000 + council_tax_reduction: 0 + +- name: Pensioner couple on guarantee credit with 25,000 capital gets maximum CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + state_pension: 6_000 + partner: + age: 68 + state_pension: 6_000 + benunits: + benunit: + members: [claimant, partner] + claims_all_entitled_benefits: true + households: + household: + members: [claimant, partner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 25_000 + output: + guarantee_credit: 5_329 + in_receipt_of_guarantee_credit: true + council_tax_reduction: 2_000 + +- name: Non-dependant deductions still apply to a guarantee credit recipient + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + son: + age: 40 + employment_income: 5_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + son_benunit: + members: [son] + would_claim_uc: false + households: + household: + members: [pensioner, son] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + # The son's guarantee_credit is calculated as 7,376, but he is not + # eligible for Pension Credit, so he is not in receipt of it. + guarantee_credit: [3_376, 7_376] + in_receipt_of_guarantee_credit: [true, false] + council_tax_reduction_applicable_income: [0, 5_000] + # The national schemes' non-dependant deduction currently uses Housing + # Benefit amounts (20.40 a week here), not the CTR schedules' own amounts + # and exemptions (SI 2012/2885 Sch 1 para 8); this pins that the deduction + # still applies on guarantee credit, not its size. + council_tax_reduction_non_dep_deductions: [1_060.80, 0] + # 2,000 - 1,060.80 + simulated_council_tax_reduction_benunit: [939.20, 0] + council_tax_reduction: 939.20 + +- name: Mixed-age couple is not in receipt of guarantee credit, so the capital limit applies + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + partner: + age: 60 + benunits: + benunit: + members: [claimant, partner] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + household: + members: [claimant, partner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 20_000 + output: + guarantee_credit: 8_849 + is_pension_credit_eligible: false + in_receipt_of_guarantee_credit: false + council_tax_reduction: 0 + +- name: Wales guarantee credit recipient gets maximum CTR above the capital limit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: WALES + local_authority: CARDIFF + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 20_000 + output: + guarantee_credit: 2_336 + council_tax_reduction: 2_000 + +- name: Wales guarantee credit recipient with income above the applicable amount gets maximum CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 18_712 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + minimum_guarantee: 18_500 + households: + household: + members: [pensioner] + country: WALES + local_authority: CARDIFF + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + guarantee_credit: 1_016.40 + council_tax_reduction_applicable_income: 0 + council_tax_reduction: 2_000 + +- name: Wales working-age earner with a calculated guarantee credit keeps the taper + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 35 + employment_income: 10_000 + benunits: + benunit: + members: [claimant] + claims_all_entitled_benefits: true + would_claim_uc: false + households: + household: + members: [claimant] + country: WALES + local_authority: CARDIFF + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + guarantee_credit: 2_376 + in_receipt_of_guarantee_credit: false + council_tax_reduction_applicable_income: 10_000 + council_tax_reduction: 993.72 + +- name: Scotland guarantee credit recipient gets maximum CTR above the capital limit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 9_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: SCOTLAND + local_authority: GLASGOW_CITY + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 20_000 + output: + guarantee_credit: 2_336 + council_tax_reduction: 2_000 + +- name: Scotland guarantee credit recipient with income above the applicable amount gets maximum CTR + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 18_712 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + minimum_guarantee: 18_500 + households: + household: + members: [pensioner] + country: SCOTLAND + local_authority: GLASGOW_CITY + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + guarantee_credit: 976.73 + council_tax_reduction_applicable_income: 0 + council_tax_reduction: 2_000 + +- name: Savings credit only counts the Pension Credit assessment of income plus the savings credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 12_000 + private_pension_income: 1_500 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 0 + output: + guarantee_credit: 0 + savings_credit: 558.62 + in_receipt_of_savings_credit_only: true + pension_credit_income: 13_314 + # 13,314 + 558.62. Before: 13,314 (the savings credit was not counted). + council_tax_reduction_applicable_income: 13_872.62 + # 2,000 - 0.2 x (13,872.62 - 13,312). Before: 1,999.60. + council_tax_reduction: 1_887.88 + +- name: Savings credit only tests the Pension Credit assessment of capital against the limit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 12_000 + private_pension_income: 1_500 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 10_000 + other_residential_property_value: 7_000 + output: + savings_credit: 267.42 + in_receipt_of_savings_credit_only: true + # Pension Credit counts the second property as capital; household + # savings alone (10,000) are under the limit. Before: CTR 1,999.60. + pension_credit_assessable_capital: 17_000 + council_tax_reduction_assessable_capital: 17_000 + council_tax_reduction: 0 + +- name: The same pensioner not claiming Pension Credit is tested on household savings only + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 12_000 + private_pension_income: 1_500 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + would_claim_pc: false + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 10_000 + other_residential_property_value: 7_000 + output: + in_receipt_of_savings_credit_only: false + # Pensioners without Pension Credit are tested on household savings, a + # proxy that leaves out the second property the schemes' own capital rules + # count (SI 2012/2885 Sch 1 Part 6 Chapter 3). So, unlike the claimant + # above, this pensioner keeps CTR: the gap is in the proxy, not the law. + council_tax_reduction_assessable_capital: 10_000 + council_tax_reduction_applicable_income: 13_314 + council_tax_reduction: 1_999.60 + +- name: Capital of exactly 16,000 is within the limit for a pensioner without Pension Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 13_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 16_000 + output: + # The limit excludes capital that "exceeds" 16,000 (SI 2012/2885 reg 11(2)). + # The award pins the model's current value: it has no pensioner tariff + # income from capital (Sch 1 para 37, 624 a year here), a known gap + # (#1933), so this is not the full legal amount. + in_receipt_of_guarantee_credit: false + council_tax_reduction_assessable_capital: 16_000 + council_tax_reduction: 2_000 + +- name: Capital just over 16,000 excludes a pensioner without Pension Credit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 70 + state_pension: 13_000 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 16_000.01 + output: + council_tax_reduction_assessable_capital: 16_000.01 + council_tax_reduction: 0 + +- name: Savings-credit-only Pension Credit capital of exactly 16,000 is within the limit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 12_000 + private_pension_income: 1_500 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 16_000 + output: + # 2,000 - 0.2 x (14,247.02 - 13,312). The Secretary of State's capital is + # tested against the limit, which excludes only capital that exceeds + # 16,000 (SI 2012/2885 Sch 1 para 14(1) and reg 11(2)). + in_receipt_of_savings_credit_only: true + savings_credit: 309.02 + council_tax_reduction_assessable_capital: 16_000 + council_tax_reduction_applicable_income: 14_247.02 + council_tax_reduction: 1_813 + +- name: Savings-credit-only Pension Credit capital just over 16,000 is excluded + period: 2026 + absolute_error_margin: 0.01 + input: + people: + pensioner: + age: 90 + state_pension: 12_000 + private_pension_income: 1_500 + benunits: + benunit: + members: [pensioner] + claims_all_entitled_benefits: true + households: + household: + members: [pensioner] + country: ENGLAND + local_authority: MAIDSTONE + tenure_type: OWNED_OUTRIGHT + council_tax: 2_000 + savings: 16_000.01 + output: + in_receipt_of_savings_credit_only: true + council_tax_reduction_assessable_capital: 16_000.01 + council_tax_reduction: 0 diff --git a/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py new file mode 100644 index 0000000000..717ee595ce --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_pension_credit_properties.py @@ -0,0 +1,579 @@ +"""Property-based tests for Pension Credit recipients under the national +pensioner council tax reduction schemes (England pensioners, Wales, Scotland). + +Law: +- Guarantee credit: "the whole of his capital and income must be disregarded" + (SI 2012/2885 Sch 1 para 13; WSI 2013/3029 Sch 1 para 7; SSI 2012/319 reg + 24). Capital for the 16,000 limit is calculated under the same Part (SI + 2012/2885 reg 11(3); WSI 2013/3029 reg 30(2)), so the limit cannot apply. + The award is the maximum reduction less non-dependant deductions. +- Savings credit only: the Secretary of State's assessment of income and + capital is used, adjusted for the savings credit payable (SI 2012/2885 Sch 1 + para 14; WSI 2013/3029 Sch 1 para 8; SSI 2012/319 reg 25). + +Invariants, for any generated population of families, where L is the eligible +liability, ND the non-dependant deductions and A the applicable amount: + +1. Structural: 0 <= CTR <= L; receipt of guarantee credit and receipt of + savings credit only are mutually exclusive, and each requires Pension Credit + eligibility and take-up. +2. Guarantee credit recipients get exactly max(0, L - ND). +3. Savings-credit-only recipients get max(0, L - 0.2 x max(0, PC income + SC - + A) - ND) when the Pension Credit assessment of capital is within 16,000, and + nothing otherwise. +4. Differential: everyone else keeps the previous formula, max(0, L - 0.2 x + max(0, I - A) - ND) when household savings are within 16,000, where I is the + income definition on main before this change, recomputed here from its + components rather than read from the variable under test. +5. Metamorphic: a guarantee credit recipient's CTR does not change when the + State Pension changes. +6. Metamorphic: nor when its savings change. +7. Metamorphic: not claiming Pension Credit never raises a guarantee credit + recipient's CTR, and a non-claimant is assessed under invariant 4. +8. Under the Pension Credit freeze, receipt and the savings credit payable + follow the frozen (baseline) award, even where the reform alone would + change which credit is received. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2026 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Local authorities without a modelled local scheme, so each country's +# national scheme (England: pensioners only) applies. +COUNTRIES = { + "ENGLAND": "MAIDSTONE", + "WALES": "CARDIFF", + "SCOTLAND": "GLASGOW_CITY", +} +# 67 and over is unambiguously over State Pension age in 2026-27. Savings +# credit needs State Pension age reached before April 2016, so half the draws +# are 80 or over. +PENSION_AGE = st.one_of(st.integers(67, 79), st.integers(80, 100)) +WORKING_AGE = st.integers(18, 60) +SHAPES = { + "single_pension": [PENSION_AGE], + "couple_pension": [PENSION_AGE, PENSION_AGE], + "mixed_age": [PENSION_AGE, WORKING_AGE], + "single_working": [WORKING_AGE], +} +# Half the State Pension draws fall around the minimum guarantee, where +# guarantee credit, savings credit and the CTR taper all meet. +state_pension = st.one_of( + st.floats(0, 25_000, allow_nan=False, allow_infinity=False), + st.floats(10_000, 16_000, allow_nan=False, allow_infinity=False), +) +# Half the populations get a standard minimum guarantee above the CTR +# applicable amount (256.00 single, 383.35 couple a week), as an +# earnings-linked guarantee path produces, so guarantee credit recipients can +# have income above the applicable amount. Applied as a reform to the whole +# population: a minimum_guarantee input on some benefit units would set it to +# zero on every other one. +guarantees = st.one_of(st.none(), st.floats(240, 450)) +CAPITAL_LIMIT = 16_000 +WITHDRAWAL_RATE = 0.2 + + +@st.composite +def families(draw): + shape = draw(st.sampled_from(sorted(SHAPES))) + pension_shape = shape != "single_working" + return dict( + shape=shape, + ages=[draw(age) for age in SHAPES[shape]], + country=draw(st.sampled_from(sorted(COUNTRIES))), + council_tax=draw(st.floats(500, 4_000)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + other_property=draw(st.one_of(st.just(0.0), st.floats(0, 10_000))), + state_pension=draw(state_pension), + private_pension=draw(st.one_of(st.just(0.0), st.floats(0, 5_000))), + employment_income=draw(st.one_of(st.just(0.0), st.floats(0, 20_000))), + # Other income main's CTR definition counts, so the differential + # (invariant 4) sees more than pensions and earnings. + self_employment_income=draw(st.one_of(st.just(0.0), st.floats(0, 15_000))), + property_income=draw(st.one_of(st.just(0.0), st.floats(0, 5_000))), + carers_allowance=draw(st.one_of(st.just(0.0), st.floats(0, 4_500))), + child_ages=draw(st.lists(st.integers(1, 15), max_size=2)), + would_claim_uc=draw(st.booleans()), + would_claim_pc=draw(st.sampled_from([True, True, False])), + # A working-age non-dependant (a separate benefit unit) living with a + # pension-age family; younger than the claimant, so never the head. + non_dependant_income=( + draw(st.one_of(st.none(), st.floats(0, 15_000))) if pension_shape else None + ), + ) + + +@st.composite +def savings_credit_families(draw): + """Single pensioners who reached State Pension age before April 2016 with + income just above the minimum guarantee, the savings credit range.""" + return dict( + shape="single_pension", + ages=[draw(st.integers(80, 100))], + country=draw(st.sampled_from(sorted(COUNTRIES))), + council_tax=draw(st.floats(500, 4_000)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 25_000))), + other_property=draw(st.one_of(st.just(0.0), st.floats(0, 10_000))), + state_pension=draw(st.floats(12_400, 15_000)), + private_pension=draw(st.one_of(st.just(0.0), st.floats(0, 1_500))), + employment_income=0.0, + would_claim_pc=True, + non_dependant_income=draw(st.one_of(st.none(), st.floats(0, 15_000))), + ) + + +@st.composite +def guarantee_credit_families(draw): + """Pensioners claiming Pension Credit with income low enough to keep a + guarantee credit under the State Pension and savings changes the + metamorphic tests make.""" + shape = draw(st.sampled_from(["single_pension", "couple_pension"])) + return dict( + shape=shape, + ages=[draw(PENSION_AGE) for _ in SHAPES[shape]], + country=draw(st.sampled_from(sorted(COUNTRIES))), + council_tax=draw(st.floats(500, 4_000)), + savings=draw(st.one_of(st.just(0.0), st.floats(0, 40_000))), + other_property=0.0, + state_pension=draw(st.floats(0, 5_000)), + private_pension=0.0, + employment_income=0.0, + would_claim_pc=True, + non_dependant_income=draw(st.one_of(st.none(), st.floats(0, 15_000))), + ) + + +# Random families, plus a few that are sure to receive guarantee credit, so +# the metamorphic tests always have recipients to check. +populations = st.builds( + lambda random, recipients: random + recipients, + st.lists(families(), min_size=15, max_size=30), + st.lists(guarantee_credit_families(), min_size=3, max_size=6), +) + + +def situation( + units, + state_pension_change=0.0, + savings_change=0.0, + would_claim_pc=None, +): + people, benunits, households = {}, {}, {} + claimant_benunits = [] + benunit_index = 0 + for i, unit in enumerate(units): + names = [] + for j, age in enumerate(unit["ages"]): + name = f"p{i}_{j}" + person = {"age": {YEAR: age}} + if age >= 67: + person["state_pension"] = { + YEAR: max(0.0, unit["state_pension"] + state_pension_change) + } + person["private_pension_income"] = {YEAR: unit["private_pension"]} + else: + person["employment_income"] = {YEAR: unit["employment_income"]} + person["self_employment_income"] = { + YEAR: unit.get("self_employment_income", 0.0) + } + if j == 0: + person["property_income"] = {YEAR: unit.get("property_income", 0.0)} + person["carers_allowance"] = {YEAR: unit.get("carers_allowance", 0.0)} + people[name] = person + names.append(name) + for k, child_age in enumerate(unit.get("child_ages", [])): + name = f"c{i}_{k}" + people[name] = {"age": {YEAR: child_age}} + names.append(name) + claim_pc = unit["would_claim_pc"] if would_claim_pc is None else would_claim_pc + benunit = { + "members": names, + # claims_all_entitled_benefits sums reported benefits across the + # whole simulation, so set it for every family. + "claims_all_entitled_benefits": {YEAR: True}, + "would_claim_pc": {YEAR: claim_pc}, + "would_claim_uc": {YEAR: unit.get("would_claim_uc", False)}, + } + benunits[f"b{i}"] = benunit + claimant_benunits.append(benunit_index) + benunit_index += 1 + household_members = list(names) + if unit["non_dependant_income"] is not None: + name = f"n{i}" + people[name] = { + "age": {YEAR: 40}, + "employment_income": {YEAR: unit["non_dependant_income"]}, + } + benunits[f"n{i}"] = { + "members": [name], + "claims_all_entitled_benefits": {YEAR: True}, + "would_claim_uc": {YEAR: False}, + } + benunit_index += 1 + household_members.append(name) + households[f"h{i}"] = { + "members": household_members, + "country": {YEAR: unit["country"]}, + "local_authority": {YEAR: COUNTRIES[unit["country"]]}, + "tenure_type": {YEAR: "OWNED_OUTRIGHT"}, + "council_tax": {YEAR: unit["council_tax"]}, + "savings": {YEAR: max(0.0, unit["savings"] + savings_change)}, + "other_residential_property_value": {YEAR: unit["other_property"]}, + } + return ( + {"people": people, "benunits": benunits, "households": households}, + np.array(claimant_benunits), + ) + + +BENUNIT_VARIABLES = [ + "simulated_council_tax_reduction_benunit", + "council_tax_reduction_applicable_amount", + "council_tax_reduction_applicable_income", + "council_tax_reduction_non_dep_deductions", + "in_receipt_of_guarantee_credit", + "in_receipt_of_savings_credit_only", + "is_pension_credit_eligible", + "would_claim_pc", + "guarantee_credit", + "savings_credit", + "pension_credit_income", + "pension_credit_assessable_capital", +] +# council_tax_reduction_applicable_income on main before this change (1c5b4d04): +# these incomes and benefits, less income tax, National Insurance and half of +# pension contributions, floored at zero. +MAIN_INCOME_COMPONENTS = [ + "employment_income", + "self_employment_income", + "property_income", + "private_pension_income", + "carers_allowance", + "esa_contrib", + "jsa_contrib", + "state_pension", + "maternity_allowance", + "statutory_sick_pay", + "statutory_maternity_pay", + "ssmg", + "tax_credits", + "child_benefit", + "income_support", + "jsa_income", + "esa_income", + "universal_credit", +] +MAIN_DEDUCTIONS = ["income_tax", "national_insurance"] +HOUSEHOLD_VARIABLES = [ + "council_tax_reduction_maximum_eligible_liability", + "council_tax_reduction_household_has_pensioner", + "savings", +] + + +def guarantee_reform(single_weekly): + if single_weekly is None: + return None + period = f"{YEAR}-01-01.{YEAR}-12-31" + minimum_guarantee = "gov.dwp.pension_credit.guarantee_credit.minimum_guarantee" + return { + f"{minimum_guarantee}.SINGLE": {period: single_weekly}, + f"{minimum_guarantee}.COUPLE": {period: single_weekly * 1.53}, + } + + +def calculate(units, guarantee=None, **kwargs): + data, claimants = situation(units, **kwargs) + sim = Simulation(situation=data, reform=guarantee_reform(guarantee)) + values = { + v: np.asarray(sim.calculate(v, YEAR))[claimants] for v in BENUNIT_VARIABLES + } + # One household per family, in the same order as the claimant benefit units. + for v in HOUSEHOLD_VARIABLES: + values[v] = np.asarray(sim.calculate(v, YEAR)) + + def benunit_total(names): + return sum( + np.asarray(sim.calculate(v, YEAR, map_to="benunit"))[claimants] + for v in names + ) + + values["income_under_main_definition"] = np.maximum( + 0, + benunit_total(MAIN_INCOME_COMPONENTS) + - benunit_total(MAIN_DEDUCTIONS) + - 0.5 * benunit_total(["pension_contributions"]), + ) + values["national"] = np.array( + [ + unit["country"] != "ENGLAND" or has_pensioner + for unit, has_pensioner in zip( + units, values["council_tax_reduction_household_has_pensioner"] + ) + ] + ) + return values + + +def tapered(liability, income, applicable_amount, non_dep): + excess = max(0.0, income - applicable_amount) + return max(0.0, liability - WITHDRAWAL_RATE * excess - non_dep) + + +def check_structural(values): + ctr = values["simulated_council_tax_reduction_benunit"] + liability = values["council_tax_reduction_maximum_eligible_liability"] + gc = values["in_receipt_of_guarantee_credit"].astype(bool) + sc_only = values["in_receipt_of_savings_credit_only"].astype(bool) + received = gc | sc_only + assert np.all(ctr >= 0) + assert np.all(ctr <= liability + 0.01) + assert not np.any(gc & sc_only) + assert np.all(values["is_pension_credit_eligible"][received]) + assert np.all(values["would_claim_pc"][received]) + assert np.all(values["guarantee_credit"][gc] > 0) + assert np.all(values["savings_credit"][sc_only] > 0) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=20, max_size=40), guarantees) +def test_awards_follow_the_pension_credit_routes(units, guarantee): + check_routes(units, calculate(units, guarantee)) + + +@PROPERTY_SETTINGS +@given(st.lists(savings_credit_families(), min_size=10, max_size=20)) +def test_savings_credit_only_awards_use_the_pension_credit_assessment(units): + values = calculate(units) + assert values["in_receipt_of_savings_credit_only"].any() + check_routes(units, values) + + +def check_routes(units, values): + check_structural(values) + for i, unit in enumerate(units): + if not values["national"][i]: + continue + ctr = values["simulated_council_tax_reduction_benunit"][i] + liability = values["council_tax_reduction_maximum_eligible_liability"][i] + non_dep = values["council_tax_reduction_non_dep_deductions"][i] + applicable_amount = values["council_tax_reduction_applicable_amount"][i] + if values["in_receipt_of_guarantee_credit"][i]: + expected = max(0.0, liability - non_dep) + assert values["council_tax_reduction_applicable_income"][i] == 0, unit + elif values["in_receipt_of_savings_credit_only"][i]: + income = values["pension_credit_income"][i] + values["savings_credit"][i] + assert ( + abs(values["council_tax_reduction_applicable_income"][i] - income) + < 0.01 + ), unit + expected = tapered(liability, income, applicable_amount, non_dep) * ( + values["pension_credit_assessable_capital"][i] <= CAPITAL_LIMIT + ) + else: + income = values["income_under_main_definition"][i] + assert ( + abs(values["council_tax_reduction_applicable_income"][i] - income) + < 0.01 + ), unit + expected = tapered(liability, income, applicable_amount, non_dep) * ( + values["savings"][i] <= CAPITAL_LIMIT + ) + assert abs(ctr - expected) < 0.01, (unit, ctr, expected) + + +@PROPERTY_SETTINGS +@given( + populations, + guarantees, + st.floats(-3_000, 3_000, allow_nan=False, allow_infinity=False), +) +def test_guarantee_credit_recipients_ctr_is_invariant_to_state_pension( + units, guarantee, change +): + before = calculate(units, guarantee) + after = calculate(units, guarantee, state_pension_change=change) + assert before["in_receipt_of_guarantee_credit"].any() + for i, unit in enumerate(units): + if ( + before["in_receipt_of_guarantee_credit"][i] + and after["in_receipt_of_guarantee_credit"][i] + ): + assert ( + abs( + before["simulated_council_tax_reduction_benunit"][i] + - after["simulated_council_tax_reduction_benunit"][i] + ) + < 0.01 + ), (unit, change) + + +@PROPERTY_SETTINGS +@given( + populations, + guarantees, + st.floats(0, 30_000, allow_nan=False, allow_infinity=False), +) +def test_guarantee_credit_recipients_ctr_is_invariant_to_savings( + units, guarantee, change +): + before = calculate(units, guarantee) + after = calculate(units, guarantee, savings_change=change) + assert before["in_receipt_of_guarantee_credit"].any() + for i, unit in enumerate(units): + if ( + before["in_receipt_of_guarantee_credit"][i] + and after["in_receipt_of_guarantee_credit"][i] + ): + assert ( + abs( + before["simulated_council_tax_reduction_benunit"][i] + - after["simulated_council_tax_reduction_benunit"][i] + ) + < 0.01 + ), (unit, change) + + +@PROPERTY_SETTINGS +@given(populations, guarantees) +def test_not_claiming_pension_credit_never_raises_a_recipients_ctr(units, guarantee): + claiming = calculate(units, guarantee, would_claim_pc=True) + not_claiming = calculate(units, guarantee, would_claim_pc=False) + assert claiming["in_receipt_of_guarantee_credit"].any() + assert not np.any(not_claiming["in_receipt_of_guarantee_credit"]) + check_routes(units, not_claiming) + for i, unit in enumerate(units): + if claiming["in_receipt_of_guarantee_credit"][i]: + assert ( + not_claiming["simulated_council_tax_reduction_benunit"][i] + <= claiming["simulated_council_tax_reduction_benunit"][i] + 0.01 + ), unit + + +def test_savings_credit_only_award_counts_the_frozen_savings_credit(): + # A savings credit rate rise raises the reform's savings credit from + # 558.62 to 869.89, but the freeze keeps paying the baseline 558.62, and + # that is the savings credit payable. + person = { + "age": {YEAR: 90}, + "state_pension": {YEAR: 12_000}, + "private_pension_income": {YEAR: 1_500}, + } + data = { + "people": {"pensioner": person}, + "benunits": { + "benunit": { + "members": ["pensioner"], + "claims_all_entitled_benefits": {YEAR: True}, + } + }, + "households": { + "household": { + "members": ["pensioner"], + "country": {YEAR: "ENGLAND"}, + "local_authority": {YEAR: "MAIDSTONE"}, + "tenure_type": {YEAR: "OWNED_OUTRIGHT"}, + "council_tax": {YEAR: 2_000}, + "savings": {YEAR: 0}, + } + }, + } + period = f"{YEAR}-01-01.{YEAR}-12-31" + rate_rise = {"gov.dwp.pension_credit.savings_credit.rate.phase_in": {period: 0.8}} + frozen = Simulation( + situation=data, + reform={**rate_rise, "gov.contrib.freeze_pension_credit": {period: True}}, + ) + unfrozen = Simulation(situation=data, reform=rate_rise) + assert abs(frozen.calculate("savings_credit", YEAR)[0] - 869.89) < 0.01 + assert abs(frozen.calculate("pension_credit", YEAR)[0] - 558.62) < 0.01 + assert frozen.calculate("in_receipt_of_savings_credit_only", YEAR)[0] + # 13,314 Pension Credit income + 558.62 paid; 2,000 - 0.2 x 560.62. + frozen_income = frozen.calculate("council_tax_reduction_applicable_income", YEAR) + assert abs(frozen_income[0] - 13_872.62) < 0.01 + assert abs(frozen.calculate("council_tax_reduction", YEAR)[0] - 1_887.88) < 0.01 + # Without the freeze the higher savings credit is paid and counted. + unfrozen_income = unfrozen.calculate( + "council_tax_reduction_applicable_income", YEAR + ) + assert abs(unfrozen_income[0] - 14_183.89) < 0.01 + + +def freeze_case(age, state_pension, private_pension, savings, reform): + person = { + "age": {YEAR: age}, + "state_pension": {YEAR: state_pension}, + "private_pension_income": {YEAR: private_pension}, + } + data = { + "people": {"pensioner": person}, + "benunits": { + "benunit": { + "members": ["pensioner"], + "claims_all_entitled_benefits": {YEAR: True}, + } + }, + "households": { + "household": { + "members": ["pensioner"], + "country": {YEAR: "ENGLAND"}, + "local_authority": {YEAR: "MAIDSTONE"}, + "tenure_type": {YEAR: "OWNED_OUTRIGHT"}, + "council_tax": {YEAR: 2_000}, + "savings": {YEAR: savings}, + } + }, + } + period = f"{YEAR}-01-01.{YEAR}-12-31" + reform = {key: {period: value} for key, value in reform.items()} + frozen = Simulation( + situation=data, + reform={**reform, "gov.contrib.freeze_pension_credit": {period: True}}, + ) + unfrozen = Simulation(situation=data, reform=reform) + return frozen, unfrozen + + +def test_freeze_keeps_savings_credit_only_receipt_when_the_reform_adds_guarantee_credit(): + # A higher guarantee gives the reform a guarantee credit, but the frozen + # award is the baseline's savings credit of 558.62. + frozen, unfrozen = freeze_case( + 90, + 12_000, + 1_500, + 0, + {"gov.dwp.pension_credit.guarantee_credit.minimum_guarantee.SINGLE": 300}, + ) + assert unfrozen.calculate("in_receipt_of_guarantee_credit", YEAR)[0] + assert abs(unfrozen.calculate("council_tax_reduction", YEAR)[0] - 2_000) < 0.01 + assert not frozen.calculate("in_receipt_of_guarantee_credit", YEAR)[0] + assert frozen.calculate("in_receipt_of_savings_credit_only", YEAR)[0] + # 2,000 - 0.2 x (13,314 + 558.62 - 13,312) + assert abs(frozen.calculate("council_tax_reduction", YEAR)[0] - 1_887.88) < 0.01 + + +def test_freeze_keeps_guarantee_credit_receipt_when_the_reform_removes_it(): + # With no minimum guarantee the reform pays no guarantee credit, so the + # 20,000 savings exceed the capital limit; the frozen award is still the + # baseline guarantee credit, so capital is disregarded. + frozen, unfrozen = freeze_case( + 70, + 9_000, + 0, + 20_000, + {"gov.dwp.pension_credit.guarantee_credit.minimum_guarantee.SINGLE": 0}, + ) + assert not unfrozen.calculate("in_receipt_of_guarantee_credit", YEAR)[0] + assert unfrozen.calculate("council_tax_reduction", YEAR)[0] == 0 + assert frozen.calculate("in_receipt_of_guarantee_credit", YEAR)[0] + assert abs(frozen.calculate("council_tax_reduction", YEAR)[0] - 2_000) < 0.01 diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py new file mode 100644 index 0000000000..877ab3606b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/guarantee_credit/in_receipt_of_guarantee_credit.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * + + +class in_receipt_of_guarantee_credit(Variable): + label = "in receipt of Guarantee Credit" + documentation = ( + "Whether the claimant or partner in this benefit unit is in receipt of " + "a guarantee credit: the benefit unit is eligible for Pension Credit, " + "claims it, and its Guarantee Credit is positive. A Guarantee Credit " + "computed for a family that does not claim Pension Credit, or that the " + "model does not treat as eligible for it (for example a working-age " + "family), is not received. The model's Pension Credit eligibility " + "omits the SI 2019/37 article 4 saving, so a mixed-age couple it saves " + "(entitled to Pension Credit or Housing Benefit as that couple on 14 " + "May 2019, and to either ever since) is not treated as in receipt. Under " + "the Pension Credit freeze the baseline receipt is kept, because the " + "frozen award is the baseline award." + ) + entity = BenUnit + definition_period = YEAR + value_type = bool + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/2002/16/section/2", + "https://www.legislation.gov.uk/ukpga/1992/5/section/1", + "https://www.legislation.gov.uk/uksi/2019/37/article/4", + ) + + def formula(benunit, period, parameters): + freeze = parameters(period).gov.contrib.freeze_pension_credit + baseline = benunit.simulation.baseline + if freeze and baseline is not None: + return baseline.populations["benunit"]( + "in_receipt_of_guarantee_credit", period + ) + eligible = benunit("is_pension_credit_eligible", period) + would_claim = benunit("would_claim_pc", period) + guarantee_credit = benunit("guarantee_credit", period) + return eligible & would_claim & (guarantee_credit > 0) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/in_receipt_of_savings_credit_only.py b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/in_receipt_of_savings_credit_only.py new file mode 100644 index 0000000000..6f178fca7b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/savings_credit/in_receipt_of_savings_credit_only.py @@ -0,0 +1,33 @@ +from policyengine_uk.model_api import * + + +class in_receipt_of_savings_credit_only(Variable): + label = "in receipt of Savings Credit only" + documentation = ( + "Whether the claimant or partner in this benefit unit has an award of " + "Pension Credit comprising only the savings credit: the benefit unit is " + "eligible for Pension Credit, claims it, its Savings Credit is positive " + "and it has no Guarantee Credit. Under the Pension Credit freeze the " + "baseline receipt is kept, because the frozen award is the baseline " + "award." + ) + entity = BenUnit + definition_period = YEAR + value_type = bool + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/1", + "https://www.legislation.gov.uk/ukpga/2002/16/section/3", + ) + + def formula(benunit, period, parameters): + freeze = parameters(period).gov.contrib.freeze_pension_credit + baseline = benunit.simulation.baseline + if freeze and baseline is not None: + return baseline.populations["benunit"]( + "in_receipt_of_savings_credit_only", period + ) + eligible = benunit("is_pension_credit_eligible", period) + would_claim = benunit("would_claim_pc", period) + guarantee_credit = benunit("guarantee_credit", period) + savings_credit = benunit("savings_credit", period) + return eligible & would_claim & (guarantee_credit <= 0) & (savings_credit > 0) 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 8cf5f764b8..ea624d139d 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 @@ -5,8 +5,25 @@ class council_tax_reduction_applicable_income(Variable): value_type = float entity = BenUnit label = "relevant income for Council Tax Reduction means test" + documentation = ( + "Income taken into account in the Council Tax Reduction means test. The " + "pensioner schemes disregard the whole income of an applicant who, or " + "whose partner, is in receipt of Pension Credit guarantee credit, and " + "use the Secretary of State's Pension Credit assessment of income, plus " + "the savings credit payable, where the award is savings credit only. " + "The other adjustments those provisions allow (childcare charges, lone " + "parent and maintenance disregards, and the rest) are not modelled." + ) definition_period = YEAR unit = GBP + reference = [ + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/13", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/14", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/7", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/24", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/25", + ] def formula(benunit, period, parameters): benunit_means_tested_benefits = [ @@ -44,4 +61,28 @@ def formula(benunit, period, parameters): pension_contributions = add(benunit, period, ["pension_contributions"]) * 0.5 tax = add(benunit, period, ["income_tax", "national_insurance"]) - return max_(0, increased_income - tax - pension_contributions) + income_under_general_rules = max_( + 0, increased_income - tax - pension_contributions + ) + + # SI 2012/2885 Sch 1 para 13, WSI 2013/3029 Sch 1 para 7 and SSI + # 2012/319 reg 24: a guarantee credit recipient's whole income is + # disregarded. Para 14, para 8 and reg 25: in savings-credit-only cases + # the Secretary of State's assessment of net income is used, adjusted + # to take account of the savings credit payable. The Pension Credit + # paid on a savings-credit-only award is that savings credit (under the + # Pension Credit freeze, the frozen amount). + in_receipt_of_guarantee_credit = benunit( + "in_receipt_of_guarantee_credit", period + ) + has_savings_credit_only_award = benunit( + "in_receipt_of_savings_credit_only", period + ) + savings_credit_only_income = benunit("pension_credit_income", period) + benunit( + "pension_credit", period + ) + return select( + [in_receipt_of_guarantee_credit, has_savings_credit_only_award], + [0, savings_credit_only_income], + default=income_under_general_rules, + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py new file mode 100644 index 0000000000..16b63ab2a9 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_assessable_capital.py @@ -0,0 +1,44 @@ +from policyengine_uk.model_api import * + + +class council_tax_reduction_assessable_capital(Variable): + value_type = float + entity = BenUnit + label = "capital for the Council Tax Reduction capital limit" + documentation = ( + "Capital tested against the Council Tax Reduction capital limit. The " + "pensioner schemes disregard the whole capital of an applicant who, or " + "whose partner, is in receipt of Pension Credit guarantee credit, and " + "use the Secretary of State's Pension Credit assessment of capital where " + "the award is savings credit only. Otherwise household savings stand in " + "for the applicant's capital; the schemes' own capital rules also count " + "other property and investments, so this understates capital for " + "pensioners without Pension Credit. The recalculation when capital rises " + "above the limit during an assessed income period is not modelled." + ) + definition_period = YEAR + unit = GBP + quantity_type = STOCK + reference = [ + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/11", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/13", + "https://www.legislation.gov.uk/uksi/2012/2885/schedule/1/paragraph/14", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/30", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/7", + "https://www.legislation.gov.uk/wsi/2013/3029/schedule/1/paragraph/8", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/24", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/25", + ] + + def formula(benunit, period, parameters): + in_receipt_of_guarantee_credit = benunit( + "in_receipt_of_guarantee_credit", period + ) + has_savings_credit_only_award = benunit( + "in_receipt_of_savings_credit_only", period + ) + return select( + [in_receipt_of_guarantee_credit, has_savings_credit_only_award], + [0, benunit("pension_credit_assessable_capital", period)], + default=benunit.household("savings", period), + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py index ce3eeb7ee9..8eb33274c5 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/simulated_council_tax_reduction_benunit.py @@ -82,7 +82,8 @@ def formula(benunit, period, parameters): - excess_income * withdrawal_rate - non_dep_deductions, ) - capital_eligible = benunit.household("savings", period) <= capital_limit + capital = benunit("council_tax_reduction_assessable_capital", period) + capital_eligible = capital <= capital_limit national_ctr = ( national_scheme * is_household_head_benunit