Charge employer NI on employees over state pension age - #1895
Merged
Merged
Conversation
ni_class_1_employer was gated on ni_liable (over 16 and under state pension age), so secondary Class 1 was zero for anyone at or above state pension age. SSCBA 1992 s.6(3) ends only primary contributions at pensionable age, "without prejudice to any liability to pay secondary Class 1 contributions". Secondary contributions are due on earners over 16 with no upper age (s.6(1)(b); HMRC NIM36001). Add ni_class_1_secondary_liable (over 16) and gate ni_class_1_employer on it. ni_liable keeps gating primary Class 1, Class 2 and Class 4, and its documentation now cites the provision behind each upper age. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SSCBA 1992 s.9A sets a 0% secondary rate for earners under 21 up to the upper secondary threshold, and the model does not implement it, so the property no longer presents the model's rate for ages 16 to 20 as the law. Pin the #1894 case (aged 70 on £40,000 in 2026) as an explicit example so every run exercises it. Addresses the independent review. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two boundary cases test how primary and secondary Class 1 split at state pension age, not how that age is derived. Setting is_SP_age as an input keeps them valid under #1899, which derives it from date of birth (a 66-year-old in 2026-27 is then usually over it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exact s.9 comparison uses Class 1 earnings alone, which is right only because the test people have no employer pension contributions. Also say "at every age from 21" rather than "at every age". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator
Author
|
Merged at 700f088 (head 2022615). This fix was requested directly: charge secondary Class 1 on employees over state pension age, per SSCBA 1992 s.6(3). Gates at merge:
Follow-ups:
|
This was referenced Sep 30, 2026
MaxGhenis
added a commit
that referenced
this pull request
Oct 1, 2026
#1895 described ni_liable as covering primary Class 1, Class 2 and Class 4. Class 4 now uses ni_class_4_liable (status at 6 April, SI 2001/1004 reg 91(a)), so ni_liable covers primary Class 1 and Class 2 only. The National Insurance docs page states the Class 4 age rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1894
What was wrong
ni_class_1_employerhaddefined_for = "ni_liable", andni_liableisover_16 & ~is_SP_age. So the model charged no employer (secondary Class 1) NI on anyone at or over state pension age.The law ends only the employee side at pensionable age. SSCBA 1992 s.6(3):
Secondary contributions are due on earnings of "an earner over the age of 16" (s.6(1)(b)), with no upper age. The "prescribed" exceptions (SI 2001/1004 regs 28 and 29) adjust primary liability only. HMRC NIM36001: "Employers of people over State Pension age are liable for secondary Class 1 contributions". Nothing in ss.9–9B or SI 2001/1004 exempts or reduces secondary contributions for earners over pensionable age.
Example (2026-27): an employee aged 70 earning £40,000. The statute gives 15% × (£40,000 − £96 × 52) = £5,251.20 of employer NI, the same as at age 35. The model returned £0.
Changes
ni_class_1_secondary_liable(over 16, no upper age; s.6(1)(b) and s.6(3)).ni_class_1_employeris now gated on it.ni_liableis unchanged in logic and still gatesni_class_1_employee,ni_class_2,ni_class_4andni_class_4_main. Its label and documentation now say it is the primary-side condition, and cite the upper-age provision for each class:ni_class_1_employer.yaml: seven hand-computed cases.ni_employer,total_national_insurance,national_insurance= 0 andemployer_cost.is_SP_ageset directly: below it both sides are charged; at it only the employer side is. Setting the flag directly keeps these cases valid under Set State Pension age from date of birth, including the rise to 67 #1899, which derives state pension age from date of birth.ni_class_1_secondary_liable.yaml: ages 15, 16, 35 and 70.test_ni_class_1_age_properties.py: Hypothesis property tests (below).changelog.d/1894.fixed.md.Invariants
These hold for any secondary threshold ST ≥ 0, employer rate r ≥ 0, earnings e ≥ 0 and age a. Comparisons allow float32 storage.
ni_class_1_employer= r × max(e − 52 × ST, 0), computed exactly.ni_class_1_employer=ni_class_1_employee= 0.is_SP_ageholds,ni_class_1_employee= 0.main, only people who are over 16 and at or over state pension age change, and only upward.national_insurance,income_taxandhousehold_net_incomedo not change, because employer NI is not part of household income in the baseline.Invariants 1–3 are Hypothesis property tests:
@examplepins the Employer NI is zero for employees over state pension age #1894 case (aged 70 and aged 35, both on £40,000, in 2026), so every run exercises it.Invariants 4 and 5 are asserted on every person in the microsimulation below.
Verification
main. Run againstmain's code (d2754dc):ni_class_1_employer.yamlandni_class_1_secondary_liable.yamlfail:ni_class_1_secondary_liable, whichmainlacks. That includes the two state-pension-age boundary cases. The "at state pension age" case also expects employer NI thatmainzeroes.mainare the two pre-existing 2023 cases, age 70 below the secondary threshold, and age 15.Effect on modelled employer NI (microsimulation)
This corrects the model; it is not new revenue. Employers already pay these contributions, and the model was leaving them out.
The method is an A/B on the same data:
main's rule exactly: the sameni_class_1_employerclass withdefined_for = "ni_liable", swapped in before data load.main's code directly reproduces the "before" totals bit for bit. For example, 2026ni_employeris £154,449,978,627 both ways. This branch reproduces "after".The effect depends on the dataset: +£1.45bn to +£2.58bn in 2026-27 on the two datasets below.
enhanced_frs_2024_25.h5@1.56.16(policyengine.py's default UK dataset):In every year:
gov_taxandgov_balanceequal theni_employerchange;household_net_income,national_insuranceandincome_taxchange by exactly £0.In 2026-27 the change splits by age as SPA–69 £0.66bn, 70–74 £0.64bn and 75+ £0.15bn.
enhanced_frs_2023_24.h5@1.40.3(the repo's microsimulation test fixture): +£2.47bn (2025-26), +£2.58bn (2026-27) and +£3.09bn (2030-31), with 0.90–0.97m workers affected. In 2026-27 the 75+ band accounts for £0.67bn.Why the datasets differ, and a check against administrative data
No official source publishes employer NICs on over-SPA employees on their own. HMRC PAYE RTI (ONS/HMRC, Earnings and employment from PAYE RTI, September 2026,
rtisasep2026.xlsx, tables 28, 30 and 31, "65 and over", July 2026) records:The production dataset (2026-27):
So the production-dataset figure is likely held down by that dataset's over-SPA earnings, not by this rule. The dataset side is tracked as a follow-up.
Both figures are gross of the Employment Allowance (£10,500 per eligible employer in 2026-27, NICA 2014 s.1), which the model does not include.
Calibration interaction
policyengine-uk-dataand microcosm both calibrate weights to OBR's "Class 1 Employer NICs" receipts using this model'sni_employer(targets/sources/obr.py,obr/ni_employer). Current datasets were therefore fitted with over-SPA employer NI at zero. For 2025-26:obr.ni_employer@2025binding (OBR EFO March 2026 receipts, microcosmexperiments/receipts/uk-publication-spi-comparison.json) is £145.3bn;The next data build should recalibrate against the fixed model. I'm tracking that separately.
Consistency with employer-NI reform incidence
employer_ni_fixed_employer_cost_change,adjusted_employer_costandbaseline_employer_costcompute employer NI with their own formula and no age gate. Before this PR, an employer-NI reform moved over-SPA workers' pay through the incidence response, butni_employernever counted their contributions. The two now agree.Axiom parity
axiom: TheAxiomFoundation/rulespec-uk#377 queued (uk:statutes/ukpga/1992/4/6 is not encoded: s.9
secondary_class_1_contributionand the employer pilot pipeline take s.6(1)(b) payability as a free input with no age input, and s.6 is not in the pinned corpus release uk-rulespec-2026-09-07)Independent review
Round 1: Claude Opus 5.5 via Subfleet (read-only), on c63877f9. Verdict: REQUEST_CHANGES.
It confirmed:
Its findings:
@examplenow guarantees an over-SPA earner in every run.Round 2: Opus 5.5 via Subfleet, on 5dd457d. Verdict: REQUEST_CHANGES.
It confirmed all three round-1 findings are fixed, and re-checked the code, every YAML value and every microsimulation figure against the JSON.
Its findings:
Round 3: Opus 5.5 via Subfleet, on 2022615. Verdict: APPROVE.
Its non-blocking notes:
Commands run
ruff format .andruff check .: clean.policyengine-core test policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance -c policyengine_uk: 49 passed. The same run on this branch merged into Set State Pension age from date of birth, including the rise to 67 #1899's head also gives 49 passed.main's code: 9 failed, 4 passed.pytest policyengine_uk/tests/test_ni_class_1_age_properties.py: 2 passed on this branch. Againstmain's code, 1 failed and 1 passed.pytest policyengine_uk/tests/microsimulation -m microsimulation, with the fixture dataset: 23 passed locally. CI's Test job runs these tests with the dataset too, and they passed on c30fc13.make test) is green on the latest head.🤖 Generated with Claude Code