diff --git a/changelog.d/salary-sacrifice-needs-pay.fixed.md b/changelog.d/salary-sacrifice-needs-pay.fixed.md new file mode 100644 index 0000000000..ce4684f9a4 --- /dev/null +++ b/changelog.d/salary-sacrifice-needs-pay.fixed.md @@ -0,0 +1 @@ +- Counted salary sacrifice pension contributions only for people with pay, so from 2029 the excess over the £2,000 cap no longer gives employment income to people without pay. diff --git a/policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py b/policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py index 74d9c2a758..f6fe96de52 100644 --- a/policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py +++ b/policyengine_uk/tests/microsimulation/test_salary_sacrifice_cap_reform.py @@ -222,9 +222,10 @@ def test_full_excess_redirected(reform_simulation): The broad-base haircut reduces ALL workers' employment income, but the full excess above cap is redirected to regular pension contributions. """ - # Get weighted totals using map_to for proper aggregation + # Get weighted totals using map_to for proper aggregation. Only people + # with pay have a salary sacrifice, so the excess is measured on that. ss_contributions = reform_simulation.calculate( - "pension_contributions_via_salary_sacrifice", + "pension_contributions_via_salary_sacrifice_from_pay", POLICY_YEAR, map_to="person", ) diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml index 39785efd90..5b36e53d92 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml @@ -1,8 +1,13 @@ +# Salary sacrifice counts only for people with pay +# (employment_income_before_lsr > 0). This runner does not copy an +# employment_income input there (policyengine_uk.Simulation does), so each +# case sets both. - name: No employee NI charge when salary sacrifice below £2,000 cap period: 2025 absolute_error_margin: 0.01 input: employment_income: 40_000 + employment_income_before_lsr: 40_000 pension_contributions_via_salary_sacrifice: 1_500 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -14,6 +19,7 @@ absolute_error_margin: 0.01 input: employment_income: 45_000 + employment_income_before_lsr: 45_000 pension_contributions_via_salary_sacrifice: 2_250 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -26,6 +32,7 @@ absolute_error_margin: 0.01 input: employment_income: 50_000 + employment_income_before_lsr: 50_000 pension_contributions_via_salary_sacrifice: 10_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -38,6 +45,7 @@ absolute_error_margin: 0.01 input: employment_income: 125_000 + employment_income_before_lsr: 125_000 pension_contributions_via_salary_sacrifice: 25_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -50,6 +58,7 @@ absolute_error_margin: 0.01 input: employment_income: 80_000 + employment_income_before_lsr: 80_000 pension_contributions_via_salary_sacrifice: 5_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -62,6 +71,7 @@ absolute_error_margin: 0.01 input: employment_income: 60_000 + employment_income_before_lsr: 60_000 pension_contributions_via_salary_sacrifice: 2_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -73,6 +83,7 @@ absolute_error_margin: 0.01 input: employment_income: 35_000 + employment_income_before_lsr: 35_000 pension_contributions_via_salary_sacrifice: 2_100 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -85,6 +96,7 @@ absolute_error_margin: 0.01 input: employment_income: 45_000 + employment_income_before_lsr: 45_000 pension_contributions_via_salary_sacrifice: 3_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml index ac76d1cb41..4eb0fdf97c 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml @@ -1,8 +1,13 @@ +# Salary sacrifice counts only for people with pay +# (employment_income_before_lsr > 0). This runner does not copy an +# employment_income input there (policyengine_uk.Simulation does), so each +# case sets both. - name: No employer NI charge when salary sacrifice below £2,000 cap period: 2025 absolute_error_margin: 0.01 input: employment_income: 40_000 + employment_income_before_lsr: 40_000 pension_contributions_via_salary_sacrifice: 1_500 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -14,6 +19,7 @@ absolute_error_margin: 0.01 input: employment_income: 45_000 + employment_income_before_lsr: 45_000 pension_contributions_via_salary_sacrifice: 2_250 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -26,6 +32,7 @@ absolute_error_margin: 0.01 input: employment_income: 125_000 + employment_income_before_lsr: 125_000 pension_contributions_via_salary_sacrifice: 25_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -38,6 +45,7 @@ absolute_error_margin: 0.01 input: employment_income: 80_000 + employment_income_before_lsr: 80_000 pension_contributions_via_salary_sacrifice: 15_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -50,6 +58,7 @@ absolute_error_margin: 0.01 input: employment_income: 60_000 + employment_income_before_lsr: 60_000 pension_contributions_via_salary_sacrifice: 2_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -61,6 +70,7 @@ absolute_error_margin: 0.01 input: employment_income: 35_000 + employment_income_before_lsr: 35_000 pension_contributions_via_salary_sacrifice: 2_100 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -73,6 +83,7 @@ absolute_error_margin: 0.01 input: employment_income: 30_000 + employment_income_before_lsr: 30_000 pension_contributions_via_salary_sacrifice: 5_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 @@ -85,6 +96,7 @@ absolute_error_margin: 0.01 input: employment_income: 45_000 + employment_income_before_lsr: 45_000 pension_contributions_via_salary_sacrifice: 3_000 gov.hmrc.national_insurance.salary_sacrifice_pension_cap: 2_000 gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_for_annual_allowance.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_for_annual_allowance.yaml index db2ae81320..bc247412d8 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_for_annual_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_for_annual_allowance.yaml @@ -59,6 +59,7 @@ employee_pension_contributions_reported: 5_000 personal_pension_contributions: 0 employer_pension_contributions: 2_000 + employment_income_before_lsr: 60_000 pension_contributions_via_salary_sacrifice: 8_000 output: # employee_pension_contributions = 5_000 + 0 (no excess) = 5_000 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_from_pay.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_from_pay.yaml new file mode 100644 index 0000000000..1925d8af86 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_from_pay.yaml @@ -0,0 +1,49 @@ +- name: A sacrifice within pay is kept in full + period: 2029 + input: + employment_income_before_lsr: 40_000 + pension_contributions_via_salary_sacrifice: 5_000 + output: + pension_contributions_via_salary_sacrifice_from_pay: 5_000 + +- name: A whole-bonus sacrifice above the remaining salary is kept in full + period: 2029 + input: + # £40,000 salary kept, £50,000 bonus sacrificed (HMRC allows this while + # cash pay stays above the National Minimum Wage). + employment_income_before_lsr: 40_000 + pension_contributions_via_salary_sacrifice: 50_000 + output: + pension_contributions_via_salary_sacrifice_from_pay: 50_000 + +- name: A person without pay sacrifices nothing + period: 2029 + input: + pension_contributions_via_salary_sacrifice: 27_000 + output: + pension_contributions_via_salary_sacrifice_from_pay: 0 + +- name: Negative pay supports no sacrifice + period: 2029 + input: + employment_income_before_lsr: -1_000 + pension_contributions_via_salary_sacrifice: 27_000 + output: + pension_contributions_via_salary_sacrifice_from_pay: 0 + +- name: A negative sacrifice input counts as none + period: 2029 + input: + employment_income_before_lsr: 40_000 + pension_contributions_via_salary_sacrifice: -500 + output: + pension_contributions_via_salary_sacrifice_from_pay: 0 + +- name: The condition applies before the cap exists, 2028 + period: 2028 + input: + pension_contributions_via_salary_sacrifice: 27_000 + output: + pension_contributions_via_salary_sacrifice_from_pay: 0 + pension_contributions_via_salary_sacrifice_adjusted: 0 + salary_sacrifice_returned_to_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/salary_sacrifice_returned_to_income.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/salary_sacrifice_returned_to_income.yaml new file mode 100644 index 0000000000..2f3573564d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/pensions/salary_sacrifice_returned_to_income.yaml @@ -0,0 +1,84 @@ +# From 6 April 2029 the salary sacrifice above the £2,000 cap returns to +# employment income. Only people with pay have a salary sacrifice, so the cap +# never gives pay to a person who has none. + +- name: No pay and a salary sacrifice input gives no employment income, 2029 + period: 2029 + absolute_error_margin: 0.01 + input: + pension_contributions_via_salary_sacrifice: 27_000 + output: + salary_sacrifice_returned_to_income: 0 + pension_contributions_via_salary_sacrifice_adjusted: 0 + employment_income: 0 + salary_sacrifice_pension_ni_employee: 0 + salary_sacrifice_pension_ni_employer: 0 + employee_pension_contributions: 0 + +- name: No pay and a salary sacrifice input gives no employment income, 2030 + period: 2030 + absolute_error_margin: 0.01 + input: + pension_contributions_via_salary_sacrifice: 27_000 + output: + salary_sacrifice_returned_to_income: 0 + employment_income: 0 + +- name: Self-employed person with no pay gets no employment income, 2029 + period: 2029 + absolute_error_margin: 0.01 + input: + self_employment_income: 30_000 + pension_contributions_via_salary_sacrifice: 27_000 + output: + salary_sacrifice_returned_to_income: 0 + employment_income: 0 + +- name: Pay above the sacrifice returns the excess over the cap, 2029 + period: 2029 + absolute_error_margin: 0.01 + input: + employment_income_before_lsr: 40_000 + pension_contributions_via_salary_sacrifice: 5_000 + gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 + output: + # 5,000 - 2,000 cap + salary_sacrifice_returned_to_income: 3_000 + pension_contributions_via_salary_sacrifice_adjusted: 2_000 + employment_income: 43_000 + +- name: A whole-bonus sacrifice above the remaining salary returns its full excess, 2030 + period: 2030 + absolute_error_margin: 0.01 + input: + employment_income_before_lsr: 40_000 + pension_contributions_via_salary_sacrifice: 50_000 + gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 + output: + # 50,000 - 2,000 cap + salary_sacrifice_returned_to_income: 48_000 + pension_contributions_via_salary_sacrifice_adjusted: 2_000 + employment_income: 88_000 + +- name: Low pay with a large sacrifice still returns the excess over the cap, 2029 + period: 2029 + absolute_error_margin: 0.01 + input: + employment_income_before_lsr: 1_500 + pension_contributions_via_salary_sacrifice: 30_000 + gov.contrib.behavioral_responses.salary_sacrifice_broad_base_haircut_rate: 0 + output: + salary_sacrifice_returned_to_income: 28_000 + pension_contributions_via_salary_sacrifice_adjusted: 2_000 + employment_income: 29_500 + +- name: Nothing returns before the cap starts, 2028 + period: 2028 + absolute_error_margin: 0.01 + input: + employment_income_before_lsr: 40_000 + pension_contributions_via_salary_sacrifice: 5_000 + output: + salary_sacrifice_returned_to_income: 0 + pension_contributions_via_salary_sacrifice_adjusted: 5_000 + employment_income: 40_000 diff --git a/policyengine_uk/tests/test_salary_sacrifice_needs_pay_properties.py b/policyengine_uk/tests/test_salary_sacrifice_needs_pay_properties.py new file mode 100644 index 0000000000..043e485ff6 --- /dev/null +++ b/policyengine_uk/tests/test_salary_sacrifice_needs_pay_properties.py @@ -0,0 +1,179 @@ +"""Property-based tests: only people with pay have a salary sacrifice. + +From 6 April 2029 the salary sacrifice above the £2,000 cap returns to +employment income (salary_sacrifice_returned_to_income). Only people with pay +have a salary sacrifice (pension_contributions_via_salary_sacrifice_from_pay), +so an input sacrifice with no pay behind it cannot create pay. A sacrifice +above the pay that remains (a whole bonus, say) is kept in full. + +Invariants, for any generated pay and salary sacrifice, in a year before the +cap (2028) and two years under it (2029, 2030): + +1. Definition: the sacrifice from pay is max(sacrifice, 0) with pay, else 0. +2. Conservation: the capped sacrifice plus the amount returned to income + equals the sacrifice from pay. +3. No pay, no gain: a person without pay has no employment income, nothing + returned and no National Insurance charge on a returned amount, whatever + their sacrifice input. +4. Nothing is returned before the cap starts. +5. Accounting: employment income equals pay plus the amount returned plus + the broad-base haircut. +6. Unchanged with pay: for a person with pay, the amount returned is the + sacrifice above the cap, as before this condition. +7. Monotonicity: the amount returned never falls when the sacrifice or the + pay rises. + +The simulations go through policyengine_uk.Simulation, which moves an +employment_income input to employment_income_before_lsr as it does for a +dataset or a household calculation. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEARS = [2028, 2029, 2030] +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +# Zero pay and a zero sacrifice are the cases the limit exists for, so they +# are drawn often. +pay = st.one_of( + st.just(0.0), st.floats(0, 200_000, allow_nan=False, allow_infinity=False) +) +sacrifice = st.one_of( + st.just(0.0), st.floats(0, 100_000, allow_nan=False, allow_infinity=False) +) +increase = st.floats(0, 50_000, allow_nan=False, allow_infinity=False) +people = st.lists(st.tuples(pay, sacrifice, increase, increase), min_size=1, max_size=8) + + +def _simulate(pays, sacrifices): + """One single-adult household per (pay, sacrifice) pair.""" + names = [f"person_{i}" for i in range(len(pays))] + situation = { + "people": { + name: { + "age": {str(year): 40 for year in YEARS}, + "employment_income": {str(year): pays[i] for year in YEARS}, + "pension_contributions_via_salary_sacrifice": { + str(year): sacrifices[i] for year in YEARS + }, + } + for i, name in enumerate(names) + }, + "benunits": {f"benunit_{i}": {"members": [n]} for i, n in enumerate(names)}, + "households": {f"household_{i}": {"members": [n]} for i, n in enumerate(names)}, + } + return Simulation(situation=situation) + + +def _values(simulation, variable, year): + return np.asarray(simulation.calculate(variable, year), dtype=float) + + +def _cap(simulation, year): + parameters = simulation.tax_benefit_system.parameters(year) + return parameters.gov.hmrc.national_insurance.salary_sacrifice_pension_cap + + +@PROPERTY_SETTINGS +@given(people) +def test_only_people_with_pay_have_a_salary_sacrifice(rows): + pays = np.array([row[0] for row in rows]) + sacrifices = np.array([row[1] for row in rows]) + drawn_pays = pays + simulation = _simulate(drawn_pays, sacrifices) + for year in YEARS: + # The pay the model stores (float32: tiny draws round to zero). It + # must be the pay put in: Simulation moves employment_income there. + pays = _values(simulation, "employment_income_before_lsr", year) + np.testing.assert_allclose(pays, drawn_pays, rtol=1e-6, atol=1e-2) + from_pay = _values( + simulation, "pension_contributions_via_salary_sacrifice_from_pay", year + ) + adjusted = _values( + simulation, "pension_contributions_via_salary_sacrifice_adjusted", year + ) + returned = _values(simulation, "salary_sacrifice_returned_to_income", year) + employment_income = _values(simulation, "employment_income", year) + haircut = _values(simulation, "salary_sacrifice_broad_base_haircut", year) + cap = _cap(simulation, year) + + # 1. Definition. + np.testing.assert_allclose( + from_pay, np.where(pays > 0, sacrifices, 0), rtol=1e-6, atol=1e-2 + ) + assert np.all(returned >= 0) + # 2. Conservation. + np.testing.assert_allclose(adjusted + returned, from_pay, rtol=1e-6, atol=1e-2) + # 3. No pay, no gain. + unpaid = pays <= 0 + assert np.all(employment_income[unpaid] == 0) + assert np.all(returned[unpaid] == 0) + for charge in ( + "salary_sacrifice_pension_ni_employee", + "salary_sacrifice_pension_ni_employer", + ): + assert np.all(_values(simulation, charge, year)[unpaid] == 0) + # 4. Nothing is returned before the cap starts. + if np.isinf(cap): + assert np.all(returned == 0) + # 5. Accounting. + np.testing.assert_allclose( + employment_income, pays + returned + haircut, rtol=1e-6, atol=1e-2 + ) + # 6. Unchanged with pay. + paid = pays > 0 + if not np.isinf(cap): + np.testing.assert_allclose( + returned[paid], + np.maximum(sacrifices[paid] - cap, 0), + rtol=1e-6, + atol=1e-2, + ) + + +@PROPERTY_SETTINGS +@given(people) +def test_returned_income_never_falls_with_sacrifice_or_pay(rows): + n = len(rows) + pays = np.array([row[0] for row in rows]) + sacrifices = np.array([row[1] for row in rows]) + more_sacrifice = np.array([row[2] for row in rows]) + more_pay = np.array([row[3] for row in rows]) + # The same people three times: as drawn, with a larger sacrifice, and + # with more pay. + simulation = _simulate( + np.concatenate([pays, pays, pays + more_pay]), + np.concatenate([sacrifices, sacrifices + more_sacrifice, sacrifices]), + ) + for year in YEARS: + returned = _values(simulation, "salary_sacrifice_returned_to_income", year) + base, larger_sacrifice, larger_pay = ( + returned[:n], + returned[n : 2 * n], + returned[2 * n :], + ) + assert np.all(larger_sacrifice >= base - 1e-2) + assert np.all(larger_pay >= base - 1e-2) + + +def test_pay_set_after_construction_keeps_the_sacrifice(): + """Branches that reset employment_income keep the pay the sacrifice needs. + + The marginal tax rate branches and the participation response set + employment_income on a simulation built with pay; the original pay stays + in employment_income_before_lsr, so the sacrifice is still recognised. + """ + simulation = _simulate(np.array([30_000.0]), np.array([6_000.0])) + simulation.set_input("employment_income", 2030, np.array([0.0])) + assert _values( + simulation, "pension_contributions_via_salary_sacrifice_from_pay", 2030 + ) == [6_000] + assert _values(simulation, "salary_sacrifice_returned_to_income", 2030) == [4_000] diff --git a/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py b/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py index ec101cce42..3334cfa70c 100644 --- a/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py +++ b/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_adjusted.py @@ -5,7 +5,8 @@ class pension_contributions_via_salary_sacrifice_adjusted(Variable): label = "Adjusted salary sacrifice pension contributions (capped)" documentation = ( "The actual amount of salary sacrifice pension contributions after " - "applying the cap. Contributions above the cap are redirected to " + "counting them only for people with pay and applying the cap. " + "Contributions above the cap are redirected to " "regular employee pension contributions and subject to NI." ) entity = Person @@ -15,7 +16,9 @@ class pension_contributions_via_salary_sacrifice_adjusted(Variable): reference = "https://policyengine.org/uk/research/salary-sacrifice-cap" def formula(person, period, parameters): - intended_ss = person("pension_contributions_via_salary_sacrifice", period) + intended_ss = person( + "pension_contributions_via_salary_sacrifice_from_pay", period + ) cap = parameters( period ).gov.hmrc.national_insurance.salary_sacrifice_pension_cap diff --git a/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_from_pay.py b/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_from_pay.py new file mode 100644 index 0000000000..62cb1a357e --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/pensions/pension_contributions_via_salary_sacrifice_from_pay.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class pension_contributions_via_salary_sacrifice_from_pay(Variable): + label = "Salary sacrifice pension contributions of people with pay" + documentation = ( + "Salary sacrifice pension contributions, counted only for people with " + "pay. A salary sacrifice is pay given up in exchange for an employer " + "pension contribution, so a person without pay (employment income " + "before labour supply responses, which policyengine_uk.Simulation fills " + "from an employment_income input) has nothing to sacrifice. The " + "sacrifice is not limited to the pay that remains: an employee may " + "sacrifice, for example, a whole bonus, as long as their cash pay stays " + "above the National Minimum Wage. The cap on salary sacrifice returns " + "the excess over the cap to employment income, so without this " + "condition an input sacrifice with no pay behind it would create pay." + ) + entity = Person + definition_period = YEAR + value_type = float + unit = GBP + reference = "https://www.gov.uk/guidance/salary-sacrifice-and-the-effects-on-paye" + + def formula(person, period, parameters): + sacrifice = person("pension_contributions_via_salary_sacrifice", period) + pay = person("employment_income_before_lsr", period) + return where(pay > 0, max_(sacrifice, 0), 0) diff --git a/policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py b/policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py index 82bca27858..72c7ce0335 100644 --- a/policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py +++ b/policyengine_uk/variables/gov/hmrc/pensions/salary_sacrifice_returned_to_income.py @@ -12,6 +12,10 @@ class salary_sacrifice_returned_to_income(Variable): "The full excess is redirected - the employer cost increase is handled via " "the broad-base haircut (salary_sacrifice_broad_base_haircut) which reduces " "ALL workers' employment income by ~0.16%, not just affected workers." + "\n\n" + "Only people with pay have a salary sacrifice " + "(pension_contributions_via_salary_sacrifice_from_pay), so nothing is " + "returned to a person without pay." ) entity = Person definition_period = YEAR @@ -20,7 +24,9 @@ class salary_sacrifice_returned_to_income(Variable): reference = "https://policyengine.org/uk/research/uk-salary-sacrifice-cap" def formula(person, period, parameters): - intended_ss = person("pension_contributions_via_salary_sacrifice", period) + intended_ss = person( + "pension_contributions_via_salary_sacrifice_from_pay", period + ) cap = parameters( period ).gov.hmrc.national_insurance.salary_sacrifice_pension_cap