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
4 changes: 4 additions & 0 deletions changelog_entry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- bump: minor
changes:
added:
- Salary sacrifice pension cap policy modeling (2,000 GBP cap on NI-exempt contributions)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# The percentage by which employees reduce their salary sacrifice pension contributions in response to the cap.
# - 0 = no adjustment (employees keep high salary sacrifice and pay NI on excess)
# - 0.5 = employees reduce salary sacrifice by 50% of the excess above cap
# - 1.0 = employees fully optimize (reduce salary sacrifice to exactly the cap)
# Most employees would optimize their salary sacrifice to avoid NI charges by reducing it to the cap level.
description: The percentage by which employees reduce their salary sacrifice pension contributions in response to the salary sacrifice pension cap.
values:
2010-01-01: 1.0 # Default: full optimization (employees reduce to cap level)
metadata:
unit: /1
label: Employee salary sacrifice reduction rate in response to cap
reference:
- title: "Cap on UK salary sacrifice benefits is 'short-term' choice, warn experts"
href: https://docs.google.com/document/d/1Rhrfrg7A_oZHudmA775otAn1EE4-YthgeyS9nL-PrE8/edit?tab=t.0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit but this is more a floor than a cap right?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that it's technically a floor for the exempt amount. However, the policy literature (FT article) refers to it as a "cap on the salary sacrifice exemption," so we've followed that convention for consistency.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# The annual cap on salary sacrifice pension contributions that are exempt from NI.
# Contributions above this cap are subject to standard NI rates.
# When set to infinity (default), the scheme is inactive and all salary sacrifice remains exempt from NI.
description: The annual cap on salary sacrifice pension contributions that are exempt from employee and employer National Insurance contributions.
values:
2010-01-01: .inf # Default: no cap (scheme inactive)
metadata:
unit: currency-GBP
label: Salary sacrifice pension NI exemption cap
period: year
reference:
- title: "Cap on UK salary sacrifice benefits is 'short-term' choice, warn experts"
href: https://docs.google.com/document/d/1Rhrfrg7A_oZHudmA775otAn1EE4-YthgeyS9nL-PrE8/edit?tab=t.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
- name: No employee NI charge when salary sacrifice below £2,000 cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 40_000
pension_contributions_via_salary_sacrifice: 1_500
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: 0

- name: Employee NI charge at 8% main rate for moderate earner above cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 45_000
pension_contributions_via_salary_sacrifice: 2_250
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: (2_250 - 2_000) * 0.08 # £250 excess * 8%

- name: Employee NI charge at 8% main rate for earner just below UEL
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 50_000
pension_contributions_via_salary_sacrifice: 10_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: (10_000 - 2_000) * 0.08 # £8,000 excess * 8%

- name: Employee NI charge at 2% additional rate for high earner above UEL
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 125_000
pension_contributions_via_salary_sacrifice: 25_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: (25_000 - 2_000) * 0.02 # £23,000 excess * 2%

- name: Employee NI charge at 2% for earner well above UEL
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 80_000
pension_contributions_via_salary_sacrifice: 5_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: (5_000 - 2_000) * 0.02 # £3,000 excess * 2%

- name: No employee NI charge when exactly at cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 60_000
pension_contributions_via_salary_sacrifice: 2_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: 0

- name: Employee NI charge for small excess over cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 35_000
pension_contributions_via_salary_sacrifice: 2_100
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employee: 100 * 0.08 # £100 excess * 8%

- name: Employee optimizes salary sacrifice to cap (100% behavioral response)
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 45_000
pension_contributions_via_salary_sacrifice: 3_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 1.0
output:
salary_sacrifice_pension_ni_employee: 0
pension_contributions_via_salary_sacrifice_adjusted: 2_000
salary_sacrifice_returned_to_income: 1_000
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
- name: No employer NI charge when salary sacrifice below £2,000 cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 40_000
pension_contributions_via_salary_sacrifice: 1_500
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: 0

- name: Employer NI charge at 15% for moderate earner above cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 45_000
pension_contributions_via_salary_sacrifice: 2_250
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: (2_250 - 2_000) * 0.15 # £250 excess * 15%

