diff --git a/changelog.d/pension-credit-earnings-uprating.fixed.md b/changelog.d/pension-credit-earnings-uprating.fixed.md new file mode 100644 index 0000000000..d5a3f39534 --- /dev/null +++ b/changelog.d/pension-credit-earnings-uprating.fixed.md @@ -0,0 +1 @@ +Uprate the Pension Credit standard minimum guarantee and the lower Housing Benefit pension-age personal allowances by the previous May-July earnings increase, meeting the SSAA 1992 s150A earnings floor after the last published rates. Derive projected Savings Credit thresholds from the guarantee and a September-CPI-uprated maximum award, keeping the threshold in force when a reform sets a zero phase-in rate, and the higher pension-age allowances as the guarantee plus that maximum. April 2027's single guarantee rises from £238 to £247.282 before rounding, rather than £243.47. diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged.yaml index 69333ab432..d1b5a78e9a 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged.yaml @@ -15,15 +15,15 @@ metadata: period: week unit: currency-GBP label: Housing Benefit couple pension-age personal allowance - # Not uprated by September CPI: the pension-age personal allowances rose 4.75% - # in April 2026 (£244.40 to £256.00 single), when September CPI was 3.8%, and - # 2.3% in April 2017, when it was 1.0% (Benefit and pension rates 2026 to - # 2027, 2017 to 2018). Keeps the lagged calendar-year CPI projection it had - # through gov.benefit_uprating_cpi until its own basis is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Not uprated as a whole: the allowance is the Pension Credit standard + # minimum guarantee plus the maximum Savings Credit (HB A3/2021), which + # rise by earnings and September CPI. create_pension_age_allowances.py + # derives the years after the published amounts as that sum. reference: - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 (SI 2006/214), Schedule 3, paragraph 1(2)(b) href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3 + - title: DWP Housing Benefit circular A3/2021, paragraphs 3-5 (the allowance is the standard minimum guarantee plus the maximum Savings Credit) + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2021/a32021-housing-benefit-person-who-have-attained-the-qualifying-age-for-state-pension-credit-amendment-regulations-2021 - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations (Northern Ireland) 2006 (SR 2006/406), Schedule 4, paragraph 1 (the same amounts) href: https://www.legislation.gov.uk/nisr/2006/406/schedule/4 - title: Benefit and pension rates 2024 to 2025 - Policy paper diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged_from_cutoff.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged_from_cutoff.yaml index 01ccc8d258..2eee24814d 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged_from_cutoff.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/couple/aged_from_cutoff.yaml @@ -10,13 +10,10 @@ metadata: period: week unit: currency-GBP label: Housing Benefit couple personal allowance, State Pension age reached from April 2021 - # Not uprated by September CPI: the post-April 2021 pension-age personal - # allowances equal the Pension Credit standard minimum guarantee in every year - # from 2021-22 to 2026-27, which is reviewed against earnings under Social - # Security Administration Act 1992 s150A(1)(d). Keeps the lagged calendar-year - # CPI projection it had through gov.benefit_uprating_cpi until its own basis - # is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Equal to the Pension Credit standard minimum guarantee in 2021-2026; + # project at its earnings floor (SSAA 1992 s150A(2)). The HB allowance is + # set in Schedule 3 below. + uprating: gov.economic_assumptions.indices.earnings_uprating reference: - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) (Amendment) Regulations 2021 (SI 2021/188), regulation 2(3), inserting paragraph 1(2)(c) of Schedule 3 to SI 2006/214 from 1 April 2021 href: https://www.legislation.gov.uk/uksi/2021/188/regulation/2/made diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/create_pension_age_allowances.py b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/create_pension_age_allowances.py new file mode 100644 index 0000000000..a715f291fd --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/create_pension_age_allowances.py @@ -0,0 +1,43 @@ +"""Project the higher pension-age Housing Benefit personal allowances. + +For claimants who reached State Pension age before 1 April 2021, SI 2006/214 +Schedule 3 paragraph 1(1)(b) and (2)(b) set a personal allowance made up of +the Pension Credit standard minimum guarantee plus the maximum Savings Credit +(DWP circular HB A3/2021, paragraphs 3-5). Every published amount from +2019-20 to 2026-27 is that sum rounded up to the next 5p. The guarantee rises +with earnings and the maximum with September CPI, so neither index alone +projects the allowance. Keep the published allowances and derive later years +as the guarantee plus the maximum, each already uprated by its own index. + +Run after general uprating and before fiscal-year conversion, on both the +main tree and its baseline copy, as the Savings Credit threshold projection +is. Reprocessing a reform then reads its changed guarantee and maximum. +""" + +from policyengine_core.parameters import ParameterNode + +from policyengine_uk.parameters.gov.dwp.pension_credit.savings_credit.create_savings_credit_threshold import ( + extend_after_last_stated, + last_projected_year, +) + +# The Pension Credit amounts each allowance is built from: a lone parent's +# are a single person's. +RELATIONS = {"single": "SINGLE", "couple": "COUPLE", "lone_parent": "SINGLE"} + + +def project_pension_age_allowances(parameters: ParameterNode) -> ParameterNode: + """Fill the years after each higher allowance's last stated value.""" + pc = parameters.gov.dwp.pension_credit + allowances = parameters.gov.dwp.housing_benefit.allowances + last_year = last_projected_year(parameters) + for kind, relation in RELATIONS.items(): + guarantee = pc.guarantee_credit.minimum_guarantee.children[relation] + maximum = pc.savings_credit.maximum.children[relation] + + def derive(sample, previous): + return guarantee(sample) + maximum(sample) + + extend_after_last_stated(allowances.children[kind].aged, last_year, derive) + allowances.clear_parent_cache() + return parameters diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml index ec9e5906ab..303d7054eb 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged.yaml @@ -16,15 +16,15 @@ metadata: period: week unit: currency-GBP label: Housing Benefit lone parent pension-age personal allowance - # Not uprated by September CPI: the pension-age personal allowances rose 4.75% - # in April 2026 (£244.40 to £256.00 single), when September CPI was 3.8%, and - # 2.3% in April 2017, when it was 1.0% (Benefit and pension rates 2026 to - # 2027, 2017 to 2018). Keeps the lagged calendar-year CPI projection it had - # through gov.benefit_uprating_cpi until its own basis is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Not uprated as a whole: the allowance is the Pension Credit standard + # minimum guarantee plus the maximum Savings Credit (HB A3/2021), which + # rise by earnings and September CPI. create_pension_age_allowances.py + # derives the years after the published amounts as that sum. reference: - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 (SI 2006/214), Schedule 3, paragraph 1(1)(b) href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3 + - title: DWP Housing Benefit circular A3/2021, paragraphs 3-5 (the allowance is the standard minimum guarantee plus the maximum Savings Credit) + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2021/a32021-housing-benefit-person-who-have-attained-the-qualifying-age-for-state-pension-credit-amendment-regulations-2021 - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations (Northern Ireland) 2006 (SR 2006/406), Schedule 4, paragraph 1 (the same amounts) href: https://www.legislation.gov.uk/nisr/2006/406/schedule/4 - title: Benefit and pension rates 2024 to 2025 - Policy paper diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged_from_cutoff.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged_from_cutoff.yaml index f8e9cc9d6d..efc5481fac 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged_from_cutoff.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/lone_parent/aged_from_cutoff.yaml @@ -10,13 +10,10 @@ metadata: period: week unit: currency-GBP label: Housing Benefit lone parent personal allowance, State Pension age reached from April 2021 - # Not uprated by September CPI: the post-April 2021 pension-age personal - # allowances equal the Pension Credit standard minimum guarantee in every year - # from 2021-22 to 2026-27, which is reviewed against earnings under Social - # Security Administration Act 1992 s150A(1)(d). Keeps the lagged calendar-year - # CPI projection it had through gov.benefit_uprating_cpi until its own basis - # is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Equal to the Pension Credit standard minimum guarantee in 2021-2026; + # project at its earnings floor (SSAA 1992 s150A(2)). The HB allowance is + # set in Schedule 3 below. + uprating: gov.economic_assumptions.indices.earnings_uprating reference: - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) (Amendment) Regulations 2021 (SI 2021/188), regulation 2(3), inserting paragraph 1(1)(c) of Schedule 3 to SI 2006/214 from 1 April 2021 href: https://www.legislation.gov.uk/uksi/2021/188/regulation/2/made diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml index 5392ebe49a..5849592a72 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged.yaml @@ -15,15 +15,15 @@ metadata: period: week unit: currency-GBP label: Housing Benefit single pension-age personal allowance - # Not uprated by September CPI: the pension-age personal allowances rose 4.75% - # in April 2026 (£244.40 to £256.00 single), when September CPI was 3.8%, and - # 2.3% in April 2017, when it was 1.0% (Benefit and pension rates 2026 to - # 2027, 2017 to 2018). Keeps the lagged calendar-year CPI projection it had - # through gov.benefit_uprating_cpi until its own basis is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Not uprated as a whole: the allowance is the Pension Credit standard + # minimum guarantee plus the maximum Savings Credit (HB A3/2021), which + # rise by earnings and September CPI. create_pension_age_allowances.py + # derives the years after the published amounts as that sum. reference: - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 (SI 2006/214), Schedule 3, paragraph 1(1)(b) href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3 + - title: DWP Housing Benefit circular A3/2021, paragraphs 3-5 (the allowance is the standard minimum guarantee plus the maximum Savings Credit) + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2021/a32021-housing-benefit-person-who-have-attained-the-qualifying-age-for-state-pension-credit-amendment-regulations-2021 - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations (Northern Ireland) 2006 (SR 2006/406), Schedule 4, paragraph 1 (the same amounts) href: https://www.legislation.gov.uk/nisr/2006/406/schedule/4 - title: Benefit and pension rates 2024 to 2025 - Policy paper diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged_from_cutoff.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged_from_cutoff.yaml index a996b8e945..2cc161210f 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged_from_cutoff.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/single/aged_from_cutoff.yaml @@ -10,13 +10,10 @@ metadata: period: week unit: currency-GBP label: Housing Benefit single personal allowance, State Pension age reached from April 2021 - # Not uprated by September CPI: the post-April 2021 pension-age personal - # allowances equal the Pension Credit standard minimum guarantee in every year - # from 2021-22 to 2026-27, which is reviewed against earnings under Social - # Security Administration Act 1992 s150A(1)(d). Keeps the lagged calendar-year - # CPI projection it had through gov.benefit_uprating_cpi until its own basis - # is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Equal to the Pension Credit standard minimum guarantee in 2021-2026; + # project at its earnings floor (SSAA 1992 s150A(2)). The HB allowance is + # set in Schedule 3 below. + uprating: gov.economic_assumptions.indices.earnings_uprating reference: - title: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) (Amendment) Regulations 2021 (SI 2021/188), regulation 2(3), inserting paragraph 1(1)(c) of Schedule 3 to SI 2006/214 from 1 April 2021 href: https://www.legislation.gov.uk/uksi/2021/188/regulation/2/made diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/README.md b/policyengine_uk/parameters/gov/dwp/pension_credit/README.md index 04369faedd..385e422a94 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/README.md +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/README.md @@ -1 +1,26 @@ # Pension Credit + +After the last published rates, the standard minimum guarantee rises each +April with the previous May-July total-pay average weekly earnings increase, +using the same statutory input and published precision as the State Pension +triple lock. This models the earnings minimum in +[SSAA 1992 s150A(2)](https://www.legislation.gov.uk/ukpga/1992/5/section/150A), +with no decrease when earnings fall. There is no CPI or 2.5% floor in this +projection. The three lower Housing Benefit pension-age personal +allowances, which equal the guarantee, follow the same increase. + +The Savings Credit maximum rises with September CPI, following +[HCWS1101](https://questions-statements.parliament.uk/written-statements/detail/2025-11-26/hcws1101). +Projected thresholds are derived after uprating: +`threshold = standard minimum guarantee - maximum / phase_in`. +At the 60% phase-in rate, this ensures that the formula's maximum award +follows prices even when the guarantee follows earnings. Published +thresholds are retained, including their rounding. A zero phase-in rate pays +no Savings Credit at any threshold, so that year keeps the threshold in force +before it. + +The three higher Housing Benefit pension-age personal allowances, for those +who reached State Pension age before April 2021, are the guarantee plus the +maximum Savings Credit (HB A3/2021). After their published amounts they are +derived as that sum, so the guarantee part follows earnings and the maximum +follows September CPI. diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml index d4bdb1e6ee..9eef083e79 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml @@ -14,12 +14,9 @@ SINGLE: 2025-04-01: 227.1 2026-04-01: 238 metadata: - # Not uprated by September CPI: the standard minimum guarantee is reviewed - # against earnings under Social Security Administration Act 1992 s150A(1)(d) - # (4.8% in April 2026, written statement HCWS1101). Keeps the lagged - # calendar-year CPI projection it had through gov.benefit_uprating_cpi until - # its own basis is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Project at the earnings floor in Social Security Administration Act 1992 + # s150A(2), using the same May-July AWE input as the State Pension review. + uprating: gov.economic_assumptions.indices.earnings_uprating COUPLE: values: 2015-04-06: 230.85 @@ -35,18 +32,17 @@ COUPLE: 2025-04-01: 346.6 2026-04-01: 363.25 metadata: - # Not uprated by September CPI: the standard minimum guarantee is reviewed - # against earnings under Social Security Administration Act 1992 s150A(1)(d) - # (4.8% in April 2026, written statement HCWS1101). Keeps the lagged - # calendar-year CPI projection it had through gov.benefit_uprating_cpi until - # its own basis is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Project at the earnings floor in Social Security Administration Act 1992 + # s150A(2), using the same May-July AWE input as the State Pension review. + uprating: gov.economic_assumptions.indices.earnings_uprating metadata: period: week label: Pension Credit minimum guarantee unit: currency-GBP propagate_metadata_to_children: true reference: + - title: Social Security Administration Act 1992, section 150A(2) + href: https://www.legislation.gov.uk/ukpga/1992/5/section/150A - title: The State Pension Credit Regulations 2002 reg. 6 href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/6/2021-04-12 - title: Benefit and pension rates 2026 to 2027 diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/create_savings_credit_threshold.py b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/create_savings_credit_threshold.py new file mode 100644 index 0000000000..854b38458d --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/create_savings_credit_threshold.py @@ -0,0 +1,69 @@ +"""Project Savings Credit thresholds from the guarantee and maximum award. + +The standard minimum guarantee rises with earnings, while the Savings Credit +maximum rises with September CPI (HCWS1101). Under the 60% phase-in rule, +maximum = phase_in * (standard minimum guarantee - threshold), so the +threshold is the guarantee less maximum / phase_in. Keep the published +thresholds, including their rounding, and derive only subsequent years. + +A zero phase-in rate pays no Savings Credit at any threshold, and the +maximum fixes none, so a year with a zero rate keeps the threshold in force +before it. + +Run after general uprating and before fiscal-year conversion, on both the +main tree and its baseline copy. Reprocessing a reform then reads its changed +guarantee, maximum and phase-in rate. +""" + +from typing import Any, Callable + +from policyengine_core.parameters import Parameter, ParameterNode +from policyengine_core.parameters.parameter_at_instant import ParameterAtInstant + + +def last_projected_year(parameters: ParameterNode) -> int: + """The last year of the earnings index, which the projections run to.""" + index = parameters.gov.economic_assumptions.indices.earnings_uprating + return max(int(entry.instant_str[:4]) for entry in index.values_list) + + +def extend_after_last_stated( + parameter: Parameter, + last_year: int, + derive: Callable[[str, Any], Any], +) -> None: + """Add a value for each year after the parameter's last stated value. + + ``derive(sample, previous)`` gives the year's value from the other + parameters at its 30 April ``sample``; ``previous`` is the value in force + before it. Each value applies from 1 January, as uprating's do. + """ + last = max(parameter.values_list, key=lambda entry: entry.instant_str) + previous = last.value + for year in range(int(last.instant_str[:4]) + 1, last_year + 1): + previous = derive(f"{year}-04-30", previous) + parameter.values_list.append( + ParameterAtInstant(parameter.name, f"{year}-01-01", data=previous) + ) + parameter.values_list.sort(key=lambda entry: entry.instant_str, reverse=True) + parameter.mark_as_modified() + + +def project_savings_credit_threshold(parameters: ParameterNode) -> ParameterNode: + """Fill the years after each threshold's last stated value.""" + pc = parameters.gov.dwp.pension_credit + sc = pc.savings_credit + last_year = last_projected_year(parameters) + for relation, threshold in sc.threshold.children.items(): + guarantee = pc.guarantee_credit.minimum_guarantee.children[relation] + maximum = sc.maximum.children[relation] + + def derive(sample, previous): + phase_in = sc.rate.phase_in(sample) + if phase_in == 0: + return previous + return guarantee(sample) - maximum(sample) / phase_in + + extend_after_last_stated(threshold, last_year, derive) + sc.threshold.clear_parent_cache() + return parameters diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/maximum.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/maximum.yaml new file mode 100644 index 0000000000..52b6028c9b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/maximum.yaml @@ -0,0 +1,18 @@ +description: Maximum weekly Savings Credit, used to derive projected income thresholds. +SINGLE: + values: + 2026-04-01: 17.96 +COUPLE: + values: + 2026-04-01: 20.10 +metadata: + unit: currency-GBP + period: week + label: Pension Credit Savings Credit maximum + uprating: gov.benefit_uprating_cpi + propagate_metadata_to_children: true + reference: + - title: Benefit and pension rates 2026 to 2027 (DWP, gov.uk), Pension Credit + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2026-to-2027/proposed-benefit-and-pension-rates-2026-to-2027#pension-credit + - title: Statutory Review of State Pension and Benefit Rates 2026/27, HCWS1101 (Savings Credit maximum increases by September CPI) + href: https://questions-statements.parliament.uk/written-statements/detail/2025-11-26/hcws1101 diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml index e2d831518e..95a66f4196 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/savings_credit/threshold.yaml @@ -34,11 +34,9 @@ COUPLE: metadata: unit: currency-GBP period: week - # Not uprated by September CPI: the threshold rose 4.9% in April 2026, when - # September CPI was 3.8% (Benefit and pension rates 2026 to 2027). Keeps the - # lagged calendar-year CPI projection it had through gov.benefit_uprating_cpi - # until its own basis is modelled. - uprating: gov.economic_assumptions.indices.obr.lagged_cpi + # Projected after uprating by create_savings_credit_threshold.py: + # standard minimum guarantee - Savings Credit maximum / phase-in rate. + # Published thresholds keep their own rounding; only projections are derived. propagate_metadata_to_children: true reference: - title: The State Pension Credit Regulations 2002 reg. 7 diff --git a/policyengine_uk/parameters/gov/economic_assumptions/README.md b/policyengine_uk/parameters/gov/economic_assumptions/README.md index 753c5ee99c..f658709b0e 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/README.md +++ b/policyengine_uk/parameters/gov/economic_assumptions/README.md @@ -212,20 +212,55 @@ Order Explanatory Memorandum since April 2011; 3.8% for April 2026, written statement HCWS1101). `gov.benefit_uprating_cpi`, which the benefit rate parameters name as their `uprating`, follows `indices.september_cpi_uprating`, and so do the Universal Credit standard allowance and health element indices -built from it. Amounts not raised by September CPI in practice that sat on -`gov.benefit_uprating_cpi` keep the lagged calendar-year CPI projection they -had, through `indices.obr.lagged_cpi`. These include the earnings-linked -Pension Credit standard minimum guarantee, savings credit threshold, Housing -Benefit pension-age allowances and Housing Benefit non-dependant deduction -income bands, as well as DfE funding rates and the council tax high value -surcharge. Lagged calendar-year CPI remains a placeholder pending an -appropriate earnings-linked model for the first group; deduction amounts -continue to follow September CPI. The savings credit threshold and Housing +built from it. Housing Benefit non-dependant deduction income bands, DfE +funding rates and the council tax high value surcharge retain their lagged +calendar-year CPI projection through `indices.obr.lagged_cpi`, pending their +own uprating models. The deduction amounts continue to follow September CPI. +The savings credit threshold and Housing Benefit pension-age allowances are within the s150 review, under [s150(1)(l) and (h)](https://www.legislation.gov.uk/ukpga/1992/5/section/150), even though their increases in practice are linked to earnings rather than September CPI. +## Pension Credit earnings uprating + +[`create_earnings_uprating.py`](./create_earnings_uprating.py) builds +`yoy_growth.earnings_uprating` from the previous May-July total-pay AWE input +used by the triple lock, rounded to the published 0.1 percentage points. +`indices.earnings_uprating` compounds it without rounding the index levels, +so rounding cannot push the increase below earnings. When earnings fall, +the projection holds amounts unchanged. + +The Pension Credit standard minimum guarantee and the three lower Housing +Benefit pension-age personal allowances, which equal it, use this index after +their last published amounts. [SSAA 1992 s150A(1)(d) and (2)](https://www.legislation.gov.uk/ukpga/1992/5/section/150A) +require the guarantee to rise at least with earnings; they impose no CPI or +2.5% floor. This projection uses that earnings minimum, allowing published +amounts to reflect discretionary increases. For April 2027, May-July 2026 +earnings growth of 3.9% gives £238 × 1.039 = £247.282 a week for a single +person and £363.25 × 1.039 = £377.41675 for a couple, before benefit rounding. + +The Savings Credit maximum rises by September CPI, as stated in +[HCWS1101](https://questions-statements.parliament.uk/written-statements/detail/2025-11-26/hcws1101). +After general uprating, the projected threshold is derived as the standard +minimum guarantee minus the maximum divided by the phase-in rate (60%). +Published thresholds keep their own rounding. The 2026-27 maxima are £17.96 +and £20.10, from the [DWP rates table](https://www.gov.uk/government/publications/benefit-and-pension-rates-2026-to-2027/proposed-benefit-and-pension-rates-2026-to-2027#pension-credit). +A zero phase-in rate pays no Savings Credit, so that year keeps the threshold +in force before it. + +The three higher Housing Benefit pension-age personal allowances, for those +who reached State Pension age before April 2021, are the standard minimum +guarantee plus the maximum Savings Credit +([HB A3/2021](https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2021/a32021-housing-benefit-person-who-have-attained-the-qualifying-age-for-state-pension-credit-amendment-regulations-2021), +paragraphs 3-5); each published amount from 2019-20 to 2026-27 is that sum +rounded up to the next 5p. +[`../dwp/housing_benefit/allowances/create_pension_age_allowances.py`](../dwp/housing_benefit/allowances/create_pension_age_allowances.py) +derives the years after the published amounts as that sum, after general +uprating, so each part keeps its own index. For April 2027 that is +£247.282 + £17.96 × 1.035 = £265.8706 a week for a single person or lone +parent and £377.41675 + £20.10 × 1.035 = £398.22025 for a couple. + Income Tax Act 2007 ss21 and 57 index the basic rate limit and the personal allowance by this measure once no freeze applies. [`../hmrc/create_threshold_indexation.py`](../hmrc/create_threshold_indexation.py) diff --git a/policyengine_uk/parameters/gov/economic_assumptions/create_earnings_uprating.py b/policyengine_uk/parameters/gov/economic_assumptions/create_earnings_uprating.py new file mode 100644 index 0000000000..bfb7db0ef3 --- /dev/null +++ b/policyengine_uk/parameters/gov/economic_assumptions/create_earnings_uprating.py @@ -0,0 +1,94 @@ +"""Build April earnings uprating from the previous May-July AWE growth. + +Social Security Administration Act 1992 s150A(2) requires the Pension Credit +standard minimum guarantee to rise at least as much as earnings. This +projection holds amounts unchanged when earnings do not rise. Unlike the +State Pension triple lock, that minimum has no CPI or 2.5% component. + +``gov.economic_assumptions.yoy_growth.earnings_uprating`` holds each April's +rise, keyed to 1 January as the other growth series are. It reads the same +completed ``statutory_uprating_inputs.awe_total_pay_may_july`` series as the +triple lock and rounds to the 0.1 percentage points published by the ONS. + +``create_economic_assumption_indices`` compounds it into +``gov.economic_assumptions.indices.earnings_uprating``. The series starts +from a 2010 base and runs to one year past the statutory inputs. Published +benefit amounts remain explicit; the index projects later years. +""" + +from typing import Dict, Iterable + +from policyengine_core.parameters import Parameter, ParameterNode + +from policyengine_uk.parameters.gov.economic_assumptions.create_statutory_uprating_inputs import ( + AWE_OBSERVATION_MONTH_DAY, + last_input_year, + round_to_published_precision, +) + +# The index starts at 1 in this year; its value holds no rise. +BASE_YEAR = 2010 +FIRST_UPRATING_YEAR = BASE_YEAR + 1 + + +def earnings_uprating_rate(earnings_growth: float) -> float: + """April rise at the published earnings rate, without a cut.""" + rate = round_to_published_precision(earnings_growth) + return rate if rate > 0 else 0.0 + + +def uprating_years(parameters: ParameterNode) -> Iterable[int]: + """Aprils covered: from April 2011 to one year past the statutory inputs.""" + return range(FIRST_UPRATING_YEAR, last_input_year(parameters) + 2) + + +def earnings_uprating_rates(parameters: ParameterNode) -> Dict[int, float]: + """The rise taking effect in April of each year, in year order.""" + earnings = parameters.gov.economic_assumptions.statutory_uprating_inputs.awe_total_pay_may_july + rates = {} + for year in uprating_years(parameters): + may_july = earnings(f"{year - 1}-{AWE_OBSERVATION_MONTH_DAY}") + if may_july is None: + raise ValueError( + f"Missing May-July {year - 1} earnings for the April {year} uprating." + ) + rates[year] = earnings_uprating_rate(may_july) + return rates + + +def add_earnings_uprating(parameters: ParameterNode) -> ParameterNode: + """Add ``gov.economic_assumptions.yoy_growth.earnings_uprating``.""" + values = {f"{BASE_YEAR}-01-01": None} + values.update( + { + f"{year}-01-01": rate + for year, rate in earnings_uprating_rates(parameters).items() + } + ) + new_parameter = Parameter( + "gov.economic_assumptions.yoy_growth.earnings_uprating", + data={ + "description": ( + "Uprating taking effect in April of each year: May-July " + "earnings growth of the previous year, and no rise when " + "earnings fall." + ), + "values": values, + "metadata": { + "unit": "/1", + "label": "Earnings uprating rate", + "reference": [ + { + "title": ( + "Social Security Administration Act 1992, section 150A(2)" + ), + "href": "https://www.legislation.gov.uk/ukpga/1992/5/section/150A", + } + ], + }, + }, + ) + parameters.gov.economic_assumptions.yoy_growth.add_child( + "earnings_uprating", new_parameter + ) + return parameters diff --git a/policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py b/policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py index 8eb8e80761..90ecde8187 100644 --- a/policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py +++ b/policyengine_uk/parameters/gov/economic_assumptions/create_economic_assumption_indices.py @@ -42,10 +42,11 @@ def create_economic_assumption_indices( for year in range(start_year + 1, 2040): yoy_growth = descendant(year) - indices_value = round( - values[year - 1] * (1 + yoy_growth), - 5, - ) + indices_value = values[year - 1] * (1 + yoy_growth) + # Rounding an earnings index down can put the projected + # guarantee below its statutory annual earnings floor. + if full_name != "gov.economic_assumptions.yoy_growth.earnings_uprating": + indices_value = round(indices_value, 5) values[year] = indices_value mirror_parameter = Parameter( diff --git a/policyengine_uk/tax_benefit_system.py b/policyengine_uk/tax_benefit_system.py index 1c5565ebb5..f1fd874a8f 100644 --- a/policyengine_uk/tax_benefit_system.py +++ b/policyengine_uk/tax_benefit_system.py @@ -30,6 +30,12 @@ from policyengine_uk.parameters.gov.dwp.state_pension.triple_lock.create_triple_lock import ( add_triple_lock, ) +from policyengine_uk.parameters.gov.dwp.housing_benefit.allowances.create_pension_age_allowances import ( + project_pension_age_allowances, +) +from policyengine_uk.parameters.gov.dwp.pension_credit.savings_credit.create_savings_credit_threshold import ( + project_savings_credit_threshold, +) from policyengine_uk.parameters.gov.dwp.universal_credit.rebalancing.create_health_element_uprating import ( add_uc_health_element_uprating, ) @@ -39,6 +45,9 @@ from policyengine_uk.parameters.gov.economic_assumptions.create_september_cpi_uprating import ( add_september_cpi_uprating, ) +from policyengine_uk.parameters.gov.economic_assumptions.create_earnings_uprating import ( + add_earnings_uprating, +) from policyengine_uk.parameters.gov.economic_assumptions.create_economic_assumption_indices import ( create_economic_assumption_indices, ) @@ -122,6 +131,7 @@ def _install_parameters(system, parameters: ParameterNode) -> None: add_lagged_earnings, add_lagged_cpi, add_statutory_uprating_inputs, + add_earnings_uprating, add_triple_lock, add_september_cpi_uprating, create_economic_assumption_indices, @@ -171,7 +181,7 @@ def process_parameter_tree( - lagged earnings and CPI indices - statutory uprating inputs (September CPI, May-July earnings) - triple lock calculations for state pensions - - September CPI uprating + - earnings and September CPI uprating - economic assumption indices - income tax and NICs threshold indexation after the freeze - Universal Credit standard allowance uprating (UC Act 2025 s. 1) @@ -210,6 +220,11 @@ def process_parameter_tree( propagate_parameter_metadata(parameters) with canonical_ni_uprating_references(parameters): uprate_parameters(parameters) + project_savings_credit_threshold(parameters) + project_pension_age_allowances(parameters) + if annualise: + project_savings_credit_threshold(parameters.baseline) + project_pension_age_allowances(parameters.baseline) parameters = backdate_parameters(parameters, BACKDATED_FROM) # Prior law's projected path for every parameter a reform can change: the # gov tree, before annualisation, and the trees outside it (household), diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_earnings_uprating.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_earnings_uprating.yaml new file mode 100644 index 0000000000..6df50a6dd2 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/pension_age_earnings_uprating.yaml @@ -0,0 +1,113 @@ +# April 2027 projections, before any ministerial rounding. The lower +# allowances equal the standard minimum guarantee, which rises by May-July +# 2026 earnings growth of 3.9%. The higher allowances are the guarantee plus +# the maximum Savings Credit (HB A3/2021), which rises by September 2026 CPI +# of 3.5%. +# Lower single/lone-parent: 238 * 1.039 * 52 = 12,858.664. +# Lower couple: 363.25 * 1.039 * 52 = 19,625.671. +# Higher single/lone-parent: (238 * 1.039 + 17.96 * 1.035) * 52 +# = (247.282 + 18.5886) * 52 = 265.8706 * 52 = 13,825.2712. +# Higher couple: (363.25 * 1.039 + 20.10 * 1.035) * 52 +# = (377.41675 + 20.8035) * 52 = 398.22025 * 52 = 20,707.453. +# Regulation-set/cohort inputs isolate uprating from age-attainment logic. +- name: April 2027 lower single pension-age HB allowance follows earnings + period: 2027 + absolute_error_margin: 0.01 + input: + age: 70 + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + housing_benefit_attained_pension_age_before_cutoff: false + benefits_premiums: 0 + output: + housing_benefit_applicable_amount: 12_858.664 + +- name: April 2027 higher single pension-age HB allowance is the guarantee plus the Savings Credit maximum + period: 2027 + absolute_error_margin: 0.01 + input: + age: 80 + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + housing_benefit_attained_pension_age_before_cutoff: true + benefits_premiums: 0 + output: + housing_benefit_applicable_amount: 13_825.2712 + +- name: April 2027 lower couple pension-age HB allowance follows earnings + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + partner: + age: 68 + benunits: + benunit: + members: [claimant, partner] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + housing_benefit_attained_pension_age_before_cutoff: false + benefits_premiums: 0 + output: + housing_benefit_applicable_amount: 19_625.671 + +- name: April 2027 higher couple pension-age HB allowance is the guarantee plus the Savings Credit maximum + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + partner: + age: 70 + benunits: + benunit: + members: [claimant, partner] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + housing_benefit_attained_pension_age_before_cutoff: true + benefits_premiums: 0 + output: + housing_benefit_applicable_amount: 20_707.453 + +- name: April 2027 lower lone-parent pension-age HB allowance follows earnings + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + child: + age: 12 + benunits: + benunit: + members: [claimant, child] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + housing_benefit_attained_pension_age_before_cutoff: false + benefits_premiums: 0 + output: + is_lone_parent: true + housing_benefit_applicable_amount: 12_858.664 + +- name: April 2027 higher lone-parent pension-age HB allowance is the guarantee plus the Savings Credit maximum + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + child: + age: 12 + benunits: + benunit: + members: [claimant, child] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + housing_benefit_attained_pension_age_before_cutoff: true + benefits_premiums: 0 + output: + is_lone_parent: true + housing_benefit_applicable_amount: 13_825.2712 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/standard_minimum_guarantee.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/standard_minimum_guarantee.yaml index 03ec484e62..841ba5b6bc 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/standard_minimum_guarantee.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/guarantee_credit/standard_minimum_guarantee.yaml @@ -25,3 +25,30 @@ members: [person1, person2] output: standard_minimum_guarantee: 18_889 + +# SSAA 1992 s150A(2): May-July 2026 AWE growth of 3.9% applies in +# April 2027. These are projections before any ministerial 5p rounding: +# single 238 * 1.039 = 247.282 weekly; couple 363.25 * 1.039 = 377.41675. +- name: April 2027 single minimum guarantee rises by earnings + period: 2027 + absolute_error_margin: 0.01 + input: + age: 81 + relation_type: SINGLE + output: + standard_minimum_guarantee: 12_858.664 # 247.282 * 52 + +- name: April 2027 couple minimum guarantee rises by earnings + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + partner: + age: 79 + benunits: + benunit: + members: [claimant, partner] + output: + standard_minimum_guarantee: 19_625.671 # 377.41675 * 52 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/savings_credit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/savings_credit.yaml index cab1abeb8d..4c39f288ca 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/savings_credit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/savings_credit.yaml @@ -65,3 +65,70 @@ guarantee_credit: 123 pension_credit_entitlement: 1_094.40 pension_credit: 1_094.40 + +# April 2027 hand controls (unrounded projections): +# SMG single 238 * 1.039 = 247.282; couple 363.25 * 1.039 = 377.41675. +# Maximum single 17.96 * 1.035 = 18.5886; couple 20.10 * 1.035 = 20.8035. +# Threshold = SMG - maximum / 0.6: single 216.301; couple 342.74425. +# Historical published thresholds remain in use through April 2026. +- name: April 2027 single savings credit maximum rises by September CPI + period: 2027 + absolute_error_margin: 0.01 + input: + age: 81 + is_savings_credit_eligible: true + relation_type: SINGLE + savings_credit_income: 20_000 + pension_credit_income: 12_858.664 + output: + standard_minimum_guarantee: 12_858.664 + savings_credit: 966.6072 # 18.5886 * 52 + +- name: April 2027 couple savings credit maximum rises by September CPI + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + partner: + age: 79 + benunits: + benunit: + members: [claimant, partner] + is_savings_credit_eligible: true + savings_credit_income: 25_000 + pension_credit_income: 19_625.671 + output: + standard_minimum_guarantee: 19_625.671 + savings_credit: 1_081.782 # 20.8035 * 52 + +- name: April 2027 single savings credit phases in above the derived threshold + period: 2027 + absolute_error_margin: 0.01 + input: + age: 81 + is_savings_credit_eligible: true + relation_type: SINGLE + savings_credit_income: 12_000 + pension_credit_income: 12_000 + output: + savings_credit: 451.4088 # 0.6 * (12_000 - 216.301 * 52) + +- name: April 2027 couple savings credit phases in above the derived threshold + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 81 + partner: + age: 79 + benunits: + benunit: + members: [claimant, partner] + is_savings_credit_eligible: true + savings_credit_income: 19_000 + pension_credit_income: 19_000 + output: + savings_credit: 706.3794 # 0.6 * (19_000 - 342.74425 * 52) diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_age_personal_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_age_personal_allowance.yaml index 2679c0baa8..9c1c7c0f4e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_age_personal_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_pension_age_personal_allowance.yaml @@ -189,3 +189,48 @@ output: council_tax_reduction_attained_pension_age_before_cutoff: false council_tax_reduction_applicable_amount: 238 * 52 + +# April 2027 projections of the one Scottish pension-age rate, the higher +# Housing Benefit allowance: the standard minimum guarantee, uprated by +# May-July 2026 earnings growth of 3.9%, plus the maximum Savings Credit, +# uprated by September 2026 CPI of 3.5% (HB A3/2021). +- name: In Scotland, a single person's April 2027 pension-age rate is the projected guarantee plus the Savings Credit maximum + period: 2027 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 72 + is_male: false + benunits: + benunit: + members: [claimant] + benefits_premiums: 0 + households: + household: + members: [claimant] + region: SCOTLAND + output: + council_tax_reduction_applicable_amount: 13_825.2712 # (238 * 1.039 + 17.96 * 1.035) * 52 + +- name: In Scotland, a couple's April 2027 pension-age rate is the projected guarantee plus the Savings Credit maximum + period: 2027 + absolute_error_margin: 0.01 + input: + people: + older: + age: 71 + is_male: true + younger: + age: 69 + is_male: false + benunits: + benunit: + members: [older, younger] + benefits_premiums: 0 + households: + household: + members: [older, younger] + region: SCOTLAND + output: + council_tax_reduction_applicable_amount: 20_707.453 # (363.25 * 1.039 + 20.10 * 1.035) * 52 diff --git a/policyengine_uk/tests/test_pension_credit_uprating.py b/policyengine_uk/tests/test_pension_credit_uprating.py new file mode 100644 index 0000000000..bea73fd5bd --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_uprating.py @@ -0,0 +1,458 @@ +"""Pension Credit's statutory earnings floor and linked projected amounts. + +SSAA 1992 s150A(2) requires the standard minimum guarantee's annual increase +to be at least earnings growth. Projections use the published May-July AWE +figure, with no cut when earnings fall. The lower pension-age HB allowances +equal the guarantee and follow the same increase. The Savings Credit maximum +follows September CPI, with the threshold implied by the maximum and the +60% phase-in rate. The higher pension-age HB allowances are the guarantee +plus that maximum (HB A3/2021), so each part keeps its own index. + +Invariants, for every earnings, CPI and phase-in path: + +1. The guarantee and lower HB allowances rise by exactly the published + earnings figure, and never fall. +2. Each higher HB allowance equals the projected guarantee plus the + projected Savings Credit maximum, after its published amounts. +3. A positive phase-in rate times the guarantee less the threshold is the + maximum; a zero rate keeps the threshold in force before it. +4. Published amounts are unchanged by the projections. +""" + +from decimal import ROUND_HALF_UP, Decimal +from pathlib import Path + +import pytest +import yaml +from hypothesis import example, given, settings +from hypothesis import strategies as st +from policyengine_core.parameters import ParameterNode, load_parameter_file +from policyengine_core.parameters.operations.propagate_parameter_metadata import ( + propagate_parameter_metadata, +) +from policyengine_core.parameters.operations.uprate_parameters import uprate_parameters + +from policyengine_uk import Simulation +from policyengine_uk.parameters.gov.dwp.housing_benefit.allowances.create_pension_age_allowances import ( + project_pension_age_allowances, +) +from policyengine_uk.parameters.gov.dwp.pension_credit.savings_credit.create_savings_credit_threshold import ( + project_savings_credit_threshold, +) +from policyengine_uk.parameters.gov.economic_assumptions.create_earnings_uprating import ( + add_earnings_uprating, +) +from policyengine_uk.parameters.gov.economic_assumptions.create_economic_assumption_indices import ( + create_economic_assumption_indices, +) +from policyengine_uk.parameters.gov.economic_assumptions.create_september_cpi_uprating import ( + add_september_cpi_uprating, +) +from policyengine_uk.system import system +from policyengine_uk.tax_benefit_system import COUNTRY_DIR + +EARNINGS_INDEX = "gov.economic_assumptions.indices.earnings_uprating" +FIRST_PROJECTION_YEAR = 2027 +LAST_INDEX_YEAR = 2039 +LAST_PUBLISHED_YEAR = FIRST_PROJECTION_YEAR - 1 +PARAMETERS_DIR = COUNTRY_DIR / "parameters" +SMG_NAME = "gov.dwp.pension_credit.guarantee_credit.minimum_guarantee" +SC_NAME = "gov.dwp.pension_credit.savings_credit" +HB_NAME = "gov.dwp.housing_benefit.allowances" +RAW_SMG = load_parameter_file( + str( + PARAMETERS_DIR + / "gov/dwp/pension_credit/guarantee_credit/minimum_guarantee.yaml" + ), + name=SMG_NAME, +) +RAW_SC = load_parameter_file( + str(PARAMETERS_DIR / "gov/dwp/pension_credit/savings_credit"), name=SC_NAME +) +RAW_HB = load_parameter_file( + str(PARAMETERS_DIR / "gov/dwp/housing_benefit/allowances"), name=HB_NAME +) +RAW_BENEFIT_CPI = load_parameter_file( + str(PARAMETERS_DIR / "gov/benefit_uprating_cpi.yaml"), + name="gov.benefit_uprating_cpi", +) +# Each higher allowance's Pension Credit relation: a lone parent's is single. +HB_RELATIONS = {"single": "SINGLE", "couple": "COUPLE", "lone_parent": "SINGLE"} +PUBLISHED = yaml.safe_load( + ( + Path(__file__).parent / "fixtures" / "dwp_benefit_and_pension_rates.yaml" + ).read_text() +)["parameters"] + +# Includes negative earnings, off-grid forecasts and different CPI paths. +growth = st.integers(min_value=-600, max_value=1500).map(lambda n: n / 10_000) +paths = st.lists(st.tuples(growth, growth), min_size=1, max_size=10) +# Zero, the 60% in force and rates either side of it, for each projected year. +phase_in_rates = st.one_of(st.just(0.0), st.integers(1, 100).map(lambda n: n / 100)) +phase_in_paths = st.lists(phase_in_rates, min_size=1, max_size=10) + + +def published_rise(rate): + """Independent transcription of a published 0.1pp figure, never a cut.""" + rounded = Decimal(str(rate)).quantize(Decimal("0.001"), ROUND_HALF_UP) + return max(float(rounded), 0.0) + + +def add_node(parent, key): + """Add an empty node; Core reads an empty dict as a parameter instead.""" + node = ParameterNode(name=f"{parent.name}.{key}", data={}) + parent.add_child(key, node) + return node + + +def synthetic_parameters(path, phase_ins=()): + """Uprate and project the production Pension Credit and HB amounts. + + No simulation is built. Earlier observations merely supply the indices' + 2010 base; every generated path begins with the May-July and September + figures for the first projected April. ``phase_ins`` gives the Savings + Credit phase-in rate from each projected April, in order. + """ + observations = {year: (0.02, 0.02) for year in range(2010, 2026)} + observations.update({2026 + i: values for i, values in enumerate(path)}) + parameters = ParameterNode( + data={ + "gov": { + "economic_assumptions": { + "statutory_uprating_inputs": { + "awe_total_pay_may_july": { + "values": { + f"{year}-07-01": earnings + for year, (earnings, _) in observations.items() + } + }, + "cpi_september": { + "values": { + f"{year}-09-01": cpi + for year, (_, cpi) in observations.items() + } + }, + }, + "yoy_growth": { + "obr": { + name: { + "values": { + f"{year}-01-01": values[position] + for year, values in observations.items() + } + } + for name, position in ( + ("average_earnings", 0), + ("consumer_price_index", 1), + ) + } + }, + }, + } + } + ) + parameters.gov.add_child("benefit_uprating_cpi", RAW_BENEFIT_CPI.clone()) + dwp = add_node(parameters.gov, "dwp") + pension_credit = add_node(dwp, "pension_credit") + guarantee_credit = add_node(pension_credit, "guarantee_credit") + guarantee_credit.add_child("minimum_guarantee", RAW_SMG.clone()) + savings_credit = RAW_SC.clone() + phase_in = savings_credit.rate.phase_in + for i, rate in enumerate(phase_ins): + phase_in.update(start=f"{FIRST_PROJECTION_YEAR + i}-01-01", value=rate) + pension_credit.add_child("savings_credit", savings_credit) + # Only pension-age amounts belong to the earnings link. Including the + # working-age leaves would need the unrelated CPI index too. + allowances = add_node(add_node(dwp, "housing_benefit"), "allowances") + for kind in HB_RELATIONS: + node = add_node(allowances, kind) + for cohort in ("aged", "aged_from_cutoff"): + node.add_child(cohort, getattr(getattr(RAW_HB, kind), cohort).clone()) + parameters = add_september_cpi_uprating(add_earnings_uprating(parameters)) + parameters = create_economic_assumption_indices(parameters) + propagate_parameter_metadata(parameters) + uprate_parameters(parameters) + project_savings_credit_threshold(parameters) + project_pension_age_allowances(parameters) + return parameters + + +def earnings_linked_amounts(parameters): + minimum = parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee + yield minimum.SINGLE + yield minimum.COUPLE + allowances = parameters.gov.dwp.housing_benefit.allowances + for kind in HB_RELATIONS: + yield getattr(allowances, kind).aged_from_cutoff + + +def higher_allowances(parameters): + """Each higher HB allowance with the guarantee and maximum it is built from.""" + pc = parameters.gov.dwp.pension_credit + allowances = parameters.gov.dwp.housing_benefit.allowances + for kind, relation in HB_RELATIONS.items(): + yield ( + getattr(allowances, kind).aged, + getattr(pc.guarantee_credit.minimum_guarantee, relation), + getattr(pc.savings_credit.maximum, relation), + ) + + +def assert_published_amounts_kept(parameters): + """Invariant 4: the YAML's stated values, read where they are stated.""" + for raw, node in ( + (RAW_SMG, parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee), + (RAW_SC.threshold, parameters.gov.dwp.pension_credit.savings_credit.threshold), + (RAW_SC.maximum, parameters.gov.dwp.pension_credit.savings_credit.maximum), + ): + for relation in ("SINGLE", "COUPLE"): + for entry in getattr(raw, relation).values_list: + assert getattr(node, relation)(entry.instant_str) == entry.value + allowances = parameters.gov.dwp.housing_benefit.allowances + for kind in HB_RELATIONS: + for cohort in ("aged", "aged_from_cutoff"): + projected = getattr(getattr(allowances, kind), cohort) + for entry in getattr(getattr(RAW_HB, kind), cohort).values_list: + assert projected(entry.instant_str) == entry.value + + +@settings(max_examples=60, deadline=None) +@given(path=paths) +@example(path=[(0.01, 0.10), (-0.02, 0.05), (0.08, -0.01)]) +def test_projected_smg_and_hb_never_rise_by_less_than_published_earnings(path): + """Invariant 1, on amounts, not just the rate helper, under diverging inputs. + + CPI cannot replace the earnings floor or add the State Pension's price + protection. Falling earnings hold these amounts rather than cutting them. + """ + parameters = synthetic_parameters(path) + for parameter in earnings_linked_amounts(parameters): + assert parameter.metadata["uprating"] == EARNINGS_INDEX + for i, (earnings, _) in enumerate(path): + year = FIRST_PROJECTION_YEAR + i + before = parameter(f"{year - 1}-04-30") + after = parameter(f"{year}-04-30") + floor = published_rise(earnings) + assert after >= before * (1 + floor) - 1e-10, (parameter.name, year) + assert after / before - 1 == pytest.approx(floor, abs=1e-12) + + +@settings(max_examples=60, deadline=None) +@given(path=paths) +@example(path=[(0.039, 0.035)]) +@example(path=[(0.10, -0.02), (-0.03, 0.12), (0.0, 0.0)]) +def test_higher_hb_allowances_keep_each_part_on_its_own_index(path): + """Invariants 2 and 4: earnings move the guarantee part, CPI the maximum. + + Projecting the whole allowance by either index would misstate it once + earnings and CPI diverge. + """ + parameters = synthetic_parameters(path) + assert_published_amounts_kept(parameters) + for allowance, guarantee, maximum in higher_allowances(parameters): + assert "uprating" not in allowance.metadata + for i, (earnings, cpi) in enumerate(path): + year = FIRST_PROJECTION_YEAR + i + at, before = f"{year}-04-30", f"{year - 1}-04-30" + assert allowance(at) == pytest.approx( + guarantee(at) + maximum(at), abs=1e-9 + ), (allowance.name, year) + assert guarantee(at) / guarantee(before) - 1 == pytest.approx( + published_rise(earnings), abs=1e-12 + ) + # CPI indices keep five-decimal-place levels. + assert maximum(at) / maximum(before) - 1 == pytest.approx( + published_rise(cpi), abs=2e-5 + ) + + +@settings(max_examples=60, deadline=None) +@given(path=paths, phase_ins=phase_in_paths) +@example(path=[(0.039, 0.035)], phase_ins=[0.0]) +@example(path=[(0.039, 0.035), (0.02, 0.03), (0.05, 0.01)], phase_ins=[0.0, 0.6, 0.0]) +def test_projected_threshold_keeps_the_maximum_at_every_phase_in_rate(path, phase_ins): + """Invariant 3: a zero rate never divides, and keeps the threshold.""" + parameters = synthetic_parameters(path, phase_ins) + assert_published_amounts_kept(parameters) + pc = parameters.gov.dwp.pension_credit + phase_in = pc.savings_credit.rate.phase_in + for relation in ("SINGLE", "COUPLE"): + guarantee = getattr(pc.guarantee_credit.minimum_guarantee, relation) + threshold = getattr(pc.savings_credit.threshold, relation) + maximum = getattr(pc.savings_credit.maximum, relation) + for year in range(FIRST_PROJECTION_YEAR, LAST_INDEX_YEAR + 1): + at, before = f"{year}-04-30", f"{year - 1}-04-30" + rate = phase_in(at) + if rate == 0: + assert threshold(at) == threshold(before), (relation, year) + else: + assert rate * (guarantee(at) - threshold(at)) == pytest.approx( + maximum(at), abs=1e-9 + ), (relation, year, rate) + + +def test_baseline_smg_and_hb_respect_the_earnings_floor_in_every_projected_year(): + parameters = system.parameters + awe = parameters.gov.economic_assumptions.statutory_uprating_inputs.awe_total_pay_may_july + for parameter in earnings_linked_amounts(parameters): + for year in range(FIRST_PROJECTION_YEAR, LAST_INDEX_YEAR + 1): + before = parameter(f"{year - 1}-04-30") + after = parameter(f"{year}-04-30") + floor = published_rise(awe(f"{year - 1}-07-01")) + assert after >= before * (1 + floor) - 1e-10, (parameter.name, year) + assert after / before - 1 == pytest.approx(floor, abs=1e-12) + + +@pytest.mark.parametrize("relation", ["SINGLE", "COUPLE"]) +def test_savings_credit_threshold_preserves_the_cpi_uprated_maximum(relation): + parameters = system.parameters + pc = parameters.gov.dwp.pension_credit + minimum = getattr(pc.guarantee_credit.minimum_guarantee, relation) + threshold = getattr(pc.savings_credit.threshold, relation) + maximum = getattr(pc.savings_credit.maximum, relation) + phase_in = pc.savings_credit.rate.phase_in + cpi = parameters.gov.economic_assumptions.yoy_growth.september_cpi_uprating + for year in range(FIRST_PROJECTION_YEAR, LAST_INDEX_YEAR + 1): + at = f"{year}-04-30" + before = f"{year - 1}-04-30" + assert phase_in(at) * (minimum(at) - threshold(at)) == pytest.approx( + maximum(at), abs=1e-10 + ) + # CPI indices retain their existing five-decimal-place rounding. + assert maximum(at) / maximum(before) - 1 == pytest.approx( + cpi(f"{year}-01-01"), abs=2e-5 + ) + + +def test_hb_lower_pension_age_allowances_stay_equal_to_the_standard_minimum_guarantee(): + parameters = system.parameters.gov.dwp + minimum = parameters.pension_credit.guarantee_credit.minimum_guarantee + allowances = parameters.housing_benefit.allowances + for year in range(2021, LAST_INDEX_YEAR + 1): + at = f"{year}-04-30" + assert allowances.single.aged_from_cutoff(at) == minimum.SINGLE(at) + assert allowances.lone_parent.aged_from_cutoff(at) == minimum.SINGLE(at) + assert allowances.couple.aged_from_cutoff(at) == minimum.COUPLE(at) + + +@pytest.mark.parametrize("year", range(2019, LAST_PUBLISHED_YEAR + 1)) +@pytest.mark.parametrize("kind", sorted(HB_RELATIONS)) +def test_published_higher_hb_allowances_are_the_guarantee_plus_the_savings_credit_maximum( + kind, year +): + """The premise of invariant 2, in DWP's published rates. + + The maximum is 60% of the guarantee less the threshold (State Pension + Credit Regulations 2002 reg 7). Each published allowance is the + guarantee plus that maximum, rounded up to the next 5p. + """ + relation = HB_RELATIONS[kind] + + def published(name): + return Decimal(str(PUBLISHED[name]["values"][year])) + + guarantee = published(f"{SMG_NAME}.{relation}") + threshold = published(f"{SC_NAME}.threshold.{relation}") + allowance = published(f"{HB_NAME}.{kind}.aged") + maximum = Decimal("0.6") * (guarantee - threshold) + assert Decimal(0) <= allowance - (guarantee + maximum) < Decimal("0.05") + + +def test_baseline_higher_hb_allowances_are_the_projected_guarantee_plus_maximum(): + """Invariants 2 and 4 in current law, against the indices directly. + + The differential side compounds each part's published amount by its own + index, without reading the uprated guarantee or maximum. + """ + parameters = system.parameters + assert_published_amounts_kept(parameters) + earnings = parameters.gov.economic_assumptions.indices.earnings_uprating + benefit_cpi = parameters.gov.benefit_uprating_cpi + last = f"{LAST_PUBLISHED_YEAR}-04-30" + for allowance, guarantee, maximum in higher_allowances(parameters): + for year in range(FIRST_PROJECTION_YEAR, LAST_INDEX_YEAR + 1): + at = f"{year}-04-30" + assert allowance(at) == pytest.approx( + guarantee(at) + maximum(at), abs=1e-9 + ), (allowance.name, year) + from_indices = guarantee(last) * earnings(at) / earnings(last) + maximum( + last + ) * benefit_cpi(at) / benefit_cpi(last) + assert allowance(at) == pytest.approx(from_indices, abs=1e-9) + + +def zero_phase_in_situation(): + """A single pensioner with income between the threshold and the guarantee.""" + years = ("2027", "2028") + return { + "people": {"claimant": {"age": {"2027": 81, "2028": 82}}}, + "benunits": { + "benunit": { + "members": ["claimant"], + "savings_credit_income": {year: 12_000 for year in years}, + "pension_credit_income": {year: 12_000 for year in years}, + } + }, + "households": {"household": {"members": ["claimant"]}}, + } + + +def test_zero_phase_in_reform_builds_and_pays_no_savings_credit(): + """A zero Savings Credit phase-in rate for 2027-28 once divided by zero. + + It now keeps the 2026-27 threshold for that year, pays no Savings Credit, + and leaves 2028-29 as current law has it. + """ + reform = {"gov.dwp.pension_credit.savings_credit.rate.phase_in": {"2027": 0}} + baseline = Simulation(situation=zero_phase_in_situation()) + reformed = Simulation(situation=zero_phase_in_situation(), reform=reform) + # 0.6 * (12,000 - 216.301 * 52), as savings_credit.yaml computes. + assert baseline.calculate("savings_credit", 2027)[0] == pytest.approx( + 451.4088, abs=0.01 + ) + assert reformed.calculate("savings_credit", 2027)[0] == 0 + assert reformed.calculate("savings_credit", 2028)[0] == pytest.approx( + baseline.calculate("savings_credit", 2028)[0], abs=1e-6 + ) + assert reformed.calculate("guarantee_credit", 2027)[0] == pytest.approx( + baseline.calculate("guarantee_credit", 2027)[0], abs=1e-6 + ) + threshold = reformed.tax_benefit_system.parameters.gov.dwp.pension_credit.savings_credit.threshold.SINGLE + current = baseline.tax_benefit_system.parameters.gov.dwp.pension_credit.savings_credit.threshold.SINGLE + assert threshold("2027-04-30") == pytest.approx(208.07) + assert threshold("2028-04-30") == pytest.approx(current("2028-04-30")) + + +def test_reforms_keep_their_own_allowance_and_move_the_derived_one(): + """A dated change to a higher allowance is kept, and prior law resumes. + + A change to the guarantee moves the allowance derived from it in that + year, as it moves the Savings Credit threshold; the lower allowance has + its own earnings projection and does not follow. + """ + reform = { + f"{HB_NAME}.single.aged": {"2028": 300}, + f"{SMG_NAME}.COUPLE": {"2029": 400}, + } + situation = { + "people": {"claimant": {"age": {"2028": 81}}}, + "households": {"household": {"members": ["claimant"]}}, + } + current = system.parameters + reformed = Simulation(situation=situation, reform=reform).tax_benefit_system + allowances = reformed.parameters.gov.dwp.housing_benefit.allowances + now = current.gov.dwp.housing_benefit.allowances + sc_max = reformed.parameters.gov.dwp.pension_credit.savings_credit.maximum + for year in (2027, 2029, 2030): + at = f"{year}-04-30" + assert allowances.single.aged(at) == pytest.approx(now.single.aged(at)) + assert allowances.single.aged("2028-04-30") == 300 + assert allowances.couple.aged("2029-04-30") == pytest.approx( + 400 + sc_max.COUPLE("2029-04-30") + ) + for year in (2028, 2030): + at = f"{year}-04-30" + assert allowances.couple.aged(at) == pytest.approx(now.couple.aged(at)) + assert allowances.couple.aged_from_cutoff("2029-04-30") == pytest.approx( + now.couple.aged_from_cutoff("2029-04-30") + ) diff --git a/policyengine_uk/tests/test_september_cpi_uprating.py b/policyengine_uk/tests/test_september_cpi_uprating.py index f6d4413164..28815cca8b 100644 --- a/policyengine_uk/tests/test_september_cpi_uprating.py +++ b/policyengine_uk/tests/test_september_cpi_uprating.py @@ -239,9 +239,10 @@ def test_hb_non_dependant_income_bands_keep_lagged_cpi_while_deductions_use_sept def test_amounts_not_raised_by_september_cpi_keep_their_lagged_calendar_projection(): """Amounts that were on gov.benefit_uprating_cpi but are not raised by - September CPI (the Pension Credit standard minimum guarantee, the Housing - Benefit pension-age allowances, DfE funding rates and others) still rise - by the previous calendar year's CPI after their last value.""" + September CPI, including DfE funding rates and HB non-dependant income + bands, still rise by the previous calendar year's CPI after their last + value. Pension Credit amounts and pension-age HB allowances have their + own projections.""" parameters = system.parameters raw = load_parameter_file(str(COUNTRY_DIR / "parameters"), name="") calendar_cpi = ( @@ -263,8 +264,12 @@ def test_amounts_not_raised_by_september_cpi_keep_their_lagged_calendar_projecti assert after / before - 1 == pytest.approx( calendar_cpi(f"{year - 1}-01-01"), abs=2e-5 ), (parameter.name, year) - assert "gov.dwp.pension_credit.guarantee_credit.minimum_guarantee.SINGLE" in names - assert "gov.dwp.housing_benefit.allowances.single.aged" in names + assert names + assert any(name.startswith("gov.dfe.childcare_funding_rate") for name in names) + assert not any(name.startswith("gov.dwp.pension_credit.") for name in names) + assert not any( + name.startswith("gov.dwp.housing_benefit.allowances.") for name in names + ) def test_the_triple_lock_reads_the_same_september_cpi():