Repository navigation
Make the salary sacrifice NI measures equal the change in Class 1 liability, and correct mis-declared units - #2363
Conversation
salary_sacrifice_pension_ni_employee and _employer multiplied the whole amount returned to income by one rate. An amount crossing the upper earnings limit was charged entirely at the additional rate, an amount below the primary or secondary threshold was charged in full, and the age conditions on Class 1 were ignored. Each measure is now the complete Class 1 liability on earnings less the liability on the same earnings without the amount returned. They remain reporting measures: no tax total reads them. Refs #2361 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ence Property test: each measure equals the complete Class 1 liability of a simulation with the amount returned less that of the same people with nothing returned, under current law and in a world where the employee rates and upper earnings limit change within a year. Also bounds, exact zeros, the single-rate case and monotonicity. Refs #2361 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shareholding and consumption_shareholding return a share of a national total but declared pounds; free_tv_licence_value returns pounds but declared a ratio; seven true/false variables declared a unit. The unit also sets the default quantity type in policyengine-core: a float with unit "/1" is a stock, any other float a flow, and a yearly flow read for a month is divided by twelve. So a month's read of a share was a twelfth of the share, and a month's read of the free TV licence value was the whole year's. Every reader of these variables in the model reads them for a year, so no yearly value changes. Refs #2362 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@MaxGhenis a heads-up on merge order with #2462 (salary-sacrifice cap within National Insurance). #2462 returns only the NI excess over the cap to earnings, through a new The suggestion in the #2462 review is to land #2462 first, then point these measures at 🤖 Generated with Claude Code |
|
Adoption hand-off for the UK hub Head: Changed: resolved both NI measure conflicts and employer YAML; both measures read Tests: 0 runtime tests run. Changed Python lint passed; repository-wide Open items: run the queued focused YAML/property/unit tests with |
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
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: receipts by tax head; government balance, each component counted once; model totals; 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 84 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
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces test_ni_reporting_readers_retain_their_existing_approximation expected £560 of employee NI (£28,000 at the 2% additional rate) for an 80-year-old with £160,000 cash wages and a £30,000 sacrifice. Since this PR the measures are the change in Class 1 liability, and primary contributions end at pensionable age (SSCBA 1992 s.6(3)), so the employee measure is £0; the employer measure is £28,000 at 15%, £4,200. The test now checks both measures against the difference in ni_class_1_employee and ni_class_1_employer between the capped and uncapped simulations for every case, and pins the 80-year-old's £0 and £4,200. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Backlog F hand-off (UK hub build job, 2026-10-23) Head: Change. This fixes the blocking finding in the tax3 review (
The obsolete approximation commentary is gone, along with the "approximate reporting helper" comment in the curve test. No formula changed. Tests at 849c30d (local, foreground,
Hypothesis shrinking was off for the property runs, with the same examples. Ruff format check passes. Impact: the aggregate impact report at Open: CI at this head and the hub's independent review. The PR is still a draft; I have not merged it or marked it ready. |
|
Merged by the UK hub (Claude session 8e19f9d5) at head
|
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>
Fixes #2361
Fixes #2362
The salary sacrifice NI reporting measures now equal the change in complete Class 1 liability caused by
salary_sacrifice_pension_ni_excess. The excess remains an employer pension contribution and enters NIC earnings only, following merged #2462 (b814f507); ordinary employment income and actual member contributions stay unchanged.salary_sacrifice_pension_ni_excess, rather than the obsoletesalary_sacrifice_returned_to_incomeroute.salary_sacrifice_capBudget measure is recomputed.The existing unit corrections are retained:
shareholdingandconsumption_shareholdingare ratios/stocks;free_tv_licence_valueis GBP/a flow; boolean flags carry no currency unit. This corrects monthly reads and metadata without changing annual values.test_salary_sacrifice_ni_only.pyno longer asserts the old single-rate approximation (£28,000 × 2% = £560 employee NI for an 80-year-old with £160,000 cash wages and a £30,000 sacrifice). It now checks both measures against the difference inni_class_1_employeeandni_class_1_employerbetween the capped and uncapped simulations for every case. It pins the 80-year-old at £0 employee (primary contributions end at pensionable age, SSCBA 1992 s.6(3)) and £4,200 employer (£28,000 at 15%).Merged current
origin/mainwithout rewriting branch history, resolved both measure conflicts and the employer YAML conflict, and adapted formulas, YAML expectations and property tests to main’s contribution scales and NIC-only earnings. In particular, £50,000 pay plus £8,000 excess crosses the upper earnings limit and contributes £176.20, rather than £640.Validation at 849c30d (main c65bced merged), local, locked venv,
-n 4:test_salary_sacrifice_ni_only.pytest_salary_sacrifice_ni_measure_properties.pyandtest_variable_units.pytest_salary_sacrifice_needs_pay_properties.pyHypothesis shrinking was off for the property runs, with the same examples.
uvx ruff format --check .passes (1,960 files).Open items: hub independent review and CI. National microsimulation totals were not run by the adoption jobs; no dataset build was started. No new changes to
simulation.pybeyond the merge from main.Reproducer:
policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employee.yaml,policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/salary_sacrifice_pension_ni_employer.yaml,policyengine_uk/tests/test_salary_sacrifice_ni_measure_properties.py::test_measures_equal_the_difference_of_complete_liabilities