Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-new-state-pension-reported.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Replace `new_state_pension`'s flat-max payout with a `min(reported, max) / max * period_max` formula mirroring `basic_state_pension`, and extend `additional_state_pension` to NEW-type retirees so any pre-2016 SERPS/S2P Protected Payment flows through as an add-on instead of being silently dropped. Partial-NI-record retirees now receive their actual pro-rated rate rather than the full flat max. Closes part of the ~£12 bn residual state-pension gap vs the OBR target tracked in #1632.
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
- name: New State Pension uses reported amount, not flat max
# Under 35 NI years — partial rate should be preserved, not paid at max.
period: 2021
input:
people:
person1:
state_pension_reported: 7000 # Below max (9339.20 = 179.60 * 52)
state_pension_type: NEW
age: 67
output:
new_state_pension: 7000

- name: New State Pension equals max for full-rate retirees
period: 2021
input:
people:
person1:
state_pension_reported: 9339.20 # Exactly the 2021 max
state_pension_type: NEW
age: 67
output:
new_state_pension: 9339.20

- name: New State Pension caps at max; Protected Payment flows to additional_state_pension
# Pre-2016 SERPS/S2P accrual — reported exceeds flat max. The flat-rate
# component stays capped at the max, and the excess is treated as a
# Protected Payment top-up tracked via additional_state_pension.
period: 2021
input:
people:
person1:
state_pension_reported: 12000 # Above max — Protected Payment
state_pension_type: NEW
age: 67
output:
new_state_pension: 9339.20
additional_state_pension: 2660.80 # 12000 − 9339.20

- name: Additional State Pension captures SERPS/S2P for BASIC retirees
period: 2021
input:
people:
person1:
state_pension_reported: 9000 # Above basic max (7155.20)
state_pension_type: BASIC
age: 70
output:
additional_state_pension: 1844.80 # 9000 − 7155.20

- name: NEW-type retiree with zero reported pension gets zero NSP
# Deferral or FRS non-report. Matches reality — a deferred NSP claimant
# genuinely receives nothing until they start drawing. This differs from
# the old formula, which paid the flat max regardless of reported.
period: 2021
input:
people:
person1:
state_pension_reported: 0
state_pension_type: NEW
age: 67
output:
new_state_pension: 0
additional_state_pension: 0

- name: Basic type returns zero for new_state_pension
period: 2021
input:
people:
person1:
state_pension_reported: 5000
state_pension_type: BASIC
age: 70
output:
new_state_pension: 0

- name: New State Pension is zero when max is zero
period: 2021
input:
gov.dwp.state_pension.new_state_pension.amount: 0
people:
person1:
state_pension_reported: 9000
state_pension_type: NEW
age: 67
output:
new_state_pension: 0
46 changes: 33 additions & 13 deletions policyengine_uk/variables/gov/dwp/additional_state_pension.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,45 @@ class additional_state_pension(Variable):

def formula(person, period, parameters):
simulation = person.simulation
if simulation.dataset is None:
return 0
try:
data_year = min(simulation.dataset.years)
except:
has_dataset = simulation.dataset is not None
if has_dataset:
try:
data_year = min(simulation.dataset.years)
except:
data_year = period.start.year
else:
data_year = period.start.year
reported = person("state_pension_reported", data_year) / WEEKS_IN_YEAR
type = person("state_pension_type", data_year)
sp_amount = parameters.gov.dwp.state_pension.basic_state_pension.amount
max_sp_data_year = sp_amount(data_year)
max_sp_period = sp_amount(period)
pension_type = person("state_pension_type", data_year)
types = pension_type.possible_values

bsp_amount = parameters.gov.dwp.state_pension.basic_state_pension.amount
nsp_amount = parameters.gov.dwp.state_pension.new_state_pension.amount

# Each pension type has its own flat-rate ceiling; anything above
# the ceiling is an add-on:
# BASIC → SERPS / S2P (pre-2016 earnings-related top-up)
# NEW → Protected Payment (pre-2016 accrual exceeding the
# new flat rate, folded into NSP under current law)
max_for_type_data = select(
[pension_type == types.BASIC, pension_type == types.NEW],
[bsp_amount(data_year), nsp_amount(data_year)],
default=0,
)
max_for_type_period = select(
[pension_type == types.BASIC, pension_type == types.NEW],
[bsp_amount(period), nsp_amount(period)],
default=0,
)

amount_in_data_year = where(
type == type.possible_values.BASIC,
max_(reported - max_sp_data_year, 0),
pension_type != types.NONE,
max_(reported - max_for_type_data, 0),
0,
)
uprating = where(
max_sp_data_year > 0,
max_sp_period / max_sp_data_year,
max_for_type_data > 0,
max_for_type_period / max_for_type_data,
1,
)
return amount_in_data_year * uprating * WEEKS_IN_YEAR
35 changes: 29 additions & 6 deletions policyengine_uk/variables/gov/dwp/new_state_pension.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,33 @@ class new_state_pension(Variable):

def formula(person, period, parameters):
simulation = person.simulation
if simulation.dataset is None:
return 0
has_dataset = simulation.dataset is not None

type = person("state_pension_type", period)
eligible = type == type.possible_values.NEW
p = parameters(period).gov.dwp.state_pension.new_state_pension
return eligible * p.amount * WEEKS_IN_YEAR
if has_dataset:
try:
data_year = min(simulation.dataset.years)
except:
data_year = period.start.year
else:
data_year = period.start.year

pension_type = person("state_pension_type", period)
eligible = pension_type == pension_type.possible_values.NEW

reported_weekly = person("state_pension_reported", data_year) / WEEKS_IN_YEAR
p = parameters.gov.dwp.state_pension.new_state_pension
max_new_data_year = p.amount(data_year)
max_new_period = p.amount(period)

# Pro-rate by reported amount, capped at the flat max so a partial
# NI record gets the appropriate partial rate. Any reported amount
# above the flat max is Protected Payment (pre-2016 SERPS/S2P) and
# flows through ``additional_state_pension`` — mirrors the
# BASIC / ASP split so ``new_state_pension`` is always the headline
# flat-rate component.
share = where(
max_new_data_year > 0,
min_(reported_weekly, max_new_data_year) / max_new_data_year,
0,
)
return eligible * share * max_new_period * WEEKS_IN_YEAR
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading