-
Notifications
You must be signed in to change notification settings - Fork 32
Salary sacrifice pension modeling #1372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the UK have a separate contributed structure for structural reform? Or should this be purely standalone?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These variables are now integrated into the computation tree - |
| 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 |
|---|---|---|
| @@ -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 | ||
|
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 | ||
|
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" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.