- name: Employer NI charge at 15% for high earner with large excess
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 125_000
pension_contributions_via_salary_sacrifice: 25_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: (25_000 - 2_000) * 0.15 # £23,000 excess * 15%

- name: Employer NI charge for large salary sacrifice
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 80_000
pension_contributions_via_salary_sacrifice: 15_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: (15_000 - 2_000) * 0.15 # £13,000 excess * 15%

- name: No employer NI charge when exactly at cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 60_000
pension_contributions_via_salary_sacrifice: 2_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: 0

- name: Employer NI charge for small excess over cap
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 35_000
pension_contributions_via_salary_sacrifice: 2_100
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: 100 * 0.15 # £100 excess * 15%

- name: Employer NI charge for low earner with high pension contribution
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 30_000
pension_contributions_via_salary_sacrifice: 5_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 0
output:
salary_sacrifice_pension_ni_employer: (5_000 - 2_000) * 0.15 # £3,000 excess * 15%

- name: No employer NI charge when employee optimizes (100% behavioral response)
period: 2025
absolute_error_margin: 0.01
input:
employment_income: 45_000
pension_contributions_via_salary_sacrifice: 3_000
gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000
gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate: 1.0
output:
salary_sacrifice_pension_ni_employer: 0
pension_contributions_via_salary_sacrifice_adjusted: 2_000
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ class national_insurance(Variable):
"ni_class_2",
"ni_class_3",
"ni_class_4",
"salary_sacrifice_pension_ni_employee",
]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the UK have a separate contributed structure for structural reform?
As far as I can see this is not part of the computation tree, are we looking to estimate change in housheold income?

Or should this be purely standalone?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These variables are now integrated into the computation tree - salary_sacrifice_pension_ni_employee is added to national_insurance and salary_sacrifice_pension_ni_employer is added to total_national_insurance. This ensures they affect household net income and government balance calculations when the reform is active (cap set to finite value).

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from policyengine_uk.model_api import *


class salary_sacrifice_pension_ni_employee(Variable):
label = "Employee NI on salary sacrifice pension contributions above cap"
documentation = (
"Additional employee National Insurance contributions due to "
"salary sacrifice pension contributions exceeding the £2,000 cap. "
"The excess is subject to standard NI rates: the main rate for "
"earnings below the Upper Earnings Limit, and the additional rate "
"for earnings above it."
)
entity = Person
definition_period = YEAR
value_type = float
unit = GBP
reference = "https://docs.google.com/document/d/1Rhrfrg7A_oZHudmA775otAn1EE4-YthgeyS9nL-PrE8/edit?tab=t.0"

def formula(person, period, parameters):
# Use adjusted salary sacrifice after behavioral response
ss_contributions = person(
"pension_contributions_via_salary_sacrifice_adjusted", period
)
cap = parameters(
period
).gov.hmrc.national_insurance.salary_sacrifice_pension_cap

# If cap is infinite, scheme is inactive (no charge)
if np.isinf(cap):
return 0

excess = max_(ss_contributions - cap, 0)

# Use existing NI Class 1 parameters
ni_params = parameters(period).gov.hmrc.national_insurance.class_1
employment_income = person("employment_income", period)
upper_earnings_limit = (
ni_params.thresholds.upper_earnings_limit * WEEKS_IN_YEAR
)

# Apply appropriate NI rate based on income level
# Main rate (8%) for income ≤ UEL, additional rate (2%) for income > UEL
ni_rate = where(
employment_income <= upper_earnings_limit,
ni_params.rates.employee.main,
ni_params.rates.employee.additional,
)

return excess * ni_rate
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from policyengine_uk.model_api import *


class salary_sacrifice_pension_ni_employer(Variable):
label = "Employer NI on salary sacrifice pension contributions above cap"
documentation = (
"Additional employer National Insurance contributions due to "
"salary sacrifice pension contributions exceeding the £2,000 cap. "
"The excess is subject to the standard employer NI rate of 15%."
)
entity = Person
definition_period = YEAR
value_type = float
unit = GBP
reference = "https://docs.google.com/document/d/1Rhrfrg7A_oZHudmA775otAn1EE4-YthgeyS9nL-PrE8/edit?tab=t.0"

def formula(person, period, parameters):
# Use adjusted salary sacrifice after behavioral response
ss_contributions = person(
"pension_contributions_via_salary_sacrifice_adjusted", period
)
cap = parameters(
period
).gov.hmrc.national_insurance.salary_sacrifice_pension_cap

# If cap is infinite, scheme is inactive (no charge)
if np.isinf(cap):
return 0

excess = max_(ss_contributions - cap, 0)

# Use existing NI Class 1 employer rate parameter
ni_params = parameters(period).gov.hmrc.national_insurance.class_1
employer_rate = ni_params.rates.employer

return excess * employer_rate
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ class total_national_insurance(Variable):
adds = [
"national_insurance",
"ni_class_1_employer",
"salary_sacrifice_pension_ni_employer",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from policyengine_uk.model_api import *


class pension_contributions_via_salary_sacrifice_adjusted(Variable):
label = "Adjusted salary sacrifice pension contributions after behavioral response"
documentation = (
"The actual amount of salary sacrifice pension contributions after employees "
"adjust their behavior in response to the salary sacrifice cap. When the cap "
"is active and employees face NI charges on excess contributions, they may "
"reduce their salary sacrifice to the cap level. The reduction rate is "
"controlled by the employee_salary_sacrifice_reduction_rate parameter."
)
entity = Person
definition_period = YEAR
value_type = float
unit = GBP
reference = "https://docs.google.com/document/d/1Rhrfrg7A_oZHudmA775otAn1EE4-YthgeyS9nL-PrE8/edit?tab=t.0"

def formula(person, period, parameters):
intended_ss = person(
"pension_contributions_via_salary_sacrifice", period
)
cap = parameters(
period
).gov.hmrc.national_insurance.salary_sacrifice_pension_cap

# If cap is infinite, no adjustment needed
if np.isinf(cap):
return intended_ss

# Calculate excess above cap
excess = max_(intended_ss - cap, 0)

# Get behavioral response rate
reduction_rate = parameters(
period
).gov.contrib.behavioral_responses.employee_salary_sacrifice_reduction_rate

# Amount employee reduces their salary sacrifice
reduction = excess * reduction_rate

# Adjusted salary sacrifice
return intended_ss - reduction
Comment thread
vahid-ahmadi marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from policyengine_uk.model_api import *


class salary_sacrifice_returned_to_income(Variable):
label = "Amount of salary sacrifice returned to employment income"
documentation = (
"The amount of salary sacrifice that is returned to regular employment income "
"when employees reduce their salary sacrifice in response to the pension cap. "
"This amount becomes subject to regular income tax and NI as employment income."
)
entity = Person
definition_period = YEAR
value_type = float
unit = GBP
reference = "https://docs.google.com/document/d/1Rhrfrg7A_oZHudmA775otAn1EE4-YthgeyS9nL-PrE8/edit?tab=t.0"

def formula(person, period, parameters):
intended_ss = person(
"pension_contributions_via_salary_sacrifice", period
)
adjusted_ss = person(
"pension_contributions_via_salary_sacrifice_adjusted", period
)

# The difference is returned to employment income
return intended_ss - adjusted_ss
Comment thread
vahid-ahmadi marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from policyengine_uk.model_api import *


class pension_contributions_via_salary_sacrifice(Variable):
label = "Pension contributions via salary sacrifice"
documentation = (
"Annual amount of pension contributions made through salary sacrifice "
"arrangements, where the employee agrees to reduce their gross salary "
"in exchange for employer pension contributions"
)
entity = Person
definition_period = YEAR
value_type = float
unit = GBP
reference = "https://datacatalogue.ukdataservice.ac.uk/datasets/dataset/630d4a8d-ba6a-82b3-f33d-c713c66efcb3"
uprating = "gov.economic_assumptions.indices.obr.average_earnings"
Loading
Loading