Repository navigation
Add a trading allowance claim status, so allowance reforms reach claimants without turnover data - #2765
Conversation
Add trading_allowance_claim_status, a person enum (UNKNOWN, CLAIMANT, NON_CLAIMANT; default UNKNOWN), and gov.simulation.trading_allowance_in_input_profits, the allowance a claimant's input profit already deducts: £1,000 from 2017-18, not moved by reforms to the trading allowance and not uprated. A claimant's receipts are their profit plus that amount, so the deduction at allowance A is min(profit, A - £1,000) less capital allowances, floored at nil, and a cut below £1,000 leaves their profit as reported. A non-claimant gets no allowance at any level, on trade receipts or on income not otherwise charged. Unknown keeps the current rules, so existing datasets are unchanged. Class 4 and the current-loss election read the same deduction. Issue #2764. 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)
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
|
Independent review of #2765 at
|
Non-claimants keep full relief, which needs no election (ITTOIA 2005 s. 783AE), where supplied receipts, or casual income alone with no profit, are within the allowance; partial relief stays elective. One ordinary-treatment mask replaces the two zeroing masks. A claimant's input allowance is now the allowance legislated for the year the data were measured (a dataset's first year, or the year simulated), read from the unreformed baseline parameters, so data from either side of the April 2027 raise read right. This replaces gov.simulation.trading_allowance_in_input_profits. Claimants: capital allowances are not deducted (partial relief replaces them), the status applies only to a positive profit, the deduction is computed from the allowances directly (no float32 round trip), and the claim is their election, so they take the allowance and give up current losses at any allowance above nil. A cut's effect is set by the new gov.simulation.trading_allowance_claimant_expense_share (default 1, the lower bound), which the trading allowance's description points to. Tests: YAML cases for each review point, a property test whose reference works from the Act's options, and dataset tests for the data year and the status carrying forward. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the review at
|
Without a dataset the year simulated stands in for the year the inputs were measured, even for inputs supplied for an earlier year and carried forward, so a claimant given 2025-26 inputs and simulated in 2027-28 is read as net of £3,000 and gets nothing from the raise. Keep that convention, shared with the State Pension variables, and say so in _data_year, the claim status input and the deduction's documentation, with the workaround (build a dataset for the earlier year). A test pins the behaviour so that a change of convention has to update the docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of #2765 at
|
| # | Status |
|---|---|
| 1. Non-claimants and full relief | Fixed. The three cases give £3,486, nil on both amounts, and £3,486. Casual income above the allowance still gets nothing, as partial relief needs the election. The s. 783AL election out works in both directions. |
| 2. Claimant capital allowances | Fixed. Taxable profit is £2,500, and the raise gains the full £2,000. Class 4 and relevant UK earnings follow. |
| 3. Property test | Fixed. It works through the Act's options and exercises the min() cap. It fails each of the four re-applied bugs, as does the YAML. See 2 above for what it still copies. |
| 4. Input allowance | Fixed for datasets. Data measured in 2025 get £2,000 in 2027 and 2028, and data measured in 2027 get nothing. Five forms of reform all leave the input allowance alone. Situations: see 3 above. |
| 5. Cut assumption | Fixed. The new share parameter defaults to 1, and the allowance's description points to it. See 4 above. |
| 6. No-profit claimants | Fixed. They are read as unknown. |
| 7. float32 | Fixed. £1,800.40 gives exactly 0, and a sweep of 9,460 penny-valued profits found no non-zero deduction. |
8. uses_trading_allowance |
Fixed at the input allowance and under a raise. Under a cut, see 1 above. |
| 9. Carried-forward losses | Fixed on datasets: the raise lowers tax by exactly £400, with the loss given up both ways. |
| 10-12. Texts and the single mask | Fixed. |
Checked
- The new YAML (37 cases), the data-year file (6 tests) and the property file pass, and ruff is clean.
- On CI at
6b9a638d7, the YAML, lint and smoke-import jobs pass. The Python test shards were still running. - Not covered: the full suite, and the microsimulation tests, which need the dataset token.
🤖 Generated with Claude Code
A claimant takes the allowance only while it is above nil and at least their expenses (the expense share of the input allowance); below them they deduct those expenses, which is ordinary treatment, so a supplied current loss is claimable under any cut, not only under abolition. A situation's data year is now the latest year, up to the one simulated, for which trading_allowance_claim_status was supplied (read through supplied_input_periods; the status joins SUPPLIED_INPUT_VARIABLES), so inputs carried from an earlier year keep that year's allowance. Microdata keep their first year, chosen through built_from_data. This replaces the limitation recorded in 6b9a638. The expense share is clamped to [0, 1]. A dataset still cannot declare the year its incomes were measured; that is now documented. Tests: the property test's reference derives a claimant's choice by comparing the allowance with deducting their expenses, and gains a scenario at a share of 1.5; YAML cases cover a cut with a current loss, full relief exactly at the allowance and a share above 1; the situation data-year tests now check the supplied year. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the re-review at
|
Third review of #2765 at
|
| # | Status |
|---|---|
| 1. Cut below expenses | Fixed at the default share. The round-2 case gives £3,686 at £0, £1, £500 and £999, £3,986 at £1,000, and £3,586 at £3,000. Over 175 allowance levels and 21 profiles, the only wrong-way steps are the loss jump in 3 above. Dataset and situation paths agree on all 2,856 values compared. |
| 2. Property test | Fixed. The reference compares the claimant's options rather than restating the rule, and it fails the old rule. Two YAML cases now sit exactly at the allowance, and < fails them. The situation loss test can now fail. Limit: it still shares the model's conventions (receipts as profit plus the input allowance, the clamp, a claimant's loss left out), so it can't test those. |
| 3. Situation data year | Fixed for situations whose status carries the profit's year. Every UK dataset path reads the dataset's first year: single- and multi-year datasets, h5 files, data frames, hf:// and gs:// cache hits, Microsimulation, and clones, branches, reforms and baselines. But see 1, 2 and 4 above. |
| 4. Share bounds | Fixed: 1.5 matches 1, and −0.5 matches 0. The lower clamp makes no difference for any allowance of £0 or more, which is harmless. |
| 5. Dataset measurement year | Documented, as the reply says. |
The round-1 fixes all still hold. UNKNOWN status still gives the merge base's results, dtypes included: 400 random people × 7 scenarios × 10 variables.
Checked
- Tests: the touched test files pass: the YAML (41 cases), the data-year file (8) and the property file (4).
- Mutations: each mutation the reply lists fails its tests.
- CI on
894450054: the YAML, lint, docs, smoke-import and aggregate impact jobs pass. The Python jobs were still running. - Not covered: the full suite, and the microsimulation tests, which need the dataset token.
🤖 Generated with Claude Code
_data_year checks the dataset's type, so a policyengine-core Dataset, which loads any attribute it is asked for, no longer crashes; data without years take the earliest year the profit was supplied for. In a situation the data year is now the latest year, up to the one simulated, for which self_employment_income was supplied, which replaces the status in SUPPLIED_INPUT_VARIABLES. Claimants make the same election as everyone else, against their own options: the allowance unless its saving over deducting their expenses is negative or smaller than a supplied current loss, so income tax no longer jumps as the allowance or the loss moves. Their expenses are the share of the input allowance in pennies, or their capital allowances if larger, capped at the input allowance, and their figures are computed in float64 so the two options tie exactly where the allowance equals those expenses. Tests: YAML cases for claimants with losses, the switch, capital allowances in the choice and the penny tie; data-year tests for the profit's year and the year filter; the property reference weighs a claimant's loss; a deterministic sweep of ties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the third review at
|
Fourth review of #2765 at
|
| # | Status |
|---|---|
| 1. Core datasets | Fixed. Every UK data path, and core simulations over data, read 2025 and give the 2027 claimant £2,000. The old lookup fails test_core_simulation_over_data_gets_imputations. The new rule is untested (3). |
| 2. Profit year | Fixed. A status for 2025 with a profit for 2027 gives 0; a status for 2027 with a profit for 2025 gives £2,000; a status with no year key and a 2027 profit gives 0. Side effects: registering the profit changes nothing for UNKNOWN people: 261 arrays against the merge base show no differences and identical dtypes, with labour supply responses on as well. But see 2. |
| 3. Claimants with a loss | Fixed, except where brought-forward losses cover both options (1). Over 5,355 allowance steps and 339,612 loss steps, neither income tax nor Class 4 ever moves the wrong way, and the reply's figures match. Round 1's example gains £100. The only cliffs left are the unknown and non-claimant full-relief cliffs, which the Act has. |
| 4. Filter | Fixed. Removing it fails the test at :150. |
| 5. Small items | Fixed. Penny rounding gives no flips over 121,200 claimants at exact ties. The float64 figures stay internal, so outputs are float32 or bool as before. The capital-allowance floor gives £2,700. The texts are updated, apart from those in 4. |
Checked
- Tests: the touched test files pass: the YAML (46 cases), the property file (7), the data-year file (9) and
test_data_built_imputations.py(9). - Mutations: each fix's old rule fails its tests.
- Performance:
_data_yearcosts under 0.03 ms a call on 50,000 people. - CI on
243515502:- The YAML, lint, docs, smoke-import, changelog and aggregate-impact jobs pass, and so do Python shards 1, 4, 5 and 6.
- Shards 2 and 3 hit the 1h15m job limit and were cancelled, so "Test / Result" fails.
- At
894450054those two shards took 73.0 and 74.1 minutes, and every shard is 1 to 5 minutes slower here. So the added property and tie-sweep tests have likely tipped them over. - A rerun may pass, but the margin is gone. Smaller sweeps, or another shard, would restore it.
🤖 Generated with Claude Code
A claimant keeps the allowance only where it covers their expenses, so losses brought forward that absorb both options no longer tie them onto an allowance below those expenses, which charged Class 4 more. Simulation.build_from_situation records the years the situation gave a self-employment profit for, and _data_year reads that record, so a later write of the profit (a modifier, a derivative) no longer moves a situation's data year. Clones and branches carry it. Capital allowances are rounded to pennies before they set a claimant's expenses. Tests: the property test's four hypothesis tests are now one, which runs each reformed simulation once per draw; each worker rebuilt the 15 simulations for every test before, which took Python shards 2 and 3 past the job limit. The tie sweeps use 500 claimants. New tests cover the core-data rule, a situation claimant's supplied loss, a derivative and a no-op modifier, losses brought forward under a cut and capital-allowance ties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the fourth review at
|
Fifth review of #2765 at
|
| # | Status |
|---|---|
| 1. Brought-forward losses | Fixed. The cut to £1 with £25,000 brought forward gives a deduction of 0, False and Class 4 of £445.80, and £499 and £500 now give the same Class 4. Over 10,710 allowance steps (42 profiles, three shares), nothing moves the wrong way. The third example is the merge base's own convention: an unknown-status trader with receipts gets the same +£119.94. But see 3. |
| 2. Data year at build | Fixed for derivatives and modifiers: £2,000 and £4,558.85 both before and after. The record survives clones, branches, the reform baseline, deep copies, Microsimulation and axes. But see 1. |
| 3. Test gaps | Fixed. The core-data test, the loss tests and the capital-allowance tie case each fail under their mutation. |
| 4. Texts | Partly (2). |
| CI | Green. The merged property test still checks each invariant on 25 examples. Margin still thin: the shards ran 73.3, 72.4, 63.2, 69.0, 64.6 and 74.8 minutes. The savings landed on shards 1, 3 and 5, and runner variance is bigger than the cut. Correction to the reply: at 894450054 shard 4 failed on the core-data crash. All the shards finished inside the limit there, which is probably what was meant. |
Checked
- UNKNOWN status still matches the merge base: 501 arrays × 2 comparisons, with identical dtypes.
- The earlier rounds' fixes still hold.
- The touched files pass: the YAML (48 cases), the data-year file (13 tests), the property file (4) and
test_data_built_imputations.py(9). - Not covered: the full suite (CI is green) and the microsimulation tests, which need the dataset token.
🤖 Generated with Claude Code
A situation that gave no profit at its build now reads the years a profit is set for, and build_from_populations recomputes the recorded profit years from the populations it takes. A profit set after a build that gave one still leaves the recorded year, which the docs now say. Texts: the reliefs docstring no longer claims the tax never jumps (Class 4 can, where losses brought forward absorb the saving, as for every status); the deduction's documentation says claimants keep the allowance only where it covers their expenses, ties included; the input's documentation and the changelog describe the recorded year. Tests: a YAML case pins covered claimants whose losses brought forward absorb both options to the allowance; the property test checks Class 4 profits never rise with the allowance and compares those claimants; the data-year file covers the three record paths. The property fixture and the tie sweeps change a built simulation's parameters rather than pass a reform, which skips a baseline build they never read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the fifth review at
|
Sixth review of #2765 at
|
| # | Status |
|---|---|
| 1. Build-time record | Partly. The empty-record fallback gives a bare situation its profit's year (£2,000). A profit set after a 2025 build stays at 2025, as documented. build_from_populations refreshes for situation donors. The residuals are in 1 and 2. |
| 2. Texts | Fixed. "Never jumps" is gone, ties are described correctly, and the recorded year is explained. Nits are in 3. |
| 3. Covered side of the loss fix | Fixed. The new YAML case and the near-tie mask change pin it: sending covered ties to ordinary treatment fails six YAML cases, two data-year tests and the property test. Correction to my round-5 suggestion: the new Class 4 monotonicity check doesn't catch that mutation, since Class 4 profits are flat across allowances there. It pins the below-expenses side instead. |
| 4. Fixture | Fixed. Changing a built simulation's parameters leaves parameters.baseline at law and leaks nothing between tests. The outputs match the reform route on every draw. |
Checked
- Grids: the allowance and loss grids are unchanged from round 5, cell by cell (108,360 cells, and over five million).
- UNKNOWN status: still identical to the merge base (501 arrays × 2, with identical dtypes).
- Earlier rounds: the round-1 to round-4 fixes hold.
- Tests: the touched files pass: the YAML (49 cases), the data-year file (16 tests), the property file (4) and
test_data_built_imputations.py(9). The tests that rebuild populations pass too.
🤖 Generated with Claude Code
CI: the Python shards' time limit goes from 75 to 90 minutes, and the YAML job's, which its comment ties to theirs. On 1 November the shards took 63 to 75 minutes on runners of varying speed, and the limit cancelled shards with no test failing (runs 41303444320, 41313872613 and 41326415023). A situation that gave no profit at its build records the years of the first profit set afterwards, so a later write (a derivative's, a branch's) no longer moves its data year; _data_year falls back to the input history only where no recorded year is at or before the one simulated. build_from_populations takes the donor simulation's record, or a data-built donor's first year, instead of the donor's live input history. Texts: the reliefs docstring says which figures can step where the election switches; the changelog, the deduction's and the input's documentation and the data-year tests' docstring describe the recorded year; the fixture's saving reads about a third. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the sixth review at
|
Seventh review of #2765 at
|
| # | Status |
|---|---|
| 1. Fallback path | Partly. The cases I reported are fixed. A bare situation given a 2025 profit keeps £2,000 after a derivative (0.80), branches keep £2,000, and the 2028-only case reads 2025. The new rule's own problems are in 1. |
2. build_from_populations |
Fixed. Both of my cases now give £2,000. Hook, branch, core-built and detached-population donors behave consistently, and a self-rebuild keeps its record without a stale cache. |
| 3. Texts | Fixed. The reliefs docstring's pension-relief and s. 24A cases check out, and so do the changelog, the deduction's and input's documentation, and the test docstrings. The residuals are in 1 and 5. |
| 4. CI | Fixed, and green. The six shards ran 65.8 to 71.4 minutes. The scope points are in 4. |
Checked
- Unknown status: results are identical to the merge base and to round 6 (1,002 comparisons, with identical dtypes).
- Grids: the allowance and loss grids are unchanged, cell by cell.
- Earlier rounds: the round-1 to round-5 cases still hold.
- Performance: the hook doesn't run for data builds; a 50,000-person dataset builds in the same time.
- Signature: it matches policyengine-core's
set_input. - Whole-PR checks:
- the supplied-input registry and the aggregate-impact guard;
- the branches that write the profit;
- the uprating of the new input;
- the new parameter's metadata;
- the towncrier config;
- the absence of any downstream caller of
trading_allowance_reliefs.
- Tests: the touched files pass: the data-year file (21 tests), the property file (4), the YAML (49 cases) and the CI code-health tests.
🤖 Generated with Claude Code
- A situation's data year falls back to the profit years supplied when the allowance is first calculated after construction, not at the first write, so multi-year writes don't depend on their order and later writes don't move it. Branches read their root simulation's record. - uses_trading_allowance adds a claimant's election only where both deductions come from their formulas, not inputs or neutralised, and skips the reliefs where no one is a claimant with a profit. - Share the data-built rule (data_profit_year), narrow the property test's share invariant to claimants with no current loss, and add tests for write order, modifiers, the fallback, set_model_value, self-rebuilds, data donors and pinned or neutralised deductions. - Put the YAML job's time limit back at 75 minutes with a corrected comment; the Python shards stay at 90. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the seventh review at
|
test_household_subset requires every walk up the parent_branch chain to be reviewed. branch_root reads the years a profit was supplied for and a dataset's first year, which are the same for every household, so a household subset reads the whole population's data year and no formula needs listing in WHOLE_POPULATION_VARIABLES. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Eighth review of #2765 at
|
| # | Status |
|---|---|
| 1. Write order | Fixed, apart from the documented residual. Profits set one year at a time, in any order, give no deduction and £4,895.89 in 2027, as the dict build does. So do two people set in turn. The no-op modifier's impact is 0.00. The 2028-only derivative returns 0.80, and branches read 2025. Each old rule, re-applied, fails the tests the reply names. |
| 2. Pinned deduction | Partly. The round-7 cases are fixed. Income tax is £3,686 at both £1,000 and £3,000. 2025 data run in 2027 now moves by £0.00 under the raise, where round 7 measured about +£319. The neutralised deduction keeps the loss. But see 2 and 5. |
| 3. Expense share | Fixed. A share of 0 gives £3,886, against £3,686, and the YAML pins it. Invariant 4 now holds off its grid for claimants with no current loss, and its docstring says why a loss breaks it. |
| 4. CI | The YAML job is back at 75 minutes with a corrected comment, and the PR body states the Release test's cost. The shards stay at 90; that's the maintainers' call. |
| 5. Smaller | Fixed. That covers set_model_value, the shared data-year rule, the repeated work (the reliefs now run only for claimants with a profit), both surviving mutants and the listed texts. |
Checked
- Unknown status: identical to the merge base on 5,760 people, six allowances and eight variables: 368,640 cells, with no difference in value or dtype.
- Construction:
__init__can't return with the flag still set, and nothing calculated during construction survives it. A bare build given its inputs afterwards matches the dict build for income tax, Class 4 and Universal Credit in every year from 2025 to 2030, in eight cases. - The registry: adding the two deductions changes nothing else. Its only other readers are
Simulation.calculate's record purge and the aggregate-impact guard, which reads partition components only. - The short-circuit returns a bool array in mixed, claimant-free and subset populations.
- Tests: the touched files pass: the data-year file (30 tests), the property file (4), the YAML (53 cases, through
policyengine-core test) and the code-health files (280). At6f05924a3, CI's other 11,468 tests gave 11,405 passed, 61 skipped and 2 xfailed.
🤖 Generated with Claude Code
- A situation's data year is the year simulated, as the State Pension variables read it; datasets keep their first year. This removes the record, the first-read freeze, the construction flag and the branch walk, so a situation's data year no longer depends on how, when or where its profit was written. The limit is documented: a profit given for an earlier year and carried forward is read as net of the later year's allowance. - A claimant's election weighs a pinned or neutralised deduction at the value applied, and the other deduction follows that election, so a pin to the formula's own value changes nothing and a positive pin means the allowance applies. uses_trading_allowance keeps the claimant term. - Tests: the data-year file is rewritten for the new rule, with a grid test that pinning either deduction to its own value changes nothing for every status; YAML cases for round 8's pins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of #2765 at Must None found. Should
Nit
What I checked and found fine
CI at Verdict: the engine logic is sound and UNKNOWN is neutral. Item 1 needs a fix or an explicit decision before the data side starts flagging traders, and the PR body should be brought up to date with the head. 🤖 Generated with Claude Code |
- A non-claimant with no profit is read as unknown, as a claimant is: the status describes a positive profit. So their casual income above the allowance gets partial relief again; a YAML case pins it, and the property test's reference has the same gate. The non-claimant's full relief now comes only from supplied receipts. - Keep the full rules in utils/trading_allowance.py. The deduction, casual-income deduction and uses_trading_allowance docs go back to main's text with a one-line pointer; income_not_otherwise_charged's docs and the statutory parameter's description go back to main's; the input's docs and the expense share's description are shorter; the changelog fragment is two sentences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Response to the eighth review at
|
Response to @vahid-ahmadi's review at
|
|
Round 2 at Round-1 items
New findings None. The diff only adds the gate, removes the branch the gate makes unreachable, and shortens the docs. Unknown status: still identical to the merge base. This used the same randomised setup as round 1: 300 people per run, with half set explicitly to UNKNOWN, in 2026, 2027 and 2029, at the default allowance and at £0, £500, £3,000 and £5,000. In all 15 runs the trade deduction, the casual-income deduction, CI at Verdict: all round-1 items are fixed or accepted, and I have nothing further on this PR. 🤖 Generated with Claude Code |
Fixes #2764
Survey data hold profit, not turnover, so a raise in the trading allowance missed its main base: traders who already claim £1,000 against larger receipts. The Budget 2026 raise to £3,000 (in current law since #2719) is costed on that base; HMRC's policy paper puts it at about 400,000 taxpayers with a lower bill, by about £380 on average. This PR lets a dataset say who claims, so the engine reaches that base at any allowance level.
What changes
trading_allowance_claim_status(person, enum, defaultUNKNOWN): whether the person claimed the allowance in the year their input profit was measured. It describes a positive profit, so a person with no profit is read as unknown, whichever status they have.CLAIMANT: the profit is receipts less the allowance, as under partial relief (ITTOIA 2005 s. 783AI).NON_CLAIMANT: the profit is receipts less actual expenses (capital allowances come off separately), and those deductions exceed the allowance.Dataset, it is the earliest year the profit was supplied for.set_inputcan't move it.parameters.baseline, which reforms leave alone.gov.simulation.trading_allowance_claimant_expense_share(default 1, bounded to [0, 1]): claimants' expenses and capital allowances as a share of the input allowance. It sets claimants' ordinary treatment, so it matters under a cut below the input allowance and, for a claimant with a supplied current loss, at any allowance.utils/trading_allowance.py, whichtrading_allowance_deduction,miscellaneous_income_trading_allowance_deductionanduses_trading_allowanceshare:uses_trading_allowance, giving up a current loss) where it covers their expenses, unless a supplied current loss is worth more than its saving over them; ties take the allowance. The election weighs the loss against the saving in taxable trade profit and casual income, so Class 4, and income tax where a relief reads trade profit, can step where it switches, under the convention every status shares.self_employment_gross_receiptsis supplied, otherwise the no-receipts rule. Existing datasets and situations give the same results.ni_class_4_profits_before_losses) and relevant UK earnings read the same deduction, and their formulas are unchanged..github/workflows/test-suite.yamlraises the Python shards' time limit from 75 to 90 minutes. Shards took up to 75 minutes on slower runners and were cancelled with no test failing (runs 41303444320 and 41326415023). The YAML job stays at 75 minutes; its comment now says Release each YAML case's simulation and its file's reformed systems #2742 released the cases and the job takes about 9 minutes. Editing this file also makes each push runversioning.yaml's Release test suite, roughly six more runner-hours, which isn't a required check.Design choices for review
An enum, not a bool with a known flag. One column holds three exclusive states. With a bool and a known flag, the two columns must agree: a dataset that sets the flag but not its partner is silently ignored, and one that marks everyone known turns every unflagged trader into a non-claimant. The default
UNKNOWNis what leaves existing data alone. Datasets store the enum name (CLAIMANT), whichSimulationencodes like other enum columns, and the value carries forward from the year supplied.The input allowance is a fact about the data, not a simulation-year parameter. Reading it for the data year lets one model version serve data from before and after the April 2027 raise, with no separate copy of the allowance's history.
A dataset's year is a fact about the whole file: its survey year, which the engine projects forward. A situation states no such year, so it uses the year simulated, the model's existing rule for situations (State Pension). Earlier rounds inferred a situation's year from the history of profit writes instead; that needed state, and each fix to it opened new edge cases, so this PR no longer does it.
Two limits remain, documented in
_data_year:A cut below the input allowance is set by an explicit share. Claimants' expenses are below the input allowance but unknown. Under a cut, a claimant with no current loss keeps claiming while the new allowance covers their expenses, and otherwise deducts those (ordinary treatment). So their taxable profit rises by the input allowance less the larger of the two.
A claimant with a supplied current loss claims it where it is worth more than the allowance's saving over their expenses; ties take the allowance. At the input allowance and the default share that saving is nil, so they claim it there: their claim and the loss contradict each other, and the election settles it as it does for everyone. So the share matters without a cut too: in 2026, a claimant with £30,000 of pay, a £2,500 profit and a £1,500 loss pays £3,686 at the default share and £3,886 at a share of 0.
Non-claimants keep full relief, but not partial relief. Partial relief needs an election (s. 783AM), and electing after a raise is behaviour, as HMRC's costings book it. Full relief needs none.
Without supplied receipts, a non-claimant's receipts are unknown but above their profit plus the input allowance. So the model shows full relief only from supplied receipts. Elsewhere, under a raise it gives none, which is a lower bound. A non-claimant with no profit is read as unknown, as a claimant is, so their casual income gets the allowance as anyone's does.
Chapter 8 miscellaneous income (#2725, #2726) is handled, not left as is. A claimant's receipts take up the allowance first (trades first, s. 783AK), and income not otherwise charged gets the rest.
A non-claimant gets it only through full relief.
For the data side (PolicyEngine/microcosm#1439)
CLAIMANTis read as receipts above £1,000 and taxed at baseline, whereUNKNOWNrelieves it in full. Traders whose receipts were within £1,000 belong atUNKNOWN.self_employment_gross_receipts, and the input allowance is nominal, so the gap no longer grows with mixed-income uprating.trading_allowance_deductionto zero, as policyengine-uk-data does on SPI records, bypasses the formula and blocks every allowance reform. A flagged claimant's election weighs a pinned deduction at its value, so their treatment stays consistent, but the raise can't reach them. Flagging claimants and non-claimants, without the pin, avoids that.Tests
policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/allowances/trading_allowance_claim_status.yaml(57 cases):policyengine_uk/tests/test_trading_allowance_claim_status_properties.py(one hypothesis test, 30 people per draw, 15 scenarios of allowance and expense share, one at a share of 1.5, with every invariant checked on one run of each scenario):uses_trading_allowance, with Chapter 8 and miscellaneous income drawn separately.policyengine_uk/tests/test_trading_allowance_claim_status_data_year.py(18 tests on small datasets and situations):set_inputin any order, or for two people in turn;set_model_value), each keep a raise from costing a claimant their loss;set_inputorset_model_value, changes no status's election, deductions, Class 4 or income tax, over a grid of 216 people at three allowances and shares.Commands run in the locked environment (
uv run --frozen --extra dev):policyengine-core teston the claim-status YAML atedda47ea2: 57 passed. On all ofpolicyengine_uk/tests/policyat870b07f3b: 5,466 passed. Since then only the claim-status file and the non-claimant gate changed, and no other YAML file sets the status.pytestatedda47ea2on the data-year and property files, the parameter and variable metadata tests, the tests that scan the package (test_household_subset.py's review tests,test_warning_filters.py) and all ofpolicyengine_uk/tests/code_health: 3,176 passed.ruff formatandruff check: clean..github/check_changelog_fragments.py: OK.Not run locally: the full suite, left to CI, and the microsimulation tests, which need the dataset token.
axiom: n/a: adds an input that records how the data's profits were measured and a gov.simulation convention parameter; the Part 6A rules (ss. 783AE, 783AF, 783AI) are unchanged
🤖 Generated with Claude Code