diff --git a/changelog.d/2072.fixed.md b/changelog.d/2072.fixed.md new file mode 100644 index 0000000000..a93ededf80 --- /dev/null +++ b/changelog.d/2072.fixed.md @@ -0,0 +1 @@ +- Corrects the Pension Credit standard minimum guarantee projection to follow the statutory May-July earnings input with a non-negative floor. diff --git a/changelog.d/2083.fixed.md b/changelog.d/2083.fixed.md new file mode 100644 index 0000000000..19f381e40c --- /dev/null +++ b/changelog.d/2083.fixed.md @@ -0,0 +1 @@ +- Corrected the 2025-26 Pension Credit rates and derived future Savings Credit thresholds from the earnings-projected guarantee and CPI-projected maximum without replacing explicit or reformed threshold values. diff --git a/changelog.d/2154.fixed.md b/changelog.d/2154.fixed.md new file mode 100644 index 0000000000..acdf000718 --- /dev/null +++ b/changelog.d/2154.fixed.md @@ -0,0 +1 @@ +- Correct the 2025-26 rates and forecast basis for protected single, lone-parent, and couple pension-age Housing Benefit personal allowances. diff --git a/docs/book/programs/gov/dwp/state-pension.md b/docs/book/programs/gov/dwp/state-pension.md index 985576b98a..6caf7eaaba 100644 --- a/docs/book/programs/gov/dwp/state-pension.md +++ b/docs/book/programs/gov/dwp/state-pension.md @@ -210,6 +210,15 @@ statutory-basis forecast minus the calendar-year growth stored in OBR does not forecast the ONS AWE series; its measure is wages and salaries per employee, and Q2 is the quarter nearest May to July. +The State Pension triple lock consumes both statutory inputs. The shared +`indices.statutory_earnings_floor` series, which floors May-July earnings at +zero, also projects the Pension Credit standard minimum guarantee. The model +derives the Savings Credit threshold so that this earnings-projected guarantee +and a non-negative September CPI projection of maximum Savings Credit preserve +the statutory 60% relationship. Protected pension-age Housing Benefit uses +the same earnings and CPI components. These are forecast conventions after +the last enacted rates, not announced future benefit amounts. + After the EFO horizon the gap is zero, so the inputs follow calendar-year growth. Smooth forecasts pay the higher of earnings, CPI and 2.5% each year, so the baseline has none of the extra cost the triple lock builds up when 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 a32bf5a7b0..8358a4dacb 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 @@ -1,4 +1,7 @@ -description: Personal allowance for Housing Benefit for a couple where one or both members attained State Pension age before 1 April 2021 (and, before that date, every couple with a member over State Pension age). +description: >- + Personal allowance for Housing Benefit for a couple where one or both members attained State Pension age before 1 April 2021 (and, before that date, every couple with a member over State Pension age). + Amounts after the last enacted rate use a component-weighted forecast and + are not enacted future rates. values: # 2020-21 to 2026-27: DWP Benefit and pension rates (gov.uk), each # year's rate as listed in that year's publication. @@ -8,19 +11,32 @@ values: 2022-04-06: 294.9 2023-04-01: 324.7 2024-04-01: 352 - 2025-04-01: 366 + 2025-04-01: + value: 366 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025, Schedule 6 paragraph 1(2)(b) + href: https://www.legislation.gov.uk/uksi/2025/295/schedule/6/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025, Schedule 6 paragraph 1(2)(b) + href: https://www.legislation.gov.uk/nisr/2025/64/schedule/6/made + - title: Benefit and pension rates 2025 to 2026, Housing Benefit personal allowances + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 383.35 metadata: period: week unit: currency-GBP label: Housing Benefit couple pension-age personal allowance - uprating: gov.benefit_uprating_cpi + uprating: gov.dwp.housing_benefit.allowances.protected_pension_age_uprating.couple 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: 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: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 3 paragraph 1 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/1 + - title: The Social Security Benefits Up-rating Order 2026 article 24(6) and Schedule 6 + href: https://www.legislation.gov.uk/uksi/2026/148/schedule/6/made - title: Benefit and pension rates 2024 to 2025 - Policy paper href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2024-to-2025/benefit-and-pension-rates-2024-to-2025 - title: Benefit and pension rates 2020 to 2021 (DWP, gov.uk) @@ -33,3 +49,5 @@ metadata: href: https://www.legislation.gov.uk/uksi/2025/295/contents/made - title: The Social Security Benefits Up-rating Order 2026 (SI 2026/148) href: https://www.legislation.gov.uk/uksi/2026/148/contents/made + - title: A14/2025 Housing Benefit uprating for the financial year ending March 2027, paragraphs 5, 6 and 20 + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2025/a142025-housing-benefit-uprating-for-the-financial-year-ending-march-2027 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/create_protected_pension_age_uprating.py b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/create_protected_pension_age_uprating.py new file mode 100644 index 0000000000..e8450b5e52 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/allowances/create_protected_pension_age_uprating.py @@ -0,0 +1,185 @@ +"""Project protected pension-age Housing Benefit allowances by component. + +The protected allowances consist of the corresponding Pension Credit +standard minimum guarantee plus a retained Savings Credit uplift. The +guarantee component follows the shared statutory earnings index; the retained +uplift follows non-negative September CPI. Projecting either whole allowance +with one series would therefore misstate its relationship to Pension Credit. +""" + +from policyengine_core.parameters import Parameter, ParameterNode + +from policyengine_uk.parameters.gov.economic_assumptions.create_statutory_uprating_inputs import ( + CPI_OBSERVATION_MONTH_DAY, + last_input_year, +) + + +BASE_YEAR = 2026 +BASE_INSTANT = f"{BASE_YEAR}-04-01" + + +def project_retained_uplift(amount: float, cpi_growth: float) -> float: + """Project the retained Savings Credit uplift without a cash reduction.""" + return amount * (1 + max(cpi_growth, 0)) + + +def protected_allowance_index_values( + base_allowance: float, + guarantee_component: float, + earnings_index: Parameter, + cpi_september: Parameter, + final_observation_year: int, +) -> dict[str, float]: + """Build a component-weighted index from the last enacted allowance.""" + retained_uplift = base_allowance - guarantee_component + if retained_uplift < 0: + raise ValueError( + "The protected Housing Benefit allowance cannot be lower than " + "the corresponding Pension Credit guarantee." + ) + + base_earnings_index = float(earnings_index(BASE_INSTANT)) + values = {BASE_INSTANT: 1.0} + for year in range(BASE_YEAR + 1, final_observation_year + 2): + observation_year = year - 1 + guarantee = guarantee_component * ( + float(earnings_index(f"{year}-04-01")) / base_earnings_index + ) + retained_uplift = project_retained_uplift( + retained_uplift, + float(cpi_september(f"{observation_year}-{CPI_OBSERVATION_MONTH_DAY}")), + ) + values[f"{year}-04-01"] = (guarantee + retained_uplift) / base_allowance + return values + + +def _projection_parameter( + relationship: str, + base_allowance: float, + guarantee_component: float, + earnings_index: Parameter, + cpi_september: Parameter, + final_observation_year: int, +) -> Parameter: + retained_uplift = base_allowance - guarantee_component + relationship_label = ( + "single and lone-parent" if relationship == "single" else "couple" + ) + return Parameter( + ( + "gov.dwp.housing_benefit.allowances." + f"protected_pension_age_uprating.{relationship}" + ), + data={ + "description": ( + f"Forecast index for the protected {relationship_label} " + "pension-age Housing Benefit allowance. The enacted 2026-27 " + f"base comprises a £{guarantee_component:g} Pension Credit " + f"guarantee and a £{retained_uplift:g} retained Savings " + "Credit uplift. The components follow the shared statutory " + "earnings index and non-negative September CPI, respectively. " + "Projected values are not enacted rates." + ), + "values": protected_allowance_index_values( + base_allowance, + guarantee_component, + earnings_index, + cpi_september, + final_observation_year, + ), + "metadata": { + "unit": "/1", + "label": ( + f"Protected {relationship_label} pension-age Housing " + "Benefit allowance forecast index" + ), + "reference": [ + { + "title": ( + "Social Security Administration Act 1992 section 150" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/5/section/150" + ), + }, + { + "title": ( + "Social Security Administration Act 1992 section 150A" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/5/section/150A" + ), + }, + { + "title": ( + "A14/2025 Housing Benefit uprating for the " + "financial year ending March 2027, paragraphs " + "5, 6 and 20" + ), + "href": ( + "https://www.gov.uk/government/publications/" + "housing-benefit-adjudication-circulars-2025/" + "a142025-housing-benefit-uprating-for-the-" + "financial-year-ending-march-2027" + ), + }, + { + "title": ( + "ONS KAC3 average weekly earnings, whole economy, total pay" + ), + "href": ( + "https://www.ons.gov.uk/" + "employmentandlabourmarket/peopleinwork/" + "earningsandworkinghours/timeseries/kac3/lms" + ), + }, + { + "title": "ONS D7G7 Consumer Prices Index annual rate", + "href": ( + "https://www.ons.gov.uk/economy/" + "inflationandpriceindices/timeseries/d7g7/mm23" + ), + }, + ], + }, + }, + ) + + +def add_protected_pension_age_uprating( + parameters: ParameterNode, +) -> ParameterNode: + """Add relationship-specific protected-allowance forecast indices.""" + allowances = parameters.gov.dwp.housing_benefit.allowances + guarantees = parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee + earnings_index = ( + parameters.gov.economic_assumptions.indices.statutory_earnings_floor + ) + cpi_september = ( + parameters.gov.economic_assumptions.statutory_uprating_inputs.cpi_september + ) + projection = ParameterNode( + name=("gov.dwp.housing_benefit.allowances.protected_pension_age_uprating"), + data={}, + ) + allowances.add_child("protected_pension_age_uprating", projection) + + relationship_parameters = { + "single": (allowances.single.aged, guarantees.SINGLE), + "couple": (allowances.couple.aged, guarantees.COUPLE), + } + final_observation_year = last_input_year(parameters) + for relationship, (allowance, guarantee) in relationship_parameters.items(): + projection.add_child( + relationship, + _projection_parameter( + relationship, + float(allowance(BASE_INSTANT)), + float(guarantee(BASE_INSTANT)), + earnings_index, + cpi_september, + final_observation_year, + ), + ) + 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 4f1a89f61f..8d980cc06f 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 @@ -1,4 +1,7 @@ -description: Personal allowance for Housing Benefit for a lone parent who attained State Pension age before 1 April 2021 (and, before that date, every pension-age lone parent). +description: >- + Personal allowance for Housing Benefit for a lone parent who attained State Pension age before 1 April 2021 (and, before that date, every pension-age lone parent). + Amounts after the last enacted rate use a component-weighted forecast and + are not enacted future rates. values: # 2020-21 to 2026-27: DWP Benefit and pension rates (gov.uk), each @@ -9,19 +12,32 @@ values: 2022-04-06: 197.1 2023-04-01: 217 2024-04-01: 235.2 - 2025-04-01: 244.4 + 2025-04-01: + value: 244.4 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025, Schedule 6 paragraph 1(1)(b) + href: https://www.legislation.gov.uk/uksi/2025/295/schedule/6/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025, Schedule 6 paragraph 1(1)(b) + href: https://www.legislation.gov.uk/nisr/2025/64/schedule/6/made + - title: Benefit and pension rates 2025 to 2026, Housing Benefit personal allowances + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 256 metadata: period: week unit: currency-GBP label: Housing Benefit lone parent pension-age personal allowance - uprating: gov.benefit_uprating_cpi + uprating: gov.dwp.housing_benefit.allowances.protected_pension_age_uprating.single 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: 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: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 3 paragraph 1 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/1 + - title: The Social Security Benefits Up-rating Order 2026 article 24(6) and Schedule 6 + href: https://www.legislation.gov.uk/uksi/2026/148/schedule/6/made - title: Benefit and pension rates 2024 to 2025 - Policy paper href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2024-to-2025/benefit-and-pension-rates-2024-to-2025 - title: Benefit and pension rates 2020 to 2021 (DWP, gov.uk) @@ -34,3 +50,5 @@ metadata: href: https://www.legislation.gov.uk/uksi/2025/295/contents/made - title: The Social Security Benefits Up-rating Order 2026 (SI 2026/148) href: https://www.legislation.gov.uk/uksi/2026/148/contents/made + - title: A14/2025 Housing Benefit uprating for the financial year ending March 2027, paragraphs 5 and 20 + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2025/a142025-housing-benefit-uprating-for-the-financial-year-ending-march-2027 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 4fdbd68a0a..729e314b03 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 @@ -1,4 +1,7 @@ -description: Personal allowance for Housing Benefit for a single claimant who attained State Pension age before 1 April 2021 (and, before that date, every single pension-age claimant). +description: >- + Personal allowance for Housing Benefit for a single claimant who attained State Pension age before 1 April 2021 (and, before that date, every single pension-age claimant). + Amounts after the last enacted rate use a component-weighted forecast and + are not enacted future rates. values: # 2020-21 to 2026-27: DWP Benefit and pension rates (gov.uk), each # year's rate as listed in that year's publication. @@ -8,19 +11,32 @@ values: 2022-04-06: 197.1 2023-04-01: 217 2024-04-01: 235.2 - 2025-04-01: 244.4 + 2025-04-01: + value: 244.4 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025, Schedule 6 paragraph 1(1)(b) + href: https://www.legislation.gov.uk/uksi/2025/295/schedule/6/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025, Schedule 6 paragraph 1(1)(b) + href: https://www.legislation.gov.uk/nisr/2025/64/schedule/6/made + - title: Benefit and pension rates 2025 to 2026, Housing Benefit personal allowances + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 256 metadata: period: week unit: currency-GBP label: Housing Benefit single pension-age personal allowance - uprating: gov.benefit_uprating_cpi + uprating: gov.dwp.housing_benefit.allowances.protected_pension_age_uprating.single 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: 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: The Housing Benefit (Persons who have attained the qualifying age for state pension credit) Regulations 2006 Schedule 3 paragraph 1 + href: https://www.legislation.gov.uk/uksi/2006/214/schedule/3/paragraph/1 + - title: The Social Security Benefits Up-rating Order 2026 article 24(6) and Schedule 6 + href: https://www.legislation.gov.uk/uksi/2026/148/schedule/6/made - title: Benefit and pension rates 2024 to 2025 - Policy paper href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2024-to-2025/benefit-and-pension-rates-2024-to-2025 - title: Benefit and pension rates 2020 to 2021 (DWP, gov.uk) @@ -33,3 +49,5 @@ metadata: href: https://www.legislation.gov.uk/uksi/2025/295/contents/made - title: The Social Security Benefits Up-rating Order 2026 (SI 2026/148) href: https://www.legislation.gov.uk/uksi/2026/148/contents/made + - title: A14/2025 Housing Benefit uprating for the financial year ending March 2027, paragraphs 5 and 20 + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2025/a142025-housing-benefit-uprating-for-the-financial-year-ending-march-2027 diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/create_savings_credit_threshold_projection.py b/policyengine_uk/parameters/gov/dwp/pension_credit/create_savings_credit_threshold_projection.py new file mode 100644 index 0000000000..b8f102eb36 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/create_savings_credit_threshold_projection.py @@ -0,0 +1,240 @@ +"""Build Savings Credit threshold indices from the statutory uprating bases. + +The maximum Savings Credit is the phase-in rate multiplied by the difference +between the standard minimum guarantee and the Savings Credit threshold. DWP +uprates the guarantee with earnings and the maximum with CPI. The threshold is +therefore a derived residual; applying CPI to it directly, or freezing it, +does not preserve the statutory relationship. Each generated index is applied +by the standard parameter-uprating operation, so explicit threshold values and +reforms remain authoritative anchors. +""" + +from decimal import ROUND_HALF_UP, Decimal + +from policyengine_core.parameters import Parameter, ParameterNode + +from policyengine_uk.parameters.gov.economic_assumptions.create_statutory_uprating_inputs import ( + CPI_OBSERVATION_MONTH_DAY, + last_input_year, +) + + +BASE_YEAR = 2026 +BASE_INSTANT = f"{BASE_YEAR}-04-01" +CURRENCY_PRECISION = Decimal("0.01") + + +def projection_instant(year: int) -> str: + """Return the date sampled by fiscal-year parameter conversion.""" + return f"{year}-04-30" + + +def round_currency(amount: float) -> float: + """Round a published weekly amount to the nearest penny, halves up.""" + return float( + Decimal(repr(float(amount))).quantize( + CURRENCY_PRECISION, + ROUND_HALF_UP, + ) + ) + + +def project_maximum_savings_credit(amount: float, cpi_growth: float) -> float: + """Project maximum Savings Credit without reducing its cash amount.""" + return amount * (1 + max(cpi_growth, 0)) + + +def savings_credit_threshold( + guarantee: float, + maximum_savings_credit: float, + phase_in_rate: float, +) -> float: + """Solve the statutory maximum-Savings-Credit formula for its threshold.""" + if phase_in_rate <= 0: + raise ValueError("The Savings Credit phase-in rate must be positive.") + return guarantee - maximum_savings_credit / phase_in_rate + + +def threshold_index_values( + base_guarantee: float, + base_threshold: float, + phase_in_rate: Parameter, + earnings_index: Parameter, + cpi_september: Parameter, + final_observation_year: int, +) -> dict[str, float]: + """Build a forecast index from the last enacted threshold.""" + base_phase_in_rate = float(phase_in_rate(projection_instant(BASE_YEAR))) + maximum = round_currency(base_phase_in_rate * (base_guarantee - base_threshold)) + base_earnings_index = float(earnings_index(BASE_INSTANT)) + values = {BASE_INSTANT: 1.0} + + for year in range(BASE_YEAR + 1, final_observation_year + 2): + observation_year = year - 1 + maximum = project_maximum_savings_credit( + maximum, + float(cpi_september(f"{observation_year}-{CPI_OBSERVATION_MONTH_DAY}")), + ) + guarantee = base_guarantee * ( + float(earnings_index(f"{year}-04-01")) / base_earnings_index + ) + threshold = savings_credit_threshold( + guarantee, + maximum, + float(phase_in_rate(projection_instant(year))), + ) + values[f"{year}-04-01"] = threshold / base_threshold + + return values + + +def _projection_parameter( + relationship: str, + base_guarantee: float, + base_threshold: float, + phase_in_rate: Parameter, + earnings_index: Parameter, + cpi_september: Parameter, + final_observation_year: int, +) -> Parameter: + relationship_label = relationship.lower() + return Parameter( + (f"gov.dwp.pension_credit.savings_credit.threshold_uprating.{relationship}"), + data={ + "description": ( + f"Forecast index for the {relationship_label} Savings Credit " + "threshold. It derives the threshold from an earnings-projected " + "standard minimum guarantee and a maximum Savings Credit amount " + "projected by non-negative September CPI. Explicit threshold " + "values remain authoritative. Projected values are not enacted " + "rates." + ), + "values": threshold_index_values( + base_guarantee, + base_threshold, + phase_in_rate, + earnings_index, + cpi_september, + final_observation_year, + ), + "metadata": { + "unit": "/1", + "label": ( + f"Pension Credit Savings Credit {relationship_label} " + "threshold forecast index" + ), + "reference": [ + { + "title": "State Pension Credit Act 2002 section 3", + "href": ( + "https://www.legislation.gov.uk/ukpga/2002/16/section/3" + ), + }, + { + "title": ( + "Social Security Administration Act 1992 section 150" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/5/section/150" + ), + }, + { + "title": ( + "Social Security Administration Act 1992 section 150A" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/5/section/150A" + ), + }, + { + "title": ( + "Social Security Administration (Northern Ireland) " + "Act 1992 section 132" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/8/section/132" + ), + }, + { + "title": ( + "Social Security Administration (Northern Ireland) " + "Act 1992 section 132A" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/8/section/132A" + ), + }, + { + "title": ( + "A14/2025 Housing Benefit uprating for the financial " + "year ending March 2027" + ), + "href": ( + "https://www.gov.uk/government/publications/" + "housing-benefit-adjudication-circulars-2025/" + "a142025-housing-benefit-uprating-for-the-financial-" + "year-ending-march-2027" + ), + }, + { + "title": ( + "ONS KAC3 average weekly earnings, whole economy, total pay" + ), + "href": ( + "https://www.ons.gov.uk/employmentandlabourmarket/" + "peopleinwork/earningsandworkinghours/timeseries/" + "kac3/lms" + ), + }, + { + "title": "ONS D7G7 Consumer Prices Index annual rate", + "href": ( + "https://www.ons.gov.uk/economy/" + "inflationandpriceindices/timeseries/d7g7/mm23" + ), + }, + ], + }, + }, + ) + + +def add_savings_credit_threshold_projection( + parameters: ParameterNode, +) -> ParameterNode: + """Add forecast indices for each Savings Credit threshold.""" + savings_credit = parameters.gov.dwp.pension_credit.savings_credit + guarantees = parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee + earnings_index = ( + parameters.gov.economic_assumptions.indices.statutory_earnings_floor + ) + cpi_september = ( + parameters.gov.economic_assumptions.statutory_uprating_inputs.cpi_september + ) + projection = ParameterNode( + name=("gov.dwp.pension_credit.savings_credit.threshold_uprating"), + data={}, + ) + savings_credit.add_child("threshold_uprating", projection) + relationship_parameters = { + "SINGLE": (guarantees.SINGLE, savings_credit.threshold.SINGLE), + "COUPLE": (guarantees.COUPLE, savings_credit.threshold.COUPLE), + } + final_observation_year = last_input_year(parameters) + for relationship, ( + guarantee_parameter, + threshold_parameter, + ) in relationship_parameters.items(): + projection.add_child( + relationship, + _projection_parameter( + relationship, + float(guarantee_parameter(projection_instant(BASE_YEAR))), + float(threshold_parameter(projection_instant(BASE_YEAR))), + savings_credit.rate.phase_in, + earnings_index, + cpi_september, + final_observation_year, + ), + ) + return parameters 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 a1dfb5f57b..aa8636a178 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 @@ -1,4 +1,8 @@ -description: Minimum income guarantee (basic amount) for Pension Credit. +description: >- + Minimum income guarantee (basic amount) for Pension Credit. Values after the + last enacted rate use the non-negative May-July average weekly earnings input + from the annual statutory review as a forecast floor; they are not enacted + future rates. SINGLE: values: 2015-04-06: 151.2 @@ -11,10 +15,19 @@ SINGLE: 2022-04-01: 182.6 2023-04-01: 201.05 2024-04-01: 218.15 - 2025-04-01: 227.1 + 2025-04-01: + value: 227.1 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025 art. 29 + href: https://www.legislation.gov.uk/uksi/2025/295/article/29/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025 art. 29 + href: https://www.legislation.gov.uk/nisr/2025/64/article/29/made + - title: Benefit and pension rates 2025 to 2026 + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 238 metadata: - uprating: gov.benefit_uprating_cpi + uprating: gov.economic_assumptions.indices.statutory_earnings_floor COUPLE: values: 2015-04-06: 230.85 @@ -27,10 +40,19 @@ COUPLE: 2022-04-01: 278.7 2023-04-01: 306.85 2024-04-01: 332.95 - 2025-04-01: 346.6 + 2025-04-01: + value: 346.6 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025 art. 29 + href: https://www.legislation.gov.uk/uksi/2025/295/article/29/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025 art. 29 + href: https://www.legislation.gov.uk/nisr/2025/64/article/29/made + - title: Benefit and pension rates 2025 to 2026 + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 363.25 metadata: - uprating: gov.benefit_uprating_cpi + uprating: gov.economic_assumptions.indices.statutory_earnings_floor metadata: period: week label: Pension Credit minimum guarantee @@ -47,3 +69,9 @@ metadata: href: https://www.legislation.gov.uk/uksi/2025/295/contents/made - title: The Social Security Benefits Up-rating Order 2026 (SI 2026/148) href: https://www.legislation.gov.uk/uksi/2026/148/contents/made + - title: Social Security Administration Act 1992 section 150A + href: https://www.legislation.gov.uk/ukpga/1992/5/section/150A + - title: Social Security Administration (Northern Ireland) Act 1992 section 132A + href: https://www.legislation.gov.uk/ukpga/1992/8/section/132A + - title: A14/2025 Housing Benefit uprating for the financial year ending March 2027 + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2025/a142025-housing-benefit-uprating-for-the-financial-year-ending-march-2027 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 568efeac00..2babfeffd8 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 @@ -1,4 +1,8 @@ -description: Income threshold at which Savings Credit is eligible. +description: >- + Income threshold at which Savings Credit is eligible. Values after the last + enacted rate are derived so that the earnings-projected standard minimum + guarantee and the CPI-projected maximum Savings Credit remain consistent + with the statutory 60% formula; they are not enacted future rates. SINGLE: values: 2015-04-06: 126.5 @@ -11,10 +15,20 @@ SINGLE: 2022-04-01: 158.47 2023-04-01: 174.49 2024-04-01: 189.8 - 2025-04-01: 198.27 + 2025-04-01: + value: 198.27 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025 art. 29 + href: https://www.legislation.gov.uk/uksi/2025/295/article/29/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025 art. 29 + href: https://www.legislation.gov.uk/nisr/2025/64/article/29/made + - title: Benefit and pension rates 2025 to 2026 + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 208.07 metadata: label: Pension Credit Savings Credit income threshold (single) + uprating: gov.dwp.pension_credit.savings_credit.threshold_uprating.SINGLE COUPLE: values: 2015-04-06: 201.8 @@ -27,16 +41,27 @@ COUPLE: 2022-04-01: 251.7 2023-04-01: 277.12 2024-04-01: 301.22 - 2025-04-01: 314.34 + 2025-04-01: + value: 314.34 + metadata: + reference: + - title: The Social Security Benefits Up-rating Order 2025 art. 29 + href: https://www.legislation.gov.uk/uksi/2025/295/article/29/made + - title: The Social Security Benefits Up-rating Order (Northern Ireland) 2025 art. 29 + href: https://www.legislation.gov.uk/nisr/2025/64/article/29/made + - title: Benefit and pension rates 2025 to 2026 + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 2026-04-01: 329.75 metadata: label: Pension Credit Savings Credit income threshold (couple) + uprating: gov.dwp.pension_credit.savings_credit.threshold_uprating.COUPLE metadata: unit: currency-GBP period: week - uprating: gov.benefit_uprating_cpi propagate_metadata_to_children: true reference: + - title: State Pension Credit Act 2002 s. 3 + href: https://www.legislation.gov.uk/ukpga/2002/16/section/3 - title: The State Pension Credit Regulations 2002 reg. 7 href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/7/2021-04-12 - title: Benefit and pension rates 2025 to 2026 (DWP, gov.uk) @@ -47,3 +72,19 @@ metadata: href: https://www.legislation.gov.uk/uksi/2025/295/contents/made - title: The Social Security Benefits Up-rating Order 2026 (SI 2026/148) href: https://www.legislation.gov.uk/uksi/2026/148/contents/made + - title: Social Security Administration Act 1992 section 150 + href: https://www.legislation.gov.uk/ukpga/1992/5/section/150 + - title: Social Security Administration Act 1992 section 150A + href: https://www.legislation.gov.uk/ukpga/1992/5/section/150A + - title: Social Security Administration (Northern Ireland) Act 1992 section 132 + href: https://www.legislation.gov.uk/ukpga/1992/8/section/132 + - title: Social Security Administration (Northern Ireland) Act 1992 section 132A + href: https://www.legislation.gov.uk/ukpga/1992/8/section/132A + - title: A11/2024 Housing Benefit uprating for the financial year ending March 2026 + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2024/a112024-housing-benefit-uprating-for-the-financial-year-ending-march-2026 + - title: A14/2025 Housing Benefit uprating for the financial year ending March 2027 + href: https://www.gov.uk/government/publications/housing-benefit-adjudication-circulars-2025/a142025-housing-benefit-uprating-for-the-financial-year-ending-march-2027 + - title: ONS KAC3 average weekly earnings, whole economy, total pay + href: https://www.ons.gov.uk/employmentandlabourmarket/peopleinwork/earningsandworkinghours/timeseries/kac3/lms + - title: ONS D7G7 Consumer Prices Index annual rate + href: https://www.ons.gov.uk/economy/inflationandpriceindices/timeseries/d7g7/mm23 diff --git a/policyengine_uk/parameters/gov/dwp/state_pension/triple_lock/create_triple_lock.py b/policyengine_uk/parameters/gov/dwp/state_pension/triple_lock/create_triple_lock.py index 530f8b1b54..85073a6a50 100644 --- a/policyengine_uk/parameters/gov/dwp/state_pension/triple_lock/create_triple_lock.py +++ b/policyengine_uk/parameters/gov/dwp/state_pension/triple_lock/create_triple_lock.py @@ -1,4 +1,4 @@ -"""Build the State Pension uprating series from the statutory inputs. +"""Build statutory earnings and State Pension uprating series. Each April the basic and new State Pension rise by the highest of: @@ -29,10 +29,13 @@ earnings path started from its level in the year before the guarantee first applied. Current law leaves it off. -The output parameter ``gov.economic_assumptions.yoy_growth.triple_lock`` holds -the rate taking effect in April of each year, keyed to 1 January as the other -growth series are, and runs to one year past the last year of the -economic-assumption series. +The output parameters hold the rates taking effect in April of each year, +keyed to 1 January as the other growth series are. ``triple_lock`` applies the +State Pension rule. ``statutory_earnings_floor`` holds the non-negative +earnings component for amounts, including the Pension Credit standard minimum +guarantee, that section 150A protects without extending the triple lock to +them. Both run to one year past the last year of the economic-assumption +series. """ from dataclasses import dataclass @@ -148,6 +151,13 @@ def uprating_rates(years: Dict[int, UpratingYear]) -> Dict[int, float]: return rates +def statutory_earnings_floor_rates( + years: Dict[int, UpratingYear], +) -> Dict[int, float]: + """Return the non-negative earnings rate from each statutory review.""" + return {year: max(inputs.earnings, 0.0) for year, inputs in years.items()} + + def _flag(parameter: Parameter, instant: str) -> bool: value = parameter(instant) return bool(value) if value is not None else False @@ -193,22 +203,53 @@ def read_uprating_years(parameters: ParameterNode) -> Dict[int, UpratingYear]: def add_triple_lock(parameters: ParameterNode) -> ParameterNode: - """Add ``gov.economic_assumptions.yoy_growth.triple_lock``.""" - rates = uprating_rates(read_uprating_years(parameters)) - new_parameter = Parameter( + """Add the statutory earnings floor and State Pension triple lock.""" + years = read_uprating_years(parameters) + triple_lock_rates = uprating_rates(years) + earnings_floor_rates = statutory_earnings_floor_rates(years) + triple_lock = Parameter( "gov.economic_assumptions.yoy_growth.triple_lock", data={ "description": ( "State Pension uprating taking effect in April of each year." ), - "values": {f"{year}-01-01": rate for year, rate in rates.items()}, + "values": { + f"{year}-01-01": rate for year, rate in triple_lock_rates.items() + }, "metadata": { "unit": "/1", "label": "State Pension uprating rate", }, }, ) + parameters.gov.economic_assumptions.yoy_growth.add_child("triple_lock", triple_lock) + statutory_earnings_floor = Parameter( + "gov.economic_assumptions.yoy_growth.statutory_earnings_floor", + data={ + "description": ( + "Non-negative May-July average weekly earnings growth used " + "as the statutory uprating floor under section 150A." + ), + "values": { + f"{year}-01-01": rate for year, rate in earnings_floor_rates.items() + }, + "metadata": { + "unit": "/1", + "label": "Statutory earnings uprating floor", + "reference": [ + { + "title": ( + "Social Security Administration Act 1992 section 150A" + ), + "href": ( + "https://www.legislation.gov.uk/ukpga/1992/5/section/150A" + ), + } + ], + }, + }, + ) parameters.gov.economic_assumptions.yoy_growth.add_child( - "triple_lock", new_parameter + "statutory_earnings_floor", statutory_earnings_floor ) return parameters diff --git a/policyengine_uk/tax_benefit_system.py b/policyengine_uk/tax_benefit_system.py index 9e4175a690..0940da58f8 100644 --- a/policyengine_uk/tax_benefit_system.py +++ b/policyengine_uk/tax_benefit_system.py @@ -25,6 +25,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_protected_pension_age_uprating import ( + add_protected_pension_age_uprating, +) +from policyengine_uk.parameters.gov.dwp.pension_credit.create_savings_credit_threshold_projection import ( + add_savings_credit_threshold_projection, +) from policyengine_uk.parameters.gov.economic_assumptions.create_september_cpi_uprating import ( add_september_cpi_uprating, ) @@ -91,6 +97,8 @@ def _add_generated_parameters(parameters: ParameterNode) -> ParameterNode: parameters = add_triple_lock(parameters) parameters = add_september_cpi_uprating(parameters) parameters = create_economic_assumption_indices(parameters) + parameters = add_savings_credit_threshold_projection(parameters) + parameters = add_protected_pension_age_uprating(parameters) parameters = add_threshold_indexation(parameters) parameters = add_lsr_deprecation_aliases(parameters) return parameters @@ -110,6 +118,8 @@ def process_parameter_tree( - triple lock calculations for state pensions - September CPI uprating - economic assumption indices + - Pension Credit Savings Credit threshold projections + - protected pension-age Housing Benefit allowance indices - income tax and NICs threshold indexation after the freeze - parameter uprating and backdating - conversion to fiscal year parameters diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/protected_pension_age_rates.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/protected_pension_age_rates.yaml new file mode 100644 index 0000000000..82e5edb3c1 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/protected_pension_age_rates.yaml @@ -0,0 +1,55 @@ +# Protected pre-April-2021 pension-age rates: SI 2025/295 Sch 6, table +# paras 1(1)(b) and 1(2)(b); NI SR 2025/64 Sch 6 gives the same amounts. +# https://www.legislation.gov.uk/uksi/2025/295/schedule/6/made +# https://www.legislation.gov.uk/nisr/2025/64/schedule/6/made +# Taper: SI 2006/214 reg 51 (65% of excess applicable income). +# https://www.legislation.gov.uk/uksi/2006/214/regulation/51 +# Eligibility, assessed income, premiums and rent deductions are supplied to +# isolate the personal allowance's effect on the tapered award. + +- name: Protected single pensioner receives the enacted 2025 allowance + period: 2025 + absolute_error_margin: 0.01 + input: + age: 80 + is_claimant_or_partner: true + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + benefits_premiums: 0 + housing_benefit_applicable_income: 13_000 + benunit_rent: 10_000 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + housing_benefit_applicable_amount: 12_708.80 # £244.40 * 52 + housing_benefit_entitlement: 9_810.72 # £10,000 - 0.65 * (£13,000 - £12,708.80) + +- name: Protected pensioner couple receives the enacted 2025 allowance + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + partner: + age: 80 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant, partner] + housing_benefit_eligible: true + housing_benefit_pension_age_regulations_apply: true + benefits_premiums: 0 + housing_benefit_applicable_income: 19_500 + benunit_rent: 10_000 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + households: + household: + members: [claimant, partner] + output: + housing_benefit_applicable_amount: 19_032 # £366 * 52 + housing_benefit_entitlement: 9_695.80 # £10,000 - 0.65 * (£19,500 - £19,032) 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..67f6cde11d 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,31 @@ members: [person1, person2] output: standard_minimum_guarantee: 18_889 + +- name: Case 3, 2025 single-person standard minimum guarantee uses the published rate. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + benunits: + benunit: + members: [person1] + output: + standard_minimum_guarantee: 11_809.2 + +- name: Case 4, 2025 couple standard minimum guarantee uses the published rate. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 80 + person2: + age: 78 + benunits: + benunit: + members: [person1, person2] + output: + standard_minimum_guarantee: 18_023.2 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml index 8e32aebaa7..08d297b002 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/is_savings_credit_eligible.yaml @@ -99,3 +99,69 @@ output: # 12,000 > 208.07 * 52, but no claimant reached SPA before the cutoff. is_savings_credit_eligible: false + +- name: Case 6, 2025 single claimant below the published threshold is ineligible. + period: 2025 + input: + people: + person: + age: 80 + state_pension_age: 60 + benunits: + benunit: + members: [person] + relation_type: SINGLE + # One penny below 198.27 * 52. + savings_credit_income: 10_310.03 + output: + is_savings_credit_eligible: false + +- name: Case 7, 2025 couple below the published threshold is ineligible. + period: 2025 + input: + people: + person: + age: 80 + state_pension_age: 60 + person2: + age: 78 + state_pension_age: 60 + benunits: + benunit: + members: [person, person2] + relation_type: COUPLE + savings_credit_income: 16_000 + output: + is_savings_credit_eligible: false + +- name: Case 8, 2025 single claimant above the published threshold is eligible. + period: 2025 + input: + people: + person: + age: 80 + state_pension_age: 60 + benunits: + benunit: + members: [person] + relation_type: SINGLE + # One penny above 198.27 * 52. + savings_credit_income: 10_310.05 + output: + is_savings_credit_eligible: true + +- name: Case 9, 2027 uses the threshold derived from the two projection bases. + period: 2027 + input: + people: + person: + age: 82 + state_pension_age: 60 + benunits: + benunit: + members: [person] + relation_type: SINGLE + savings_credit_income: 11_000 + output: + # The projected weekly threshold is about 216.71465, or 11,269.16 a year. + is_savings_credit_eligible: false 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 1d613f2042..ee80935333 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 @@ -23,3 +23,41 @@ minimum_guarantee: 12_376 output: savings_credit: 684.216 + +- name: Case 3, 2025 single claimant uses the corrected threshold. + period: 2025 + absolute_error_margin: 0.01 + input: + is_savings_credit_eligible: true + relation_type: SINGLE + savings_credit_income: 11_000 + pension_credit_income: 11_000 + output: + # 0.6 * (11,000 - 198.27 * 52). + savings_credit: 413.976 + +- name: Case 4, 2025 couple uses the corrected threshold. + period: 2025 + absolute_error_margin: 0.01 + input: + is_savings_credit_eligible: true + relation_type: COUPLE + savings_credit_income: 17_000 + pension_credit_income: 17_000 + output: + # 0.6 * (17,000 - 314.34 * 52). + savings_credit: 392.592 + +- name: Case 5, 2027 uses the threshold derived from the two projection bases. + period: 2027 + absolute_error_margin: 0.01 + input: + is_savings_credit_eligible: true + relation_type: SINGLE + savings_credit_income: 12_000 + pension_credit_income: 12_000 + standard_minimum_guarantee: 12_376 + minimum_guarantee: 20_000 + output: + # 0.6 * (12,000 - 216.7146545 * 52). + savings_credit: 438.5028 diff --git a/policyengine_uk/tests/test_housing_benefit_protected_allowance_uprating.py b/policyengine_uk/tests/test_housing_benefit_protected_allowance_uprating.py new file mode 100644 index 0000000000..1ec190d5f5 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_protected_allowance_uprating.py @@ -0,0 +1,183 @@ +import pytest + +from policyengine_uk import Simulation, system +from policyengine_uk.parameters.gov.dwp.housing_benefit.allowances.create_protected_pension_age_uprating import ( + project_retained_uplift, +) + + +def _parameters(): + return system.parameters + + +@pytest.mark.parametrize( + ( + "allowance_name", + "index_name", + "base_allowance", + "expected_2027_allowance", + ), + [ + ("single", "single", 256.0, 265.6622572049), + ("lone_parent", "single", 256.0, 265.6622572049), + ("couple", "couple", 383.35, 397.9411154524), + ], +) +def test_protected_allowances_use_relationship_specific_component_projections( + allowance_name, + index_name, + base_allowance, + expected_2027_allowance, +): + """The 2026 bases are guarantee plus retained Savings Credit uplift.""" + allowances = _parameters().gov.dwp.housing_benefit.allowances + index = getattr(allowances.protected_pension_age_uprating, index_name) + allowance = getattr(allowances, allowance_name).aged + expected = base_allowance * index("2027-04-01") / index("2026-04-01") + + assert allowance("2026-04-01") == base_allowance + assert expected == pytest.approx(expected_2027_allowance) + assert allowance("2027-04-01") == pytest.approx(expected) + assert allowance("2027-04-01") != pytest.approx( + base_allowance + * _parameters().gov.benefit_uprating_cpi("2027-04-01") + / _parameters().gov.benefit_uprating_cpi("2026-04-01") + ) + + +def test_protected_allowances_cover_guarantee_and_maximum_savings_credit(): + parameters = _parameters() + allowances = parameters.gov.dwp.housing_benefit.allowances + pension_credit = parameters.gov.dwp.pension_credit + guarantee = pension_credit.guarantee_credit.minimum_guarantee + threshold = pension_credit.savings_credit.threshold + + for year in range(2027, 2040): + instant = f"{year}-04-01" + phase_in_rate = float(pension_credit.savings_credit.rate.phase_in(instant)) + for relationship, allowance_name in ( + ("SINGLE", "single"), + ("COUPLE", "couple"), + ): + weekly_guarantee = float(getattr(guarantee, relationship)(instant)) + weekly_threshold = float(getattr(threshold, relationship)(instant)) + maximum_savings_credit = phase_in_rate * ( + weekly_guarantee - weekly_threshold + ) + protected_allowance = float( + getattr(allowances, allowance_name).aged(instant) + ) + assert protected_allowance >= ( + weekly_guarantee + maximum_savings_credit - 0.01 + ), (relationship, year) + + +def test_retained_uplift_projection_does_not_reduce_the_cash_amount(): + assert project_retained_uplift(18, -0.02) == 18 + + +def _boundary_simulation(couple: bool, private_pension: float) -> Simulation: + year = 2035 + names = ["claimant", "partner"] if couple else ["claimant"] + people = { + name: { + "age": {year: 85}, + "private_pension_income": { + year: private_pension if name == "claimant" else 0 + }, + } + for name in names + } + return Simulation( + situation={ + "people": people, + "benunits": {"benunit": {"members": names}}, + "households": { + "household": { + "members": names, + "country": {year: "ENGLAND"}, + "local_authority": {year: "MAIDSTONE"}, + "tenure_type": {year: "RENT_FROM_COUNCIL"}, + "rent": {year: 10_000}, + "council_tax": {year: 2_000}, + "savings": {year: 0}, + } + }, + } + ) + + +def _boundary_calculation(couple: bool, private_pension: float) -> dict[str, float]: + simulation = _boundary_simulation(couple, private_pension) + return { + variable: float(simulation.calculate(variable, 2035)[0]) + for variable in ( + "guarantee_credit", + "housing_benefit", + "council_tax_benefit", + "household_net_income", + "tv_licence", + ) + } + + +@pytest.mark.parametrize("couple", [False, True]) +def test_net_income_does_not_fall_when_guarantee_credit_ends(couple): + """Test £1 income steps across each modeled 2035 guarantee boundary. + + A normal marginal withdrawal may occur, but there must be no discontinuous + loss of Housing Benefit or Council Tax Reduction when Guarantee Credit ends. + """ + # Locate the actual cutoff, including income-tax deductions. Forecasts and + # tax thresholds can change independently of this discontinuity regression. + income_below, income_above = 0, 100_000 + assert ( + _boundary_simulation(couple, income_below).calculate("guarantee_credit", 2035)[ + 0 + ] + > 0 + ) + assert ( + _boundary_simulation(couple, income_above).calculate("guarantee_credit", 2035)[ + 0 + ] + == 0 + ) + while income_above - income_below > 1: + midpoint = (income_below + income_above) // 2 + if ( + _boundary_simulation(couple, midpoint).calculate("guarantee_credit", 2035)[ + 0 + ] + > 0 + ): + income_below = midpoint + else: + income_above = midpoint + + below = _boundary_calculation(couple, income_below) + above = _boundary_calculation(couple, income_above) + + assert below["guarantee_credit"] > 0 + assert above["guarantee_credit"] == 0 + assert above["housing_benefit"] >= below["housing_benefit"] - 1 + assert above["council_tax_benefit"] >= below["council_tax_benefit"] - 1 + assert above["household_net_income"] + above["tv_licence"] >= ( + below["household_net_income"] + below["tv_licence"] - 0.01 + ) + + +@pytest.mark.parametrize( + ("allowance_name", "weekly_amount"), + [("single", 244.40), ("lone_parent", 244.40), ("couple", 366.00)], +) +def test_protected_allowances_match_enacted_2025_amounts(allowance_name, weekly_amount): + """Pin the published protected-cohort rates, not a computed CPI estimate. + + SI 2025/295 Sch 6 and NI SR 2025/64 Sch 6, table paras 1(1)(b) + and 1(2)(b), prescribe these amounts independently of the model. + """ + allowance = getattr( + _parameters().gov.dwp.housing_benefit.allowances, allowance_name + ).aged + assert allowance("2025-04-30") == weekly_amount diff --git a/policyengine_uk/tests/test_savings_credit_threshold_projection.py b/policyengine_uk/tests/test_savings_credit_threshold_projection.py new file mode 100644 index 0000000000..06f7ac86f4 --- /dev/null +++ b/policyengine_uk/tests/test_savings_credit_threshold_projection.py @@ -0,0 +1,155 @@ +import pytest +from policyengine_core.periods import instant + +from policyengine_uk import Simulation, system +from policyengine_uk.model_api import Scenario +from policyengine_uk.parameters.gov.dwp.pension_credit.create_savings_credit_threshold_projection import ( + project_maximum_savings_credit, + round_currency, + savings_credit_threshold, +) +from policyengine_uk.tax_benefit_system import CountryTaxBenefitSystem + + +PENSIONER = { + "people": {"person": {"age": {2027: 70}}}, + "benunits": {"benunit": {"members": ["person"]}}, + "households": {"household": {"members": ["person"]}}, +} + + +def _parameters(): + return system.parameters + + +@pytest.mark.parametrize( + ("relationship", "base_maximum", "expected_2027_maximum"), + [ + ("SINGLE", 17.96, 18.3400336), + ("COUPLE", 20.10, 20.525316), + ], +) +def test_2027_threshold_preserves_the_cpi_projected_maximum( + relationship, + base_maximum, + expected_2027_maximum, +): + """DWP A14/2025 sets £17.96/£20.10 and a 3.8% enacted CPI rise. + + The first forecast uses the repository's 2.116% September 2026 CPI input. + """ + parameters = _parameters() + savings_credit = parameters.gov.dwp.pension_credit.savings_credit + guarantee = parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee + phase_in_rate = float(savings_credit.rate.phase_in("2027-04-01")) + projected_guarantee = float(getattr(guarantee, relationship)("2027-04-01")) + projected_threshold = float( + getattr(savings_credit.threshold, relationship)("2027-04-01") + ) + projected_maximum = phase_in_rate * (projected_guarantee - projected_threshold) + + assert base_maximum * 1.02116 == pytest.approx(expected_2027_maximum) + assert projected_maximum == pytest.approx(expected_2027_maximum) + + +def test_projected_thresholds_preserve_the_maximum_in_every_forecast_year(): + parameters = _parameters() + savings_credit = parameters.gov.dwp.pension_credit.savings_credit + guarantee = parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee + cpi = parameters.gov.economic_assumptions.statutory_uprating_inputs.cpi_september + + for relationship in ("SINGLE", "COUPLE"): + guarantee_parameter = getattr(guarantee, relationship) + threshold_parameter = getattr(savings_credit.threshold, relationship) + base_phase_in_rate = float(savings_credit.rate.phase_in("2026-04-01")) + maximum = round_currency( + base_phase_in_rate + * ( + float(guarantee_parameter("2026-04-01")) + - float(threshold_parameter("2026-04-01")) + ) + ) + for year in range(2027, 2040): + maximum = project_maximum_savings_credit( + maximum, + float(cpi(f"{year - 1}-09-01")), + ) + phase_in_rate = float(savings_credit.rate.phase_in(f"{year}-04-01")) + actual = phase_in_rate * ( + float(guarantee_parameter(f"{year}-04-01")) + - float(threshold_parameter(f"{year}-04-01")) + ) + assert actual == pytest.approx(maximum), (relationship, year) + + +def test_projection_floors_cpi_and_validates_the_phase_in_rate(): + assert project_maximum_savings_credit(17.96, -0.02) == 17.96 + assert savings_credit_threshold(238, 17.96, 0.6) == pytest.approx(208.0666666667) + with pytest.raises(ValueError, match="must be positive"): + savings_credit_threshold(238, 17.96, 0) + + +@pytest.mark.parametrize( + ("relationship", "reformed_value"), + [("SINGLE", 100), ("COUPLE", 200)], +) +def test_preprocessing_threshold_reform_is_not_replaced_by_projection( + relationship, + reformed_value, +): + """Generated indices must not overwrite scenario parameter changes.""" + path = f"gov.dwp.pension_credit.savings_credit.threshold.{relationship}" + simulation = Simulation( + situation=PENSIONER, + scenario=Scenario( + parameter_changes={path: {"2027": reformed_value}}, + applied_before_data_load=True, + ), + ) + threshold = simulation.tax_benefit_system.parameters.get_child(path) + + assert threshold("2027-06-01") == reformed_value + + +def test_postprocessing_threshold_reform_is_not_replaced_by_projection(): + """The direct reform interface must preserve its explicit value too.""" + path = "gov.dwp.pension_credit.savings_credit.threshold.SINGLE" + simulation = Simulation( + situation=PENSIONER, + reform={path: {"2027": 100}}, + ) + threshold = simulation.tax_benefit_system.parameters.get_child(path) + + assert threshold("2027-06-01") == 100 + + +def test_future_explicit_threshold_is_the_uprating_anchor(): + """A later source value remains exact and anchors subsequent forecasts.""" + tax_benefit_system = CountryTaxBenefitSystem() + tax_benefit_system.reset_parameters() + savings_credit = tax_benefit_system.parameters.gov.dwp.pension_credit.savings_credit + savings_credit.threshold.SINGLE.update( + start=instant("2027-04-01"), + stop=None, + value=100, + ) + + tax_benefit_system.process_parameters() + savings_credit = tax_benefit_system.parameters.gov.dwp.pension_credit.savings_credit + threshold = savings_credit.threshold.SINGLE + index = savings_credit.threshold_uprating.SINGLE + + assert threshold("2027-06-01") == 100 + assert threshold("2028-06-01") == pytest.approx( + 100 * index("2028-06-01") / index("2027-06-01") + ) + + +@pytest.mark.parametrize("relationship", ["SINGLE", "COUPLE"]) +def test_threshold_uses_generated_index_through_standard_uprating(relationship): + savings_credit = _parameters().gov.dwp.pension_credit.savings_credit + threshold = getattr(savings_credit.threshold, relationship) + + assert threshold.metadata["uprating"] == ( + f"gov.dwp.pension_credit.savings_credit.threshold_uprating.{relationship}" + ) diff --git a/policyengine_uk/tests/test_triple_lock_statutory_inputs.py b/policyengine_uk/tests/test_triple_lock_statutory_inputs.py index a3101905d0..8320947bc1 100644 --- a/policyengine_uk/tests/test_triple_lock_statutory_inputs.py +++ b/policyengine_uk/tests/test_triple_lock_statutory_inputs.py @@ -4,9 +4,10 @@ earnings growth from the previous year. These tests check that forecasts of those inputs follow the OBR where it publishes them, fall back to calendar-year growth after, respond to macro scenarios applied before the -data load, and can be set directly; and that the earnings-path guarantee -models one reading of the plan announced in September 2026 as a -parameter reform. +data load, and can be set directly. They also check that Pension Credit uses +the same May-July earnings input without inheriting the CPI and 2.5% elements +of the State Pension triple lock, and that the earnings-path guarantee models +one reading of the plan announced in September 2026 as a parameter reform. """ import numpy as np @@ -43,10 +44,21 @@ def uprating(parameters, year): return parameters.gov.economic_assumptions.yoy_growth.triple_lock(f"{year}-01-01") +def statutory_earnings_floor(parameters, year): + return parameters.gov.economic_assumptions.yoy_growth.statutory_earnings_floor( + f"{year}-01-01" + ) + + def new_state_pension_weekly(parameters, year): return parameters.gov.dwp.state_pension.new_state_pension.amount(f"{year}-06-01") +def minimum_guarantee_weekly(parameters, relation_type, year): + guarantee = parameters.gov.dwp.pension_credit.guarantee_credit.minimum_guarantee + return getattr(guarantee, relation_type)(f"{year}-06-01") + + def statutory_inputs(parameters): return parameters.gov.economic_assumptions.statutory_uprating_inputs @@ -63,6 +75,42 @@ def test_april_2027_uses_published_may_july_2026_earnings(): ) +def test_pension_credit_uses_the_state_pension_statutory_earnings_input(): + parameters = system.parameters + + assert statutory_inputs(parameters).awe_total_pay_may_july( + "2026-07-01" + ) == pytest.approx(0.039) + assert statutory_earnings_floor(parameters, 2027) == pytest.approx(0.039) + assert minimum_guarantee_weekly(parameters, "SINGLE", 2027) == pytest.approx( + 238 * 1.039, rel=1e-5 + ) + assert minimum_guarantee_weekly(parameters, "COUPLE", 2027) == pytest.approx( + 363.25 * 1.039, rel=1e-5 + ) + + +def test_pension_credit_earnings_floor_rounds_and_never_reduces_the_guarantee(): + parameters = parameters_under( + { + f"{INPUTS}.awe_total_pay_may_july": { + "2027": 0.0124, + "2028": -0.004, + } + } + ) + + assert [ + statutory_earnings_floor(parameters, year) for year in (2028, 2029) + ] == pytest.approx([0.012, 0.0]) + for relation_type in ("SINGLE", "COUPLE"): + guarantee_2027 = minimum_guarantee_weekly(parameters, relation_type, 2027) + guarantee_2028 = minimum_guarantee_weekly(parameters, relation_type, 2028) + guarantee_2029 = minimum_guarantee_weekly(parameters, relation_type, 2029) + assert guarantee_2028 == pytest.approx(guarantee_2027 * 1.012, rel=1e-5) + assert guarantee_2029 == pytest.approx(guarantee_2028, rel=1e-5) + + def test_every_published_year_has_its_own_value(): """A missing year would silently carry the previous year's figure.""" inputs = statutory_inputs(system.parameters)