Repository navigation
Hold UC trade tax parts in the trading loss netted world - #2849
Conversation
The netted world in test_means_tests_see_the_loss_only_through_tax held UC's tax and NI totals on earnings at their with-loss values but let the parts in respect of the trade be recomputed for the cut profit. For a loss larger than the profit beside a private pension, that moved the trade's share of the tax onto employed earnings. Hold both parts, pin the minimised family from #2832 and restate invariant 4. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LwQq6HiQgbqpFzJ8CLCXuP
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: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Standard reform costings (change in gov_balance)
The report suppresses a reform's costing and target change in these years, for the reasons below (
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 92 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 cd994ad23f01e42c90c6d760de77d221a4cac7eb. COMPLETE — no findings or material evidence gaps.
The corrected controlled comparison holds both trade tax/NI components alongside the totals, so the loss-netted world isolates the intended loss rule. It preserves the model's stated allocation convention; it does not claim that convention is uniquely required by the legislation.
Validation: 16 focused tests passed. Diagnostics reproduced £32,319.60 for the actual and corrected worlds versus £30,519.60 under the old oracle, and matched all four asserted UC outputs across five families, including four positive final UC awards. The limited UC/ITA source claims were independently checked against five validated official originals. The initial diagnostic-only output-name error was corrected without repeating the passing test suite.
Exact-head CI is green. Approval is deferred because the PR remains a draft; the completed review can be retained when readiness and current-head CI are checked later.
juaristi22
left a comment
There was a problem hiding this comment.
Approval-only follow-up at unchanged cd994ad23f01e42c90c6d760de77d221a4cac7eb: COMPLETE, no findings or material gaps.
The previously completed code/source review, 16 passing tests and qualified final-UC diagnostics remain valid: head, body, dependencies, runtime and saved artifacts are unchanged. No tests were rerun. The controlled tax-allocation convention remains distinguished from a uniquely statutory allocation.
The PR is now ready; applicable exact-head required checks, activity and source evidence were refreshed before approval.
Fixes #2832
Reproducer: policyengine_uk/tests/test_trading_loss_properties.py::test_means_tests_see_the_loss_only_through_tax
axiom: n/a: test-only change; no rule or parameter value changes
What the code does now
This PR changes no model code.
uc_individual_earned_income_before_miftakesuc_income_tax_on_tradeanduc_national_insurance_on_tradeoff the reg 57(2) trade total before the nil floor, and takes the rest ofuc_income_tax_on_earningsanduc_national_insurance_on_earningsoff employed earnings.uc_income_tax_on_tradetakesuc_income_tax_on_earnings_slicewith the trade, less the same slice without it, and caps the difference atuc_income_tax_on_earnings. The slice takes earnings as the lowest slice of non-savings income, and counts as other incomenet_income, less the exclusions, less the earnings. Trade loss relief lowersnet_incomebut not earnings, so the slice takes the relief off the other income above earnings first.test_means_tests_see_the_loss_only_through_taxcompares UC with the loss against a netted world: the same family with the profit cut bymin(loss, profit)and the tax and NI deductions held at their with-loss values. Before this change the netted world held the totals on earnings but let the formula recompute the parts in respect of the trade for the cut profit. Where the loss exceeds the profit, that moved the trade's share of the tax from the trade total, where the reg 57(2) step 3 nil floor absorbs it, onto employed earnings. The netted world now holdsuc_income_tax_on_tradeanduc_national_insurance_on_tradetoo, so it holds every tax and NI deduction the UC formula reads.The #2832 family
One person aged 18 in London in 2026: wages £40,000, profit £9,000, private pension £20,000, trading loss £16,000.
uc_income_tax_on_earningsuc_income_tax_on_tradeuc_national_insurance_on_earningsuc_national_insurance_on_tradeuc_earned_incomeRelief of £16,000 leaves net income £53,000 and taxable income £40,430. The slice puts earnings of £49,000 lowest, which leaves £4,000 of other income, so the tax on earnings comes to (40,430 - 4,000) x 20% = £7,286. Without the trade, other income comes to £13,000 and the tax to 27,430 x 20% = £5,486, so the trade bears £1,800. Class 1 comes to 27,430 x 8% = £2,194.40. With the loss, self-employed earnings come to max(0, 9,000 - 9,000 - 1,800) = 0 and employed earnings to 40,000 - 5,486 - 2,194.40 = £32,319.60. The netted world before this change recomputed the trade's part as nil, so all £7,286 came off employed earnings: £30,519.60.
The law
None of these says which non-savings income a trade loss relieves when UC splits one tax bill between the employment and the trade. Employment, trade and pension income share the same rate bands, so s. 25(2) does not choose between them.
Readings weighed
uc_earned_incomewith the loss, for the #2832 family (loss £16,000, which exceeds the profit) and the same family with a loss of £4,000:The old netted world matches no reading for the #2832 family. Readings A and B agree there, because the step 3 nil floor absorbs the £1,800 that reading A puts on the trade. They differ by £800 (20% of the loss) for the £4,000 family. These provisions leave both readings defensible, so this change follows the convention that
uc_income_tax_on_earnings_slicestates inpolicyengine_uk/utils/uc_income_tax.py(earnings form the lowest slice, so tax on other income never comes off earnings), fixes the test and leaves the model alone. A comment on #2832 sets out reading B, its households and what it would change.Invariant
Invariant 4 in the test's docstring now says that UC, its benefit cap earnings test and the capped award with the loss equal the same family with the other trades' profits cut by the loss and every tax and NI deduction the UC formula reads held at its with-loss value: the totals on earnings and the parts of them in respect of the trade. The person pays those deductions with the loss, and the parts record the model's split of that payment between the employment and the trade; no loss rule sets them.
Tests
@exampleontest_means_tests_see_the_loss_only_through_taxpins the Trading-loss property test fails when a loss wipes out a profit beside a private pension (UC earned income) #2832 family, with the arithmetic in a comment.test_trading_loss_properties.py: 12 passed, both on this branch and run against currentmain's code.test_uc_earned_income_before_work_allowance_properties.py: 4 passed.policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/income: 174 passed.policyengine_uk/tests/code_health: 3,089 passed. Ruff format, ruff check, ruff F811/F821 andgit diff --check: clean.main, the test with CI's settings butderandomizeoff fails for 1 of 20 Hypothesis seeds (seed 2584); with this change, all 20 pass. With CI's own settings (derandomize=True) the seed has no effect andmainpasses.main's code: holding only the totals fails the pinned example (uc_earned_income32,319.60 against 30,519.60); leaving the profit uncut in the netted world fails the pinned example (32,319.60 against 39,519.60) and four other explicit examples.derandomizeoff, otherwise CI's settings), 1,000 generated families in all, againstmain's code with this change. All passed; no new counterexample.🤖 Generated with Claude Code