Skip to content

Charge employer NI on employees over state pension age - #1895

Merged
MaxGhenis merged 4 commits into
mainfrom
fix-employer-ni-over-pension-age
Sep 30, 2026
Merged

MaxGhenis merged 4 commits into
mainfrom
fix-employer-ni-over-pension-age

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1894

What was wrong

ni_class_1_employer had defined_for = "ni_liable", and ni_liable is over_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):

(3) Except as may be prescribed, no primary Class 1 contribution shall be payable in respect of earnings paid to or for the benefit of an employed earner after he attains pensionable age, but without prejudice to any liability to pay secondary Class 1 contributions in respect of any such earnings.

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

  • New ni_class_1_secondary_liable (over 16, no upper age; s.6(1)(b) and s.6(3)). ni_class_1_employer is now gated on it.
  • ni_liable is unchanged in logic and still gates ni_class_1_employee, ni_class_2, ni_class_4 and ni_class_4_main. Its label and documentation now say it is the primary-side condition, and cite the upper-age provision for each class:
    • primary Class 1: s.6(3)
    • Class 2: s.11(7)(b)
    • Class 4: SI 2001/1004 reg 91(a)
  • Tests:
    • ni_class_1_employer.yaml: seven hand-computed cases.
      • Age 70 on £40,000: 2026 (£5,251.20) and 2024 (0.138 × (40,000 − 175 × 52) = £4,264.20). The 2026 case also checks ni_employer, total_national_insurance, national_insurance = 0 and employer_cost.
      • Age 75 above the UEL: £8,251.20. Secondary Class 1 has no upper earnings cap.
      • Age 70 below the secondary threshold: £0.
      • The state pension age boundary, with is_SP_age set 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.
      • Age 15: £0.
    • ni_class_1_secondary_liable.yaml: ages 15, 16, 35 and 70.
    • test_ni_class_1_age_properties.py: Hypothesis property tests (below).
  • Docs: the NI notebook's "How PolicyEngine computes" section now states the age condition for each side of Class 1. The edited cell is markdown.
  • 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.

  1. Secondary Class 1 continues past state pension age. At every age from 21, including at and over state pension age, ni_class_1_employer = r × max(e − 52 × ST, 0), computed exactly.
    • That is the s.9 charge for someone who is not an apprentice, before the elective veterans and freeport reliefs and the employer-level Employment Allowance.
    • The test people are not apprentices and have no employer pension contributions, so e is their Class 1 earnings.
    • Ages 16–20 are not checked against it. s.9A gives them a 0% rate up to the upper secondary threshold, and the model does not implement s.9A. That gap, with s.9B and the Employment Allowance, is tracked in Employer NI ignores the under-21 and apprentice zero rates and the Employment Allowance #1918.
  2. Nothing under 16. For a < 16, ni_class_1_employer = ni_class_1_employee = 0.
  3. Primary Class 1 stops at state pension age. When is_SP_age holds, ni_class_1_employee = 0.
  4. Scope of change. Relative to main, only people who are over 16 and at or over state pension age change, and only upward.
  5. No household-side change. national_insurance, income_tax and household_net_income do not change, because employer NI is not part of household income in the baseline.

Invariants 1–3 are Hypothesis property tests:

  • 25 derandomised examples per property, up to 12 people each, ages 0–100, years 2016–2030.
  • Optional random reforms to ST (£0–£1,000 a week) and the employer rate (0–30%).
  • An explicit @example pins 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

  • The new tests fail on main. Run against main's code (d2754dc):
    • The secondary property fails on the pinned 2026 example. The primary-side property passes on both, as it should.
    • 9 of the 13 cases in ni_class_1_employer.yaml and ni_class_1_secondary_liable.yaml fail:
      • Three fail on the employer amount alone: age 70 in 2026 and 2024, and age 75 above the UEL.
      • Six fail because they check ni_class_1_secondary_liable, which main lacks. That includes the two state-pension-age boundary cases. The "at state pension age" case also expects employer NI that main zeroes.
      • The four that pass on main are the two pre-existing 2023 cases, age 70 below the secondary threshold, and age 15.
    • On this branch everything passes.
  • It composes with Set State Pension age from date of birth, including the rise to 67 #1899. I merged this branch into Set State Pension age from date of birth, including the rise to 67 #1899's head (date-of-birth state pension age) and ran the NI YAML tests: 49 passed. There, a 66-year-old in 2026 has state pension age 66 years 1 month and is over it, and pays £5,251.20 of employer NI.
  • The calculator case, 2026, £40,000:
Age Employee NI Employer NI (main) Employer NI (this PR)
15 £0 £0 £0
35 £2,194.40 £5,251.20 £5,251.20
66 (SPA 66 in the model) £0 £0 £5,251.20
70 £0 £0 £5,251.20

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:

  • "Before" rebuilds main's rule exactly: the same ni_class_1_employer class with defined_for = "ni_liable", swapped in before data load.
  • "After" is this branch.
  • Differential check. Running main's code directly reproduces the "before" totals bit for bit. For example, 2026 ni_employer is £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):

Year Employer NI before (£bn) After (£bn) Change (£bn) Workers affected (m) Mean per affected worker
2025-26 148.10 149.47 +1.38 0.85 £1,621
2026-27 154.45 155.90 +1.45 0.85 £1,702
2027-28 159.27 160.77 +1.51 0.86 £1,758
2028-29 163.72 165.28 +1.56 0.86 £1,810
2029-30 169.88 171.49 +1.62 0.86 £1,868
2030-31 175.23 176.91 +1.68 0.87 £1,929

In every year:

  • the changes in gov_tax and gov_balance equal the ni_employer change;
  • household_net_income, national_insurance and income_tax change 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:

  • 1,306,782 payrolled employees aged 65+;
  • mean pay of £2,449 a month;
  • aggregate pay of £3.20bn a month, about £38.4bn a year.

The production dataset (2026-27):

  • has 1.59m people aged 65+ with any Class 1 earnings in the year, against RTI's single-month count of 1.31m (an annual count should be higher);
  • gives them earnings of £29.6bn, less than RTI records;
  • has only £14.5bn of that over state pension age: 1.21m earners with a mean of £12.0k;
  • puts 1.12m people at exactly age 65 (against 0.62m at 64 and 0.56m at 66), carrying £15.1bn of earnings;
  • has a maximum age of 80, with 3.69m people coded as 80.

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-data and microcosm both calibrate weights to OBR's "Class 1 Employer NICs" receipts using this model's ni_employer (targets/sources/obr.py, obr/ni_employer). Current datasets were therefore fitted with over-SPA employer NI at zero. For 2025-26:

  • microcosm's recorded obr.ni_employer@2025 binding (OBR EFO March 2026 receipts, microcosm experiments/receipts/uk-publication-spi-comparison.json) is £145.3bn;
  • the production dataset models £148.1bn before this fix and £149.5bn after.

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_cost and baseline_employer_cost compute 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, but ni_employer never 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_contribution and 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:

  • the legal basis (s.6(1)(b), s.6(3), regs 28 and 29, and no over-SPA secondary exception);
  • that no other code path still zeroes employer NI over SPA;
  • every YAML expectation, re-derived by hand;
  • the A/B method and every reported figure.

Its findings:

  • Blocking: fixed in 5dd457d. The property test presented the model's rate for ages 16–20 as statute, although s.9A zero-rates them. The statutory property now covers ages 21+ and says why.
  • Non-blocking: fixed in 5dd457d. An explicit @example now guarantees an over-SPA earner in every run.
  • Non-blocking: addressed above. Present the revenue as a dataset-dependent range and as a model correction, and check the calibration interaction.

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:

  • Blocking: fixed. The Verification section still described the earlier age-67 boundary cases and a stale fail count. It now gives the rerun count above.
  • Non-blocking: fixed in 2022615. The docstring wording "at every age" is now "at every age from 21". The docstring also states the no-employer-pension-contributions premise.
  • Non-blocking: done. Open and link an issue for s.9A/s.9B: Employer NI ignores the under-21 and apprentice zero rates and the Employment Allowance #1918.
  • Non-blocking: done. Back every admin-data figure with evidence. Each figure above now traces to a saved source or script output. I dropped a back-of-envelope RTI estimate, because it depended on assumed shares.

Round 3: Opus 5.5 via Subfleet, on 2022615. Verdict: APPROVE.

  • It confirmed every round-2 finding is fixed, not renamed or suppressed.
  • It re-checked the code against s.6, the hand calculations, the Verification counts against the saved rerun log, and every admin-data figure against its source file.
  • It found no correctness errors and no unsupported claims.

Its non-blocking notes:

  • Addressed. The wording of the RTI count comparison.
  • Addressed. Save logs for the test-run claims. The targeted runs above were rerun on 2022615 with logs saved; CI covers the rest.

Commands run

  • ruff format . and ruff 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.
  • The same two new YAML files against main's code: 9 failed, 4 passed.
  • pytest policyengine_uk/tests/test_ni_class_1_age_properties.py: 2 passed on this branch. Against main'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.
  • CI (the full make test) is green on the latest head.
  • The microsimulation A/B, differential and dataset-diagnostic scripts, with their JSON outputs.

🤖 Generated with Claude Code

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>
MaxGhenis and others added 2 commits September 30, 2026 08:33
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>
@MaxGhenis
MaxGhenis merged commit 700f088 into main Sep 30, 2026
7 checks passed
@MaxGhenis
MaxGhenis deleted the fix-employer-ni-over-pension-age branch September 30, 2026 15:02
@MaxGhenis

Copy link
Copy Markdown
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:

  • Checks: all 7 passed on the latest run for 2022615, including the full Test job, which runs the microsimulation tests with the dataset.
  • Mergeability: the PR was MERGEABLE and CLEAN.
  • Reviews: there was no CHANGES_REQUESTED review. The independent Opus 5.5 reviews ran through Subfleet:
    • 20260928-150201-uk-employer-ni-review: REQUEST_CHANGES, one blocker (the statutory property covered ages 16–20).
    • 20260930-083212-uk-employer-ni-review-r2: REQUEST_CHANGES, one blocker (stale Verification text).
    • 20260930-083815-uk-employer-ni-review-r3: APPROVE on 2022615, with no correctness errors or unsupported claims. Both of its non-blocking notes were addressed in the PR body before merge.

Follow-ups:

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Employer NI is zero for employees over state pension age

1 participant