Repository navigation
Zero-rate employer NI for under-21s and apprentices under 25 - #2505
vahid-ahmadi wants to merge 12 commits into
Conversation
Apply SSCBA 1992 s.9A (0% up to the upper secondary threshold for earners under 21) and s.9B (0% up to the apprentice upper secondary threshold for apprentices under 25) in ni_class_1_employer, with the thresholds from SI 2001/1004 reg 10(e)-(f) and kept equal to the UEL after 2030-31. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 35 (thresholds in Notes
Moves flagged for review
Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileNo household's net income changes in any year. No change in any year: household income totals; population; poverty rates; people in poverty; inequality; mean household net income in each side's own deciles. Checks failing on both base and head (1)
Counted-once checkEach of 86 components was raised by £1 per unit in 2026-27, and every head of its totals was read household by household: its own head must rise by exactly £1, no other tax head may move, no other benefit may rise, and the model's own total must move by what its heads moved. A head that breaks a rule is measured again twice: with every other head held at its baseline value, and with every other head held at its value after the first £1 while the component rises again. It fails where either still shows the response. 0 failed; 0 were skipped.
Provenance
Dataset: Receipts by tax head: head levels
Spending by benefit and service: head levels
Caseloads: head levels
Government balance, each component counted once: head levels
Model totals: head levels
Household income totals: head levels
Population: head levels
Poverty rates: head levels
People in poverty: head levels
Inequality: head levels
Mean household net income in each side's own deciles: head levels
Standard reform costings (change in gov_balance): head levels
Standard reforms: change in each reform's target: head levels
|
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed a66fd641d4fc02d9eb4f4875d8bda113e0c59e8a.
The main under-21/apprentice NI formula, dated UST/AUST parameters and indexation pass their focused checks. The downstream cost limitation is material and keeps this review at REQUEST_CHANGES.
C1 — Apply the zero-rate schedule in employer-cost and salary-sacrifice consumers. employer_ni_fixed_employer_cost_change.py:49–75, baseline_employer_cost.py:39–54, adjusted_employer_cost.py:31–42 and salary_sacrifice_pension_ni_employer.py:21–27 still calculate NI using only the ordinary secondary threshold.
Executed 2026 case: an employee aged 20 with £30,000 pay, no pension contributions and the default fixed-cost incidence. Raising employer NI from 15% to 16% should leave pay unchanged because NI is zero below UST under SSCBA 1992 s.9A. The head instead cuts pay by £215.52 to £29,784.48, while actual employer NI remains £0. An apprentice aged 24 has the same error under s.9B; age-21/25 ordinary-rate controls behave as expected.
The consistent 2029 salary-sacrifice case also shows the same duplicated-schedule defect: £3,000 returned to pay leaves total pay £33,000 below UST/AUST, yet the helper charges £450 extra employer NI and reports adjusted employer cost £39,200 against actual £35,000 (including an explicit £2,000 remaining employer pension contribution). Count this as one shared consumer defect. Please apply the applicable age/apprentice schedule in these calculations and cover the wage and cost results.
Validation: 212 focused tests passed, including Hypothesis properties and age/earnings/indexation/salary-sacrifice tests; separate source-backed diagnostics establish the remaining defect. The existing axiom: needed records the outstanding parity work.
Review status: COMPLETE. One confirmed critical finding (C1). Hosted CI is separate and was not fully complete in the captured check set.
…fice variables Regressions for review C1 on #2505: an earner aged 20 and an apprentice aged 24 on £30,000 keep their pay when the employer rate rises to 16% (2026), and pay returned from a capped salary sacrifice below the UST/AUST bears no employer NI (2029). Ages 21 and 25 are ordinary-rate controls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ifice variables baseline_employer_cost, adjusted_employer_cost, employer_ni_fixed_employer_cost_change and salary_sacrifice_pension_ni_employer recomputed employer NI from the ordinary secondary threshold, so a rate rise cut the pay of a 20-year-old on £30,000 who pays no employer NI (review C1 on #2505). They and ni_class_1_employer now take the threshold from one helper, secondary_class_1_threshold (SSCBA 1992 ss.9, 9A, 9B). The salary sacrifice measure charges the returned pay as the top slice of the earnings ni_class_1_employer charges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@juaristi22 thanks, C1 is fixed in 5a1846c (tests first in af62664).
The PR stays in draft. 🤖 Generated with Claude Code |
Resolve conflicts with the NI canonical scale refactor (#222) and the NIC-only salary sacrifice excess (#2454), keeping main's behaviour: - The zero-rate helper and its five consumers read the secondary threshold and rate from class_1.employer_scale; the helper returns float32 so the ordinary case stays bit-identical to the scalar formula (test_ni_scale_equivalence). - Threshold indexation lists the UST and AUST alongside main's canonical scale paths (ten indexed thresholds); the registration test counts them. - The salary sacrifice zero-rate tests use main's semantics: the excess over the cap enters NIC earnings only and is not returned to pay. - uv.lock is main's; the branch's lockfile churn was unintended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
UK hub (Claude session, for Max). @juaristi22, Vahid's fix for C1 is in @vahid-ahmadi, this is still a draft. Mark it ready when you're done, or say so here and the hub will. On María's approval, the hub lands it through the merge queue before the Budget. |
|
UK hub (Claude session, for Max): independent review (GPT-6.1 Sol) at María's three C1 cases are fixed. On the 2026 cases (age 20, and apprentice age 24, both on £30,000 with employer NI raised from 15% to 16%), pay stays at £30,000, employer NI is £0 and employer cost is unchanged. The 2029 salary-sacrifice case adds no extra employer NI. Every consumer uses [Blocking] The fixed-cost shortcut misses statutory pay and lowered thresholds.
The guard predates this PR, but raising its threshold from £5,000 to £50,270 exposes these earners. Fix: solve the fixed-cost equation piecewise, and add regressions for the statutory-pay and threshold-crossing cases for both groups. Nonblocking:
@vahid-ahmadi, can you take the blocking item? With it fixed and María's re-review done, the hub lands this before the Budget. |
The shortcut compared pay alone with the baseline threshold and used the above-threshold solution everywhere, so it missed statutory pay that takes NIC earnings over the UST/AUST and reforms that move a threshold across a person's earnings. Solve c_b = x + r * max(x - k, 0) on each side of the reformed threshold instead, with regressions for both zero-rated groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@juaristi22 a correction to my 20 Oct reply. Main's #2454 changed salary sacrifice, so the £3,000 excess over the cap now goes into NIC earnings only and is not added back to pay. Your 2029 case at age 20 (or apprentice 24) now has pay of £30,000, NIC earnings of £33,000, £0 extra employer NI and adjusted employer cost of £32,000 (£30,000 pay plus £2,000 employer pension), not £35,000. At age 21 the extra employer NI is still £450. The tests and PR body already use £32,000; only my reply was out of date. @MaxGhenis the blocking item is fixed in 1b17148.
All 6 cases fail at f022ab6 and pass now. All 15 tests in @juaristi22 could you re-review at 1b17148? It stays in draft. 🤖 Generated with Claude Code |
|
UK hub (Claude session, for Max): round 2 of independent review (GPT-6.1 Sol) at Fixed. [Blocking 1] Penny precision and the conservation property aren't tested yet.
The reviewer's hand-calculated expectations (2026 relief threshold £966.73 × 52 = £50,269.96, full fixed-cost incidence, no pension unless stated):
[Blocking 2] CI didn't finish at this head. All five Python shards were cancelled (runner capacity, not a test failure), so Test / Result failed and the Aggregate impact report didn't run. Your next push re-runs them. Round 1's impact numbers don't cover this head. @vahid-ahmadi, can you take both? Once they're in and María has re-reviewed, the hub lands this before the Budget. |
- Use the stored 2026 relief threshold (£966.73 x 52 = £50,269.96) and penny tolerances in the statutory-pay and raised-threshold regressions, which allowed £0.10. - Add María's original salary sacrifice case: £5,000 of total employer pension with the £5,000 sacrifice, employer cost £35,000 for a zero-rated earner. - Add a Hypothesis property over mixed under-21, apprentice and ordinary earners with statutory pay and employer pensions, under random employer rate, secondary threshold and UST/AUST reforms: reformed employer cost equals baseline cost whenever reformed pay is positive, and pay floors at zero (cost at least baseline) when statutory pay and pensions alone cost more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
UK hub (Claude session, for Max): pushed the round 2 test fixes at
No local runs (the host is reserved), so CI is the check; the push also re-runs the cancelled shards and the Aggregate impact report. The hub gets a delta review and asks María to re-review. |
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed 4f47cbf4dbcd67a0e34d2e76b4e9287ae2b4e64b.
The original below-threshold examples are fixed. In particular, £30,000 cash pay plus £2,000 actual employer pension now costs £32,000, with no employer NI. The shared threshold helper and statutory-pay/reformed-threshold regressions improve the other cases too.
C1 remains open for the NIC-only salary-sacrifice excess. baseline_employer_cost.py:29–34, adjusted_employer_cost.py:26–28 and employer_ni_fixed_employer_cost_change.py:36–41 reconstruct NI earnings without salary_sacrifice_pension_ni_excess, although actual Class 1 NI includes it.
Executed consistent 2029 case: age 20, £49,000 cash pay, £5,000 intended sacrifice, £2,000 actual employer pension, no broad-base haircut. There is no returned cash; the £3,000 NIC-only excess makes assessed earnings £52,000. Actual employer NI is £259.51 and employer_cost £51,259.51, while baseline and adjusted costs report £51,000. Raising the employer rate from 15% to 16% with full employee incidence leaves pay unchanged and increases actual cost by £17.30, instead of holding cost fixed. An above-UST £60,000 control also shows the omitted excess.
Include the excess in NI assessment and the fixed-cost equation, while keeping it out of cash pay and actual pension expenditure. Earnings above UST/AUST remain subject to the ordinary secondary rate under SSCBA s.9A and s.9B.
The latest commit changes two tests only; implementation and dependency Git objects are identical to the executed reproduction. The new property cases stop at 2028 and do not exercise the 2029 excess. 188 focused checks passed at the identical implementation; those results and the diagnostic are retained with hash provenance. Current required CI passes. Review status: COMPLETE; one confirmed critical finding remains.
María's re-review of 4f47cbf (C1): baseline_employer_cost, adjusted_employer_cost and employer_ni_fixed_employer_cost_change rebuilt NIC earnings without salary_sacrifice_pension_ni_excess, although ni_class_1_employer charges it. From 2029, a 15% to 16% rate rise left pay unchanged for an earner aged 20 with £49,000 pay and a £5,000 sacrifice, and actual cost rose by £17.30. The equations also left out salary_sacrifice_returned_to_income and salary_sacrifice_broad_base_haircut, which employment_income adds. So from 2029, with the default 0.16% haircut, the costs missed every worker's haircut. A shared helper returns each schedule's pay terms and NIC-only excess. The baseline cost uses the baseline simulation's terms, and the reform uses its own. The solver's kink moves down by the excess. Regressions: - María's case to the penny: pay £48,985.09, cost £51,259.51; - an above-threshold control; - a default-haircut case. Each also checks against employer_cost in both simulations. The Hypothesis property now covers 2029-30 with a drawn sacrifice and checks that agreement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
UK hub pushed
The host is reserved for the Budget certification, so these run in CI. I'll request your re-review once CI is green at this head. |
María's 2029 case assumes no broad-base haircut. The test removed it with a parameter reform, which changes the reformed population only: a reform's baseline simulation is built from the situation under current law, so the baseline kept the 0.16% haircut, its pay was £48,921.60 and its cost £51,169.35 rather than £51,259.51. The haircut is now a situation input of zero, which both populations share, and the test checks the baseline pay. The fixed-cost property also draws salary sacrifice returned to pay, and four explicit examples make every run cover a sacrifice above the cap in 2029 and 2030, returned pay and the zero-pay floor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts with #2363 in salary_sacrifice_pension_ni_employer: - formula: this branch's, which charges the excess above the earner's own secondary threshold (s.9, s.9A, s.9B); - defined_for = "ni_class_1_secondary_liable" and the secondary liability note from main; references from both; - YAML: main's cases, then this branch's. main's test_salary_sacrifice_ni_measure_properties.py checks invariant 4 (one band, one rate) against the plain secondary threshold, which this branch raises for earners under 21 and apprentices under 25. Its employer bands now split at each earner's own threshold, worked out from the parameters, and a known case aged 20 crosses the upper secondary threshold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
UK hub: merged main into this branch ( Round 5 approved a1cd832 (both round 4 findings fixed, CI green). Main then gained #2363, which conflicted in
One change goes beyond the conflict. Main's A delta review of the resolution is running, and CI is running at @juaristi22: your CHANGES_REQUESTED review at 4f47cbf still stands. Its C1 was implemented in 5774899. Could you re-review once CI is green? |
The "rates change within the year" world turns off the employer pension contribution exemption, which switches on the fixed-employer-cost pay change. The test removes the excess from the reformed simulation only, so that change passed employer NI on the excess into pay there, and pay differed between the simulations with and without the excess (invariant 1 failed for most known cases). Employee incidence is 0 in that world, so pay is the same in both. A known case aged 20 now has its excess between the secondary and upper secondary thresholds, so the zero-rate band of invariant 4 always sees it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rows can now be apprentices, drawn and pinned. The new known row is an apprentice aged 23 whose excess lies between the secondary threshold and the apprentice upper secondary threshold (SSCBA 1992 s.9B), so invariant 4's rate-0 employer band checks the s.9B arm on every run. Before this, no row set is_apprentice, and _own_secondary_threshold's apprentice arm was never exercised (round 7 review, P3). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
UK hub pushed Why. CI at
The model behaves as designed. The test's "all else equal" no longer held.
Checks.
@juaristi22, your C1 was fixed in |
|
@juaristi22 I checked both of your reviews again at the current head, Status at
Tests in a clean environment at
Could you re-review at 🤖 Generated with Claude Code |
Fixes #1918 (the under-21 and apprentice zero rates; the Employment Allowance is #2459)
Summary
ni_class_1_employercharged the full secondary percentage above the secondary threshold for every earner aged 16 or over. This PR applies the two age-related zero rates:Earnings above the UST or AUST bear the ordinary secondary percentage (s.9A(9)-(10), s.9B(7)). The formula takes the higher of the secondary threshold, the UST (under 21) and the AUST (relevant apprentices). The relevant-apprentice condition uses the existing
is_apprenticeinput.The schedule lives in one helper,
policyengine_uk/utils/employer_ni.py::secondary_class_1_threshold, which takes a Class 1 parameter node so baseline and reformed rules use the same function.ni_class_1_employer,baseline_employer_cost,adjusted_employer_cost,employer_ni_fixed_employer_cost_changeandsalary_sacrifice_pension_ni_employerall call it. The salary sacrifice measure now charges the excess over the cap (which, since #2454, enters NIC earnings only) as the top slice of the earningsni_class_1_employercharges (rate x (max(E - T, 0) - max(E - excess - T, 0))), so it is £0 where those earnings are zero-rated, and for an adult earner it differs from the old flat charge only where earnings are below the secondary threshold. All five read the secondary threshold and rate fromclass_1.employer_scale(the canonical NI scales from main).Found during an internal Budget-day rehearsal, where baseline employer NI for under-21 earners was higher than hand calculation.
Parameters
class_1.thresholds.upper_secondary_thresholdandclass_1.thresholds.apprentice_upper_secondary_threshold, weekly, from SI 2001/1004 reg 10(e)-(f) for each year from 2015-16 (UST) and 2016-17 (AUST). Both have equalled the upper earnings limit every year (£815, £827, £866, £892, £962, £962, £967). From 2021-22 they are stored as £50,270 / 52 = 966.73 (reg 11(3B)-(3C)), the convention the UEL file uses. 2027-28 to 2030-31 are announced policy (HMRC Budget 2025 note: "will remain aligned to the National Insurance contributions UEL at £967 per week up until the 2030 to 2031 tax year"). The AUST is 0 in 2015-16, before s.9B took effect, because processing backdates a parameter's earliest value.class_1.secondary_zero_rate_age_limits.under_21_group(21, s.9A(3)) and.relevant_apprentice(25, s.9B(2)(a)).create_threshold_indexation.pykeeps both thresholds equal to the indexed UEL from 2031-32 and lists them inINDEXED_THRESHOLDS, so dated reforms treat them as the other indexed thresholds. Keeping them aligned with the UEL is a modelling assumption, as it is for the UEL itself.Tests
ni_class_1_employer.yaml: 10 new cases. Under 21 below and above the UST (2026: £0 on £30,000; 0.15 x (60,000 - 50,270) = £1,459.50 on £60,000), age 21, apprentice aged 24 and 25, non-apprentice aged 22, 2024-25, 2015-16 (UST £815 a week, no apprentice zero rate) and 2016-17 (AUST £827 a week).test_employer_ni_zero_rate_consumers.py(review C1): an earner aged 20 and an apprentice aged 24 on £30,000 keep their pay when the employer rate rises to 16% in 2026 (baseline and adjusted employer cost £30,000); ages 21 and 25 lose (33,750 + 800) / 1.16 - 30,000 = -£215.52; a mixed-age population checks the no-change shortcut with per-person thresholds; and in 2029 a £3,000 salary sacrifice excess on £30,000 pay (NIC earnings £33,000) bears no employer NI at age 20 or apprentice 24 (employer cost £32,000 with a £2,000 employer pension contribution), and 0.15 x 3,000 = £450 at age 21.salary_sacrifice_pension_ni_employer.yamladds the same 2029 cases and a 19-year-old on £40,000 with a £6,000 sacrifice (£0, was £600).employer_ni_fixed_employer_cost_changenow solves c_b = x + r x max(x - k, 0) for NIC earnings x (pay plus statutory pay) on each side of the reformed threshold k, instead of comparing pay alone with the baseline threshold. For an earner aged 20 and an apprentice aged 24 in 2026: £49,000 pay plus £2,000 SSP with the rate raised to 16% gives pay of £48,993.71 at unchanged employer cost (£51,109.51); lowering the UST and AUST to £20,000 cuts pay of £30,000 to 33,000 / 1.15 = £28,695.65; raising them to £70,000 lifts pay of £60,000 by the whole £1,459.50 saving. On the previous head (f022ab6) these 6 cases fail; with the fix all 15 intest_employer_ni_zero_rate_consumers.pypass, as do the NI YAML tests (176) and the NI, salary sacrifice and behavioural response pytest modules (222).test_ni_class_1_age_properties.py: property 1 now checks ages 16 to 20 against r x max(e - 52 x max(ST, UST), 0) instead of skipping them, with two under-21 explicit examples.test_threshold_indexation.py: the synthetic-tree and real-tree checks cover the two new thresholds (aligned with the UEL after their last stated year), and the registration check counts ten indexed thresholds.test_ni_scale_equivalence.py.test_threshold_indexation.py(89 passed),test_dated_reform_routes.py(114 passed), the NI and salary-sacrifice property modules, and the parameter metadata tests pass.make formatis clean.Reproducer:
policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_1/ni_class_1_employer.yaml::No employer NI on an earner aged 20 below the upper secondary threshold (2026)Impact
CI's aggregate impact run at
63dbac7d1(40055160331; this head merged into maincd8972e94, certified enhanced_frs_2024_25 on policyengine-uk-data 1.58.1):gov_taxandgov_balancefall by the same amounts. No other tax head moves.employer_ni_fixed_employer_cost_change.py:88), as they do under current law.sign:vatfails on both sides, as on main.Hub fixes after the main merge (24 Oct)
e449f21b6(tests only): main's salary sacrifice measure property test (Make the salary sacrifice NI measures equal the change in Class 1 liability, and correct mis-declared units #2363) failed at the merge in its "rates change within the year" world. The test zeroes the excess only in the reformed simulation, and that world's exemption flip switches on this branch's fixed-employer-cost pay change, so pay differed between the simulations with and without the excess. That world now setsemployee_incidenceto 0, so pay is held fixed. It also adds a known row aged 20 below the upper secondary threshold.63dbac7d1(tests only): rows can be apprentices, drawn and pinned, so invariant 4's rate-0 band checks the s.9B threshold on every run.63dbac7d1. CI passes there (16 checks).Not in this PR
ni_employeris net of it (split from #1918) #2459) and the elective veterans and freeport/investment zone reliefs (NICA 2022 ss.1 and 6-7) remain unmodelled.ni_employerfalls for under-21 and apprentice earners. policyengine-uk-data calibrates to the OBR Class 1 employer NICs target using this variable, so the calibration will absorb part of the change when the data are rebuilt.axiom: TheAxiomFoundation/rulespec-uk#479 queued
🤖 Generated with Claude Code