Repository navigation
Limit Income Tax reductions to the Step 5 tax so they don't offset Step 7 charges - #2545
Conversation
ITA 2007 s. 23 deducts tax reductions at Step 6 from the Step 5 tax and adds the s. 30 charges at Step 7. s. 29(2) allows a reduction only so far as there is Step 5 tax to deduct it from. income_tax netted the reductions against the Step 5 tax and the Step 7 charges together, so other_tax_credits above the Step 5 tax offset the High Income Child Benefit Charge and the annual allowance charge, and capped_mcad was capped at the Step 5 tax plus the HICBC. Split income_tax_additions and income_tax_subtractions into explicit Step 5, Step 6 and Step 7 lists, add income_tax_reductions (Step 6, limited to the Step 5 tax) and income_tax_charges (Step 7), and cap capped_mcad at the Step 5 tax left after other_tax_credits (s. 27(5)). Fixes #2544 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
YAML cases for an HICBC payer whose other_tax_credits exceed their Step 5 tax, a married couple's allowance claimant who pays HICBC after VCT relief, the annual allowance charge, and reductions exactly at the Step 5 tax. Hypothesis properties: the Step 6 deduction is within [0, Step 5]; income tax is Step 5 - Step 6 + Step 7, at least the Step 7 charges and at most Step 5 + Step 7; each reduction is limited to the tax left before it; more other_tax_credits never raise income tax; and a differential against the pre-fix formula, which the fix raises by exactly the charges it offset. 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. Receipts by tax head
Spending by benefit and service
Caseloads
Government balance, each component counted once
Model totals
Household income totals
Inequality
Mean household net income in each side's own deciles
Standard reform costings (change in gov_balance)
Standard reforms: change in each reform's target
Household net income by decileMean change in household net income per household, and as a share of the decile's base income; households stay in the base run's deciles.
No change in any year: population; poverty rates; people in poverty. Checks failing on both base and head (1)
Counted-once checkEach of 91 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
|
The case expected a £700 cap on an £800 reduction against £500 of Step 5 tax and a £200 HICBC. Under ITA 2007 s. 29(2) the reduction is limited to the £500 of Step 5 tax, and the HICBC is added at Step 7, so income tax is £200. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Reset the reusable simulation before setting inputs: reset_calculations() cleared employment income and Child Benefit, which the simulation was not built with, so the HICBC cases never ran. The tests now assert that every drawn input reaches the calculation, and draw pension-age people too. - Compare against a simulation of the pre-fix rules (a structural reform of income_tax and capped_mcad), not a reconstruction from the fixed run's values. The property allowance election weighs income tax on each route, so it can differ under the old rules. Income tax is never lower on either route, so never lower; where both take the same route, Step 5, the charges and the finance-cost reduction are unchanged and the restored charge is exact. - Drop the assumption that more other_tax_credits leave Step 5 unchanged, which an election change breaks. - Add the YAML case of a landlord whose election let the old netting offset £168 of HICBC, and qualify the s. 27(5) ordering in the docstring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed at f903b28a75b9ef223c45547be6b3e4b40cd5c3c3. No actionable findings from independent code and statutory-source reviews.
The change correctly caps Step 6 reductions against Step 5 tax before adding Step 7 charges. Final-tax cases confirm that excess credits cannot remove the £1,406 HICBC or £4,000 annual-allowance charge. All 45 focused tests passed, including generated invariants, MCA, property elections and downstream consumers.
This approval covers the present head. The declared #2066 Winter Fuel Payment charge prerequisite still requires integration and retesting before landing; the documented aggregate foreign-relief and Gift Aid limitations remain.
|
Heads-up from #2719 (Budget 2026 measures, draft until Budget week ends): it adds 🤖 Generated with Claude Code |
|
Updated head: At that head, I ran the income-tax YAML directory (406 passed), the selected changed/winter-fuel/means-test YAML files (194 passed), and the targeted Python files/cases (127 passed, including 8 Step 6/7 property tests). The PR body lists the covered areas. I ran tests sequentially under the requested caps with no survey dataset and with the two specified environment variables unset. Ruff 0.17.2 format/check on changed Python files, the package-wide F811/F821 check, and git diff --check all passed. The scratch Step 5 placement mutation died: the winter-fuel regression returned £4,000 instead of £4,200 (1 failed, 7 passed). The code keeps the winter fuel charge at statutory Step 7, separately from the tax aggregate used by benefit means tests. María's approval covers the preceding head |
The election weighs the winter fuel payment charge, so more credits can move a person to the route with more tax before the charge. Check that tax on each fixed route instead, and add a hand-computed case where £500 more credits raise it from £0 to £86 while total tax falls from £200 to £86. The simulation reset now keeps inputs set through set_input, so run() deletes any route a previous run fixed before setting its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hand-off to the UK hub at head Review: GPT-6.1 Sol (
What
Checks at
Aggregate impact: at
Locally at Still open:
🤖 Generated with Claude Code |
Fixes #2544
Reproducer:
policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/step_6_step_7.yaml,policyengine_uk/tests/test_income_tax_step_6_step_7_properties.py::test_the_fix_restores_exactly_the_charges_the_old_netting_offsetaxiom: uk:statutes/ukpga/2007/3/23#income_tax_liability encoded-correct (companion case
no_personal_allowance_and_reductions_cannot_make_tax_left_negativeinuk/statutes/ukpga/2007/3/23.test.yaml: reductions of £800 against £500 of tax leave the £20 of s. 30 tax)This update merges
origin/mainatdaec84d5bintoincome-tax-step6-step7with merge commit280e7dc4d. It incorporates the winter fuel payment charge from #2066 and applies the Step 6 cap to the liability before that charge.Previously, the income-tax calculation netted tax reductions against the Step 5 tax and the High Income Child Benefit Charge and annual allowance charge together. Excess reductions could therefore remove a Step 7 charge. The married couple's allowance cap also included the High Income Child Benefit Charge.
ITA 2007 s. 23 deducts tax reductions from Step 5 tax at Step 6, then adds the additional tax listed in s. 30 at Step 7. Section 29(2) and (3) limits reductions to the Step 5 tax left after reductions already deducted. The resulting calculation preserves the Step 7 charges when credits exceed the Step 5 tax.
The code now uses explicit parameter lists:
step_5_tax_componentssupplies the rate taxes thatincome_tax_pre_chargesadds.step_6_tax_reductionssupplies the reductions thatincome_tax_reductionslimits in total to Step 5 tax.step_7_additional_taxsuppliesCB_HITCandpersonal_pension_contributions_taxtoincome_tax_charges.income_tax_before_winter_fuel_payment_chargesubtracts the capped Step 6 reductions from Step 5 tax, then adds those other Step 7 charges.income_taxaddswinter_fuel_payment_chargeto that result. Finance Act 2026 Sch. 10 para. 1 creates the charge in ITEPA Part 10 Chapter 9. Sch. 10 para. 4(3) places the winter fuel payment charge in ITA 2007 s. 30(1), so the charge belongs at Step 7 alongside the other charges. #2066's placement agrees with the law. Adding it separately remains part of Step 7: the means tests deduct the pre-charge aggregate, and the charge depends on Pension Credit and Universal Credit receipt, so including it in that aggregate would create a benefit calculation cycle.The merge preserves main's winter fuel payment rules, means-test readers and property allowance election, including its charge-ceiling optimization. The new YAML regression combines excess tax credits with a winter fuel payment charge and an annual allowance charge. The generated tests check the capped reductions, the accounting before the charge, the separately added winter fuel payment charge, monotonicity and the difference from the old netting rules. They check monotonicity of tax before the charge on each property allowance route, and of total tax on the route the election takes. A hand-computed case shows why: the election weighs the charge, so more credits can move a taxpayer to the route with more tax before it. The winter fuel means-test tests cover changes in benefit entitlement.
capped_mcadnow uses only Step 5 tax left afterother_tax_credits.property_finance_cost_reliefandutils/uc_income_tax.pyread the renamed Step 6 list. The UC calculation retains its allocation of finance-cost relief against property tax first. The fiscal partition retains main's metadata and records the new reductions and charges aggregates. The changelog documents the removedincome_tax_additionsandincome_tax_subtractionsnames.The history audit searched main since
76cfc9e79for every reader or writer of the old lists, the new aggregates,capped_mcad,property_finance_cost_relief,other_tax_creditsandutils/uc_income_tax.py. The merge carries those changes forward and migrates the affected list readers. Main added no further charge or reduction to the old lists that needs a new entry here.The existing aggregate-credit limitations remain documented:
other_tax_creditscombines reliefs whose statutory order differs, including foreign tax relief after the married couple's allowance, and prevents the model from applying the separate Gift Aid limit in s. 29(4A)–(4C).Open #2719 adds
pension_tax_allowanceandutils/income_tax_reductions.py::earlier_tax_reductions. Whichever PR lands second must carrypension_tax_allowanceintostep_6_tax_reductionsand keep it last, adaptearlier_tax_reductionsto read that list, and preserve the ordered caps against the remaining Step 5 tax. It must retain the helper's use byproperty_finance_cost_reliefwithout a dependency cycle and preserve UC's use of the list and its separate allocation of finance-cost relief. This merge does not copy #2719's unmerged implementation. The combined head must rerun the pension-tax-allowance YAML, properties and UC tests.The existing coordination with open #1963 (Marriage Allowance) also applies: whichever PR lands second must carry
marriage_allowance_tax_reductionintostep_6_tax_reductions.Impact
The CI aggregate-impact job ran base
daec84d5b6(main) against headcd742f7f52(the merge commit for280e7dc4d) on certified policyengine-uk-data 1.58.1 (enhanced_frs_2024_25.h5, sha256056e80fd8e91…), fiscal years 2025-26 to 2031-32. It reported 0 checks broken and 0 moves flagged. Income tax receipts moved by £0 to £36 a year in either direction, out of £294bn to £398bn a year, and household net income by at most £71 in total. Those moves are float32 rounding. In a local run on this branch before the merge, for 2025-26 and 2026-27, the Step 6 reductions (other_tax_credits, the married couple's allowance and the finance-cost reduction) summed to £0 on 1.58.1, so the cap never binds in the survey data. Commitb5fd880c9changes only tests.The fix changes tax for households, not survey aggregates: anyone whose reductions exceed their Step 5 tax keeps paying their Step 7 charges. The YAML cases show £1,406 of High Income Child Benefit Charge and £4,000 of annual allowance charge that the old netting cancelled. Main already adds the winter fuel payment charge after the netting, so credits never cancelled it; the new winter fuel YAML case checks that it still survives.
Invariants
The property tests (
policyengine_uk/tests/test_income_tax_step_6_step_7_properties.py) check these for every drawn case, in English and Scottish single-person households, at ages 45 and 70:other_tax_credits, and the finance-cost reduction at most the tax left after both.other_tax_creditsleave the Step 5 tax and every charge unchanged and never raise tax before the winter fuel payment charge. On the route the election takes, total tax never rises. Tax before the charge can rise there: in the hand-computed case, £500 more credits move a 70-year-old landlord from actual expenses (total income £35,500, so a £200 charge) to partial relief (£34,500, no charge). Their tax before the charge rises from £0 to £86, while total tax falls from £200 to £86.Validation
Validation at
280e7dc4d0791f472659fdce3c24c7e3b7401deaused household inputs and unit fixtures, with no survey dataset:cloned_uk_tax_benefit_systemfixture where appropriate; the code-sharing run uses its original setup.-n 0. Each run unsetHUGGING_FACE_TOKENandPOLICYENGINE_UK_DEFAULT_DATASET.format --checkandcheckpassed on every changed Python file;check --select F811,F821 policyengine_ukpassed. Both working-tree and PR-diffgit diff --checkpassed.At
b5fd880c962b9217713022f808cab6a08d15f3f9(review r2 fixes, tests only):git diff --check, passed.set_input, so a fixed route would leak into later runs on the shared simulation.run()deletes any fixed route before it sets its own. Without that deletion, the election case failed after a fixed-route run.The PR remains a draft under the policyengine-uk merge hold. María's approval predates the merge.
🤖 Generated with Claude Code