Repository navigation
Conversation
ITEPA 2003 s.681I (FA 2026 Sch 10, from 2025-26) charges a person entitled to a winter fuel payment the whole payment when their own total income exceeds £35,000 and they are not entitled to IS, income-based JSA, Pension Credit, income-related ESA or UC. It covers the Winter Fuel Payment in England and Wales (SI 2025/969) and Northern Ireland (NISR 2025/142) and Scotland's Pension Age Winter Heating Payment (SSI 2024/351). - winter_fuel_payment_charge (Person) under gov.hmrc.income_tax.charges, added to income_tax after tax reductions (ITA 2007 s.23 Step 7, s.30). - Payments stay gross; from 2025 the WFP pays everyone of pensionable age (require_benefits false), so entitlement, and the shared amounts, come before the charge. - Means tests deduct income_tax_before_winter_fuel_payment_charge: the charge recovers a payment, and it depends on PC/UC receipt. - pawhp counts in household_benefits and HBAI income, like the WFP. - The household income test is deprecated (off from 2025; boundary <=). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment charge ITEPA 2003 s.681I(5) lists five relevant benefits and no tax credits. A mutant that exempted tax credit recipients survived the other cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
#2038 merged into main as 4012143, so this PR's base is now
🤖 Generated with Claude Code |
Brings in #2038 as merged (paid-to versus couple receipt, payee-only is_on_pension_credit over the qualifying age, is_on_universal_credit without an ineligible partner, the dated age test), #1949 (UC earned income deducts each person's own tax on earnings) and the rest of main. Conflicts: - uc_earned_income: take #1949's formula; uc_income_tax_on_earnings works from the rates and never reads income_tax. - is_benefit_cap_exempt_earnings: main's claimant-only sum, deducting income_tax_before_winter_fuel_payment_charge. - is_benefit_cap_exempt_other: main's version (it no longer tests earnings). - winter_fuel_payment_eligible: main's age test with this branch's <=. - The Hypothesis reference: main's paid-to logic; the WFP means condition holds outside 2024; the charge exempts only a person a relevant benefit is paid to (the claimant for IS, JSA, ESA and PC; both for UC), as the model's is_on_* variables now do. Two income_tax readers came in from main and would include the charge: - housing_benefit_net_earnings (HB regs 36(3)): deducts the tax before the charge, which is not tax deducted from earnings; - uses_property_allowance: the route branches compare the tax before the charge, since the charge depends on Pension Credit, which depends on property income. New YAML cases cover both, and the partner of an Income Support claimant who is paid and charged. relevant_benefits.yaml describes the payee reading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The model's total_income is before the property allowance, so the charge is the same on either route; reading income_tax in the route branches would only recompute the means-tested benefits there (a probe with income_tax in the route made the same choice and raised no CycleError). Correct the YAML case's comment to match: total income is £35,300 on either route. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without universal_credit: 0 the model computes a Universal Credit award for the couple, which makes both members joint claimants and exempts the partner. Zero Universal Credit and Pension Credit, as the other cases do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three property tests that CI failed on 03154a1 compared a means test with income_tax, which now includes the winter fuel payment charge: - test_uc_earnings_deductions_properties: the pre-#1942 UC earned income formula deducted benunit_tax, which now reads the charge and so UC receipt (CycleError). It deducts the benefit unit's income tax before the charge and its NI instead. - test_council_tax_reduction_carer_income_properties and test_housing_benefit_savings_credit_only_properties: CTR and HB income deduct income_tax_before_winter_fuel_payment_charge, so the identities add back that liability, not income_tax. The HB boundary test also excludes the charge, as it excludes the other payments that depend on Pension Credit receipt. A YAML case pins CTR income to the tax before the charge, which no YAML case covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review r3 (lost to the 10/12 reboot) found two properties false inside their own generators on aede16d, both comparing a means test with income_tax, which now includes the winter fuel payment charge: - test_deductions_equal_tax_and_ni_on_earnings_alone: a pensioner in a mixed-age couple earning £40,000 off UC is charged £200. UC deducts tax on earnings (reg 55(5)), so the test compares with the tax before the charge. - check_routes in the CTR Pension Credit properties: MAIN_DEDUCTIONS now uses the tax before the charge, as the CTR means test does. Both counterexamples are pinned as plain tests that also assert the charge is levied. Also: - the inert income_tax: 0 inputs in two means-test YAML files now set the variable the means tests read; - the property-allowance YAML case is labelled a known gap: the law's partial relief would bring total income under £35,000; - s.681J links to ITEPA, not FA 2026; - the changelog no longer says the household test "charged" anyone; - the HB property's reason for excluding the WFP fits its 2026 year. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
#2047 merged into #2047's |
…harge # Conflicts: # policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py
…harge Conflict in is_benefit_cap_exempt_earnings.py: take main's uc_individual_earned_income_before_mif, which deducts each person's own uc_income_tax_on_earnings and so never reads income_tax or the charge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e property election Review r5 and CI on adcf821: - The charge tested total_income, which leaves out the statutory sick and maternity pay the model taxes as employment income. Total income is the income charged to tax (ITA 2007 s.23 Step 1), so they now count (test_uc_statutory_pay_properties invariant 2 failed on CI). - uses_property_allowance compared tax before the charge. A route can change Pension Credit and so the s.681I(4) exemption; it now compares income_tax, as main did (r5 P2, the m6 mutant was not equivalent). - test_trading_loss_properties: the means tests deduct tax before the charge, so the properties hold that; relief versus income cut compares it too, since relief comes after total income (Step 2). - test_variable_code_sharing reads a variable with a formula; income_tax now only adds. - YAML: statutory sick pay in total income, trade loss relief after total income, and the election that avoids the charge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main now has the PAWHP household-income lines (#2047) and UC and benefit cap earnings tests that deduct each person's own tax on earnings, so the fragments no longer claim them. The property election compares income_tax, which includes the charge, since 1024887. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The election has counted the charge since 1024887, so the case is about total income being taken before the property allowance, not about the election ignoring the charge. Expected values are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the trade loss property - test_rule_comparison (new on main) replaces income_tax's formula, but income_tax now only adds the pre-charge liability and the charge, so it has none (CI: StopIteration). It now replaces the pre-charge formula. - The zero-profit trade loss property held only tax before the charge, since 1024887. It now also checks the charge: it moves only in a household where the loss changes who is on a relevant benefit, as when lower tax ends Pension Credit and with it the exemption (s.681I(4)). A pinned pensioner exercises that case: on Pension Credit (£217.48) and not charged without the loss; off it and charged £200 with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The winter fuel payment charge tests total income (ITEPA 2003 s.681I(1)(b)), which is ITA 2007 s.23 Step 1: the income charged to tax, after employment expenses, capital allowances, the trading and property allowances and exempt ISA interest, and before the Step 2 reliefs. The charge read total_income, which sums gross income. Add total_income_charged_to_tax, the Step 1 figure adjusted_total_income already worked out for the s.24A cap (the taxable components with losses brought forward added back), and have both read it. Tests that set the charge's income now set the new variable; six YAML cases pin each deduction, and the property allowance case that pinned the old gap now follows the law. r6 P1: test_income_tax_falls_with_the_loss_by_at_most_the_relief compared income_tax, which the charge can raise when the relief ends a Pension Credit award. Compare tax before the charge, check that the charge moves only with benefit receipt or total income, and pin r6's counterexample. Invariants 3 and 9 say so. Nits: the code-sharing helper's name, the deprecated limit's reference, the changed.md wording, and a pinned pensioner charged on both sides of the zero-profit test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ousehold Review r7 found two ways the property allowance election could choose the route with the higher income tax once the winter fuel payment charge is in it: - Without finance costs at stake, the allowance was taken whenever it lowered taxable property income. Lower tax before the charge can raise the income Pension Credit counts, end the award and with it the s.681I(4) exemption, and the charge can exceed the saving. While the charge has effect, these landlords now compare both routes too; ties keep the allowance, so anyone the charge does not reach gets the answer the shortcut gave. - Landlords decided one per benefit unit, so landlords in separate benefit units of one household switched together. A couple's Pension Credit decides whether the partner is entitled to a payment, which moves another benefit unit's shared payment and charge. Landlords now decide one per household. Pins both counterexamples in YAML and adds a Hypothesis property: no landlord in a pensioner household lowers their own income tax, charge included, by switching route alone. The couples property now covers every landlord the allowance would help. Also qualifies the trading loss invariants (they hold tax before the charge) and total_income_charged_to_tax's description. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: test_trading_loss_properties.py docstring. Kept main's invariant 1 wording (pre-pension net_income, #2440) and this branch's invariants 2 and 3 (tax before the charge). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d limit Review r8: the opening claim that every election lowers tax holds only with finance costs to relieve; without them the allowance is the default and ties keep it. The household loop stops after MAX_ROUNDS even if routes are still changing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FA 2004 s.193(2) deducts contributions under a net pay arrangement from employment income, so total income (ITA 2007 s.23 Step 1), which the winter fuel payment charge tests, is after them. The model holds employment income gross of pension contributions, takes employee contributions as net pay and gives their relief through allowances, so total_income_charged_to_tax overstated a working pensioner's total income by their relieved employee contributions. New net_pay_pension_relief: the relieved employee contributions, up to taxable employment income. adjusted_total_income now deducts at Steps 3 and 4 only the relieved contributions not already out of total income (s.24A(8)), so its values are unchanged. Relief at source (s.192) still leaves total income alone. Tests: four YAML cases worked from the law, and a Hypothesis property (net pay relief, total income, adjusted total income, the charge, and relief at source leaving total income alone); two mutants each fail it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at source Review r9 found the net pay deduction capped at pension_contributions_relief, whose relevant UK earnings leave out statutory sick and maternity pay. A pensioner with £1,000 of wages, £3,000 of SSP, a £32,500 pension and £2,000 of employee contributions had £1,000 deducted and a £200 charge, where the law deducts the whole £2,000 (FA 2004 ss.189(2)(a), 190(1), 193(2); HMRC PTM044100) and charges nothing. Personal contributions also moved the deduction, through the basic-amount rise that s.191(7) keeps for relief at source. The deduction is now the employee contributions up to taxable employment income, none from age 75. Relevant UK earnings include all of that income, so the s.190 limit never binds below it. It no longer reads relief at source. Two YAML cases pin the SSP case with and without personal contributions; the net pay property test now draws statutory sick and maternity pay. ATI's comment and the changelog qualify "unchanged" for float32 rounding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…del's relief Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ATI comment's "never exceed" holds for contributions of zero or more; the add-back cancels either way. Relieving employee contributions first is the model's order, not one the Act states. The fixed fragment now says the payment reaches everyone of pensionable age who meets its other conditions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…are at stake WIP for Max's ruling on d1669, not for the PR until he rules. Since 26ff324 every landlord the property allowance helps had both routes compared while the winter fuel payment charge has effect. Each comparison works out income tax with the charge, and so the means-tested benefits, for the whole population, at every Simulation init for 2025-2032: CI shards reached 15.5 GB (9-10 GB on main) and a synthetic probe took 6.0 s against 0.9 s. On the Enhanced FRS the comparison moved no record's tax, charge or Pension Credit. Landlords without finance costs take the allowance without comparing again, as on main. Review r7's lone pensioner becomes a pinned limit: the YAML case and a property test record that the model takes the allowance and charges her £200 (£5,227.40 against £5,187.40 with expenses). The pensioner property now asserts optimality for landlords with finance costs to relieve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Back to draft: review r11 approved the head
|
Conflicts, each resolved to keep both sides' intent: - taxable_income_test parameters (maximum_taxable_income, use_maximum_taxable_income): main reworded the household test as inclusive of total income at the limit; this branch had already deprecated it with that wording. Keep this branch's descriptions. - winter_fuel_allowance/taxable_income_test.yaml: keep this branch's at-threshold case (same expected value as main) and its 2025 cases, and add main's 2024 case that other investment income counts. - test_winter_heating_payment_properties.py: main made the household stand-in inclusive; this branch removes the stand-in and covers 35,000 and 35,001. Keep this branch's. - housing_benefit_net_earnings: main moved the earnings rules into _net_earnings.person_net_earnings, shared with the new council_tax_reduction_net_earnings. Take main's helper and deduct income tax before the winter fuel payment charge inside it, so Housing Benefit and the CTR pensioner earnings disregards both keep this branch's rule. Semantic follow-ups of main's changes: - test_pension_credit_prescribed_benefits_properties.py (new on main): its tax identities now use the tax Pension Credit income deducts, before the charge, as this branch's other means-test property tests do. - winter_fuel_payment_eligible: main's comment on the inclusive limit now describes the deprecated test. Main: 61b5854 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main moved to #2078 (Council Tax Reduction applicant means test) while the previous merge was being resolved. No conflicts: CTR applicable income still deducts income tax before the winter fuel payment charge, and CTR net earnings still go through person_net_earnings, which does too. Main: 79fa317 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arge adjusted_total_income: main's #2488 relief limit (relevant earnings, raised towards the basic amount only by relief-at-source contributions) with this branch's Step 1 from total_income_charged_to_tax and its net pay add-back. Documentation keeps both. The net pay property test's reference takes #2488's limit for invariant 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since 75ca17c, landlords without finance costs take the property allowance without comparing tax. The couples property still checks every landlord the allowance would help, so for those without finance costs it now checks main's shortcut, which holds because the couples are 40 and so never charged. Its comment still said everyone compares tax while the charge has effect. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment-charge No conflicts. programs.yaml auto-merged: main's registry fixes (#2443) and this branch's notes on income tax, the Winter Fuel Payment and PAWHP touch different entries. #2493's 2022 full-year director NI and #2506's aggregate-impact runtime identity read nothing this branch changes; the director tests read income_tax only in 2022, before the charge. Main: 6990350 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hand-off to the UK hub (adoption under d1280, d1669 = B), job 20261020-223919-adopt-uk2066-r2 Head: Since review r11 (
Tests (local, locked venv, core 3.33.2, Python 3.13):
Impact (certified uk-data 1.58.1, SHA-256 Memory, one microsimulation ( Memory, CI shards (peak "memory: N MB used" per shard):
CI: at the final head
At Open items for the hub:
Merge resolutionsBranch side: Three merge commits, because main moved while the job ran:
Textual conflicts (merge 1)
Semantic resolutions (no textual conflict)
Merge 3 (
|
| File | Main's change (#2488) | Branch's change | Resolution |
|---|---|---|---|
variables/gov/hmrc/income_tax/reliefs/adjusted_total_income.py |
Relief limit = relevant earnings + min(basic amount − relevant earnings, gross personal (relief-at-source) contributions), not max(basic amount, relevant earnings); documentation says only relief at source can use the basic-amount increase (ss.190(3), 191(7)); new s.191(7) reference | Step 1 reads total_income_charged_to_tax (already after net pay contributions); add-back of net_pay_pension_relief; documentation says so |
Formula auto-merged: main's basic_amount_increase / relief_limit and the branch's Step 1 and add-back. Documentation conflict: both — the branch's Step 1 sentence, then main's limit and relief-at-source sentence. Main's s.191(7) reference kept. As the PR's merge notes said, net pay relief (employee contributions up to taxable employment income) stays within main's relieved contributions, because the limit is at least relevant earnings. |
Semantic: tests/test_total_income_net_pay_properties.py reference for invariant 3 now uses #2488's limit (taxable pay + min(max(0, 3,600 − taxable pay), personal contributions)) instead of max(3,600, taxable pay); docstring updated. Main's new adjusted_total_income_pension_relief.yaml (#2488) is run against the merged head below.
Merge 4 (56a7fe29f, main 69903500e), attempt r2
Main moved 10 commits past 60b09a4e5: #2443 (program registry references), #2506 (aggregate-impact runtime identity), #2493 (2022 full-year category A director NI).
- No textual conflict. The one file both sides changed,
policyengine_uk/programs.yaml, auto-merged: main's hunks fixvariable/parameter_prefixfor JSA, ESA, DLA, benefit cap and maternity allowance; the branch's hunks edit thenotesof income tax, Winter Fuel Payment and PAWHP. The merged file equals the branch's plus main's hunks (git diff ab69a95a9 -- policyengine_uk/programs.yamlshows only main's). - Semantic check. Apply 2022–23 annual employee NI for full-year category A directors #2493's new variable
is_full_year_category_a_directorand its Class 1 changes read no variable this branch changes. Its tests (test_ni_full_year_director_2022.py,ni_class_1_full_year_director_2022.yaml) readincome_taxonly in 2022, when the charge is off. Require complete runtime identity for aggregate-impact data comparisons #2506 touches onlyutils/aggregate_impact/compare.pyand its test. Fix program registry model references #2443'stest_program_registry.pychecks that each program'svariableandparameter_prefixexist; the branch's three programs (income_tax,winter_fuel_payment,pension_age_winter_heating_payment) keep theirs. Run in the light checks below.
After merge 4: tests from main that assumed no charge (attempt r2)
CI's YAML job at 56a7fe29f (run 39542312053) failed 2 of 3,959 cases, both in files that reached the branch with the merges of main without a textual conflict. Fixed in cc2961429, keeping both intents:
| File | Main's case | Under this PR | Resolution |
|---|---|---|---|
tests/policy/baseline/gov/dwp/winter_fuel_allowance.yaml |
#2184 (fixes issue #2174; "#2174 line" above means the issue): four £35,000 boundary cases of the household stand-in; £35,001 in 2026 pays nothing | Everyone of pensionable age is paid from 2025; the charge recovers the payment above £35,000 | Same inputs and boundaries; each case also checks winter_fuel_payment_charge (0 at £34,999 and £35,000, 200 at £35,001); the £35,001 case expects the £200 payment and is renamed; the comment describes the charge |
tests/policy/baseline/gov/hmrc/pensions/statutory_pay_pension_relief.yaml |
#2464, "Age75 exclusion retained": income tax 5,886 | Total income £42,000 (nothing relieved from 75) > £35,000, so the £200 charge is in income tax | income_tax 6,086; adds income_tax_before_winter_fuel_payment_charge 5,886 (main's figure), winter_fuel_payment_charge 200, net_pay_pension_relief 0, total_income_charged_to_tax 42,000 |
aeb1b924d: #2464 (in the branch since merge 3 or earlier) makes pension_contributions_relief count SSP and SMP; net_pay_pension_relief's documentation sentence on #2371 was dropped, as the PR's merge notes planned.
Two YAML files that reached this branch with the merges of main still expected main's rules and failed CI's YAML job at 56a7fe2. winter_fuel_allowance.yaml (#2174's boundary cases): main stood in for the charge with the household income test, so a pensioner with £35,001 of total income lost the payment. Here everyone of pensionable age is paid from 2025 and the charge recovers it, so that pensioner is paid £200 and charged £200. The four boundary cases now check winter_fuel_payment_charge (nothing at £34,999 or exactly £35,000, the whole payment at £35,001), and the comment describes the charge, not the stand-in. statutory_pay_pension_relief.yaml (#2464), "Age75 exclusion retained": the 75-year-old's total income is £42,000, with nothing relieved from age 75 (FA 2004 s.188(3)(a)), so income tax includes the £200 charge: 6,086, of which 5,886 (main's figure) is tax before the charge. 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: 22 (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
Household income totals
Poverty rates
People in poverty
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: components the model counts twice; population. 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
Components the model counts twice: 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
|
#2464 (merged into main on 2026-10-19 and into this branch with the merges of main) counts statutory sick and maternity pay as relevant UK earnings in pension_contributions_relief (utils/pensions.py relevant_uk_earnings adds employment_benefits). The documentation still said that relief counted only wages and trading profits (issue #2371) and so could give less relief than this deduction. The PR's merge notes said to drop the sentence once #2464 merged. Documentation only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes #1760
Reproducer:
policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml::2025 total income of exactly £35,000 is not chargedReproducer: policyengine_uk/tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml::2025 Northern Ireland pays everyone of pensionable age
Summary
Models the winter fuel payment charge (ITEPA 2003 s.681I, inserted by FA 2026 Sch 10, from 2025-26) person by person, for the Winter Fuel Payment in England, Wales and Northern Ireland and for Scotland's Pension Age Winter Heating Payment. The payments are now paid in full to everyone entitled, and the charge recovers them through income tax.
This was stacked on #2038, which has merged. The branch has since merged
mainup to69903500e(2026-10-21), and this PR targetsmain.d1669 = B (Max, 2026-10-19): landlords without finance costs keep
main's shortcut in the property allowance election, and review r7's household that this misses is a documented limit. Comparing routes for them had made CI's Python shards run out of memory. The UK hub adopted this PR on 2026-10-20 and finished it under that ruling.The law
What was wrong
On
maina household test stands in for the charge: a person not on a relevant benefit is paid only if some member of pensionable age in the household has total income under £35,000, in England and Wales only.Design: an income tax charge, not a net payment
The charge could be netted off each person's payment, or added to income tax. This PR adds it to income tax.
CB_HITCinsideincome_tax, withchild_benefitkept gross.gov_spendingcounts what DWP, the Department for Communities and Social Security Scotland pay, andgov_taxcounts the UK income tax charge. Netting would show an income tax receipt as lower Scottish spending on PAWHP.What the choice changes, against netting:
household_net_incomeandhbai_household_net_incomeare the same under both. Each counts the payment and deducts the tax.household_benefits,hbai_benefitsandgov_spendingcarry the gross payment.household_tax,income_taxandgov_taxcarry the charge. In 2025 that is £294.9m of charge (the 2026-10-20 run on uk-data 1.58.1: see the impact section).Three consequences of putting it in income tax:
The means tests deduct income tax before the charge. Pension Credit income, Income Support applicable income, Housing Benefit applicable income and net earnings, and Council Tax Reduction applicable income deducted
income_tax. They now deductincome_tax_before_winter_fuel_payment_charge. The charge depends on Pension Credit and Universal Credit receipt, so deducting it from Pension Credit income makes the model circular: withpension_credit_incomedeductingincome_tax, the core raisesCycleErroronincome_tax@2025. It also is not tax on the income those tests assess: it is the payment, recovered. Universal Credit earned income (Deduct only each person's own tax and NI on earnings from UC earned income #1949) and the benefit cap's earnings test (uc_individual_earned_income_before_mif) already deduct each person's ownuc_income_tax_on_earnings, worked from the rates, which leaves out charges. The Enhanced FRS run confirms no means-tested award moves (see below).The property allowance election counts the charge.
uses_property_allowancecompares each route'sincome_tax, as onmain, which now includes the charge. A route can change total income, because the allowance is deducted in working out property profits, and it can change the tax deducted from Pension Credit income, and so Pension Credit and the s.681I(4) exemption. Two YAML cases cover this. In "a landlord's property allowance election can keep total income under £35,000", partial relief brings total income from £35,300 to £34,500, so the pensioner pays £4,386 rather than £4,246 plus the £200 charge. In "a landlord's property allowance election counts the charge", total income is over £35,000 either way, and £4,490 of tax with the allowance gets the pensioner Pension Credit and no charge, against £4,350 and the charge with expenses. The cost is that each route's branch also works out the person's relevant-benefit receipt.Two parts of the election on
mainassume that lower taxable property income means lower tax, which the charge breaks. This PR changes one and, under Max's ruling d1669 = B, keeps the other as a known limit:mainlandlords in separate benefit units of one household switch together, so one landlord's switch was counted in the other's comparison. In "landlords in two benefit units of one household each elect with the other's route fixed", the lodger elects partial relief and pays £6,186 rather than £6,211.main's shortcut (a known limit). They take the allowance whenever it lowers taxable property income, without comparing tax. That is always the lower tax before the charge. While the charge has effect it can rarely cost more: the allowance's lower tax can raise the income Pension Credit counts, end the award and with it the exemption, and the charge can be more than the saving. In "a landlord without finance costs takes the allowance even where it ends Pension Credit (a model limit)", review r7's case, the allowance saves £160 of tax but ends £80 of Pension Credit and brings in the £200 charge, so the model gives £5,227.40 of income tax where expenses would give £5,187.40. From26ff32421tod0920a9cdthese landlords compared both routes too. That made every simulation from 2025 work out the means-tested benefits on both routes for the whole population, and CI's Python shards ran out of memory. On the Enhanced FRS it changed no record (see "Impact" and "Memory and run time"). The YAML case and a property test pin the limit.PAWHP has to count as household income. Count the Pension Age Winter Heating Payment in household and HBAI income #2047 (merged) put
pawhpinhousehold_benefits,hbai_household_net_incomeandhbai_benefits. Without that, the charge on PAWHP would lower a Scottish household's income with no payment to set it against.Who is "entitled to a relevant benefit"
The charge reads the model's
is_on_income_support,is_on_income_based_jsa,is_on_pension_credit,is_on_income_related_esaandis_on_universal_credit. Since #2027 and #2038 these name the person entitled:So the partner of an Income Support claimant, paid a Winter Fuel Payment of their own because the claimant is under pensionable age, is charged if their own total income exceeds £35,000. A YAML case covers this. The Hypothesis reference exempts only the person a relevant benefit is paid to, not the whole couple.
Changes
winter_fuel_payment_charge(person): the person'swinter_fuel_paymentpluspension_age_winter_heating_payment, when their own total income exceeds the threshold and they are on none of the s.681I(5) benefits.total_income_charged_to_tax(person, new): total income as s.23 Step 1 defines it. It is the model's taxable income components (net_income_before_trade_loss_relief), less the employee pension contributions relieved under a net pay arrangement, with losses brought forward (loss_relief, which the model deducts from trading profits) added back.adjusted_total_income, the base of the s.24A cap, already worked this out for its own Step 1; it now reads the new variable instead, and at Steps 3 and 4 deducts only the relieved contributions not already out of total income (s.24A(8)), so its values do not change, up to float32 rounding (review r9 measured 6p on an adjusted total income of about £905,000).net_pay_pension_relief(person, new): the employee contributions, up to taxable employment income, and none from age 75 (s.188(3)(a)). The model holdsemployment_incomegross of pension contributions, takes employee contributions as paid under a net pay arrangement and gives their relief throughallowances, after total income. FA 2004 s.193(2) deducts them from employment income, so without this a working pensioner's total income would include them. The s.190 limit for relief is relevant UK earnings, which include all employment income chargeable to tax, statutory sick and maternity pay among it (s.189(2)(a); HMRC PTM044100), so it never holds the deduction below taxable employment income. Within the limit, the model relieves employee contributions before personal ones, so contributions relieved at source never change the deduction.total_income, which sums gross income and which other parts of the model read, is unchanged.income_tax=income_tax_before_winter_fuel_payment_charge(the previous formula) +winter_fuel_payment_charge.gov.hmrc.income_tax.charges.winter_fuel_payment:in_effect(true from 2025),income_threshold(£35,000),relevant_benefits.gov.dwp.winter_fuel_payment.eligibility.require_benefitsis false from 2025, so the Winter Fuel Payment reaches everyone of pensionable age in England, Wales and Northern Ireland who meets its other conditions.gov.dwp.winter_fuel_payment.eligibility.taxable_income_testis deprecated, not removed. A reform that sets a missing parameter fails to load, so both parameters stay. The test is off from 2025 and has effect only in a reform that also setsrequire_benefitsto true. Its limit now admits total income equal to it. A saved reform that only movedmaximum_taxable_incomeno longer does anything: the lever for the threshold is nowgov.hmrc.income_tax.charges.winter_fuel_payment.income_threshold.income_tax_before_winter_fuel_payment_charge(listed above).uses_property_allowancekeeps comparingincome_tax; its docstring now says that includes the charge.Invariants
income_tax−income_tax_before_winter_fuel_payment_charge=winter_fuel_payment_charge, for every person.Tests
tests/policy/baseline/gov/hmrc/income_tax/charges/winter_fuel_payment_charge.yaml, 39 cases. They cover the £35,000 boundary on both sides, each partner charged on their own income, a person charged in full still counting as entitled for someone else's shared amount, a lodger, a non-dependant's ESA, a Universal Credit couple, the excluded partner of a Pension Credit claimant, the partner of an Income Support claimant (paid and charged), Northern Ireland, PAWHP at the 2025 and 2026 amounts, no charge in 2024, the charge insideincome_tax, PAWHP as household income, Council Tax Reduction income before the charge, what total income is (after employment expenses, capital allowances, the trading allowance and net pay pension contributions, up to taxable employment income and none from age 75, but not after contributions relieved at source; statutory sick pay as relevant UK earnings for the net pay deduction, with and without contributions relieved at source; without ISA interest; with statutory sick pay; and before trade loss relief and losses brought forward), and four landlords' property allowance elections: one that keeps total income under £35,000, one that gets Pension Credit, one without finance costs that takes the allowance even where that ends Pension Credit (review r7's case, pinned as the d1669 = B limit), and two landlords in separate benefit units of one household.reforms/parametric/winter_fuel_payment_charge/charge_parameters.yaml(a higher threshold, repeal, the charge on a universal 2024 payment, tax credits not exempting), and the deprecated household test inwinter_fuel_allowance/taxable_income_test.yaml(still works when a reform requires a relevant benefit; inert otherwise).tests/test_winter_heating_payment_properties.py): the reference implementation pays everyone from 2025 and computes each person's charge from s.681I, exempting only the person a relevant benefit is paid to. The model must match it for payments, charges andincome_taxless the pre-charge liability, over households of up to three benefit units in all four countries, with incomes either side of £35,000 and exactly on it. A further property sets every income to £5,000 and then £50,000 and requires identical payments, no charge at £5,000, and the whole payment charged at £50,000 for anyone not on a relevant benefit.main: eight mutants that each point one reader atincome_taxor droppawhpfrom HBAI. Seven are killed by the YAML suites, the Hypothesis properties or aCycleError. The Council Tax Reduction one was killed once a YAML case was added. The eighth, the property allowance election comparingincome_tax, is now the rule: review r5 found the case above, where comparing tax before the charge picks the route with the higher final tax.main: ten mutants, each breaking one rule (charged at exactly £35,000; household income; a household member's benefit exempting; PAWHP not charged; charged before 2025; entitlement tested after the charge; no exemption; charge left out ofincome_tax; Winter Fuel Payment still requiring a benefit; tax credits exempting). All ten fail at least one case.income_taxto drive a means test (Housing Benefit applicable income and net earnings, the Income Support family definitions, Pension Credit income). They now enterincome_tax_before_winter_fuel_payment_charge, with the same expected values. A Housing Benefit net earnings case checks that the charge does not come off a charged pensioner's earnings (8,628.50, whereincome_taxwould give 8,578.50).tests/test_property_income_properties.py): a new property covers pensioner households: a couple, with five children or a sixth child in place of the partner, claiming Pension Credit, plus a lodger who may claim it, in England, Wales and Scotland in 2025. No landlord with finance costs to relieve lowers their ownincome_taxby switching route alone. It pins review r7's shared-payment household. Review r7's lone pensioner without finance costs is pinned the other way, as the intended limit, bytest_a_landlord_without_finance_costs_takes_the_allowance_even_where_it_costs_more: the model takes the allowance and charges her £200, £5,227.40 against £5,187.40 with expenses. The couples property covers every landlord the allowance would help, not only those with finance costs at stake. For those without finance costs it now checksmain's shortcut, which holds because the couples are 40 and so are never charged. Atd0920a9cd, before B, electing per benefit unit failed the pensioner property. That mutant was not re-run after B.tests/test_total_income_net_pay_properties.py): over single pensioners in England with pay, statutory sick and maternity pay, employment expenses, a private pension and both kinds of contribution, the model's net pay relief, total income, adjusted total income and charge match a reference worked from FA 2004 ss.188, 192 and 193 and ITA 2007 s.24A(8), and removing contributions relieved at source changes neither total income nor the charge. Three mutants each fail it: total income not after net pay contributions (which also fails two YAML cases), adjusted total income deducting them twice, and review r9's case, the deduction capped atpension_contributions_relief(which also fails the statutory sick pay YAML case).income_taxnow only adds, sotest_variable_code_sharingandtest_rule_comparisonuseincome_tax_before_winter_fuel_payment_charge.total_income_charged_to_tax(the charge YAML, the reform tests, two relevant-benefit cases and the Hypothesis file). Four cases of the deprecated household test, which readstotal_income, enter a private pension instead, which feeds both.cc2961429: the two YAML files frommainthat CI's YAML job failed on at56a7fe29f(see Review),winter_fuel_allowance.yaml(14 cases) andstatutory_pay_pension_relief.yaml(12): 26 passed, and a mutant of each new charge expectation fails;56a7fe29f(2026-10-21): the charge YAML (39 cases), its reform testscharge_parameters.yaml(5) andtaxable_income_test.yaml(18), Restrict the ordinary ATI pension uplift to relief at source #2488'sadjusted_total_income_pension_relief.yaml(14) and Apply 2022–23 annual employee NI for full-year category A directors #2493'sni_class_1_full_year_director_2022.yaml(10), run withpolicyengine-core testas CI's YAML job runs them: 86 passed;56a7fe29f: every Python test file this PR changes, 11 files and 65 tests, includingtest_property_income_properties.pywith the pinned r7 limit andtest_total_income_net_pay_properties.py, with CI's flags (-n 4 --dist worksteal,xfail_strict): 65 passed.test_variable_code_sharing.py's coverage test skips without coverage, so it was also run with--cov=policyengine_uk, as CI runs it: passed. Main's newtest_program_registry.pyandtest_ni_full_year_director_2022.py: 110 passed;61d0bea24(2026-10-20, before the last merge ofmain): the income tax, Winter Fuel Payment and Pension Credit YAML directories, the Winter Fuel Payment and charge reform tests,pawhp.yamlandwinter_heating_payments_in_income.yaml: 708 passed. CI's Python command on the slice--shard 1/16, with the certified dataset: 500 passed, 5 skipped;uvx ruff format --check .: 1,861 files already formatted.uvx ruff check .and--select F811,F821 policyengine_uk: clean.aeb1b924d(run 39550799793, on its merge commit withmain81cbb7089), all 15 checks passed (gh pr checksexit 0 at 07:27Z; the PR is MERGEABLE). The YAML job passed 3,978 cases. The four Python shards passed 7,648 tests, with 61 skipped and 1 xfailed. Changes, Lint, the documentation build, the smoke imports on Python 3.11 to 3.14, Aggregate impact (39550799800) and Fix evidence passed. At56a7fe29f(run 39542312053), the Python shards had passed too, but the YAML job failed 2 of 3,959 cases, both in test files frommainthat assumed no charge;cc2961429fixes them (see Review).Impact: Enhanced FRS
Real runs of the microsimulation on 2026-10-20 on certified uk-data 1.58.1 (
enhanced_frs_2024_25.h5, commit2526edbb), whose SHA-256056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784was checked before each run. Basemainat60b09a4e5(the commit merged in) against this PR at61d0bea24, for 2025, 2026 and 2030. Aggregates only; nothing is scaled or estimated. £m a year; recipients are weighted counts of people (benefit units for Pension Credit, households for household net income) with a nonzero amount:main's income tax, up to £100 of float rounding on a £299bn total.75ca17cdd, against the reviewed headd0920a9cdit sits on, both on this dataset: no person's, benefit unit's or household'sincome_tax,winter_fuel_payment_charge,winter_fuel_payment,pension_age_winter_heating_payment,uses_property_allowance,pension_creditorhousehold_net_incomediffers by 1p in 2025, 2026 or 2030. That run kept per-record arrays only locally and deleted them after the comparison.56a7fe29f(39542311989) compares the merge commit5e1793b2cwithmain69903500e. Its run on the final head (39550799800, now in the comment) compares the merge commit5a8fbd04cwithmain81cbb7089. Both use the same dataset and give the same changes in every line. For 2025, 2026 and 2030 they give the same changes in income tax, the Winter Fuel Payment and household net income as the table above, to £0.1m. Over 2025-26 to 2031-32, income tax rises £294.9m to £422.6m a year and the Winter Fuel Payment £175.5m to £295.1m. The counted-once check fails for no component. The one failing check, negative VAT on 76 records, fails onmaintoo. The report flags 22 moves for review: the Winter Fuel Payment and income tax lines, and three poverty or decile figures, the largest pensioners' relative poverty before housing costs in 2027-28 (+0.20pp, +23.5k). In the other years that rate moves by at most 0.01pp.main(56a7fe29f, main69903500e), because it moves no figure here. It brings in Apply 2022–23 annual employee NI for full-year category A directors #2493, Fix program registry model references #2443 and Require complete runtime identity for aggregate-impact data comparisons #2506. Apply 2022–23 annual employee NI for full-year category A directors #2493's employee NI schedule for full-year category A directors has effect only in 2022-23 (in_effectis false from 2023-04-06, and the formula then returns the previous calculation), and its input defaults to false. Fix program registry model references #2443 changes the program registry and Require complete runtime identity for aggregate-impact data comparisons #2506 the aggregate impact tool.Earlier run (2026-10-17), superseded
It used a different dataset and older heads. It is kept for the years, the split by country, the poverty rates and the wider means-test check, which were not re-run. Its figures are not this PR's current ones.
Real runs of the microsimulation on a private copy of the Enhanced FRS 2024-25 (sha256
e433e532b17b), 2024 to 2030: basemainat88a524802against this branch at5c1dbebfa. Nothing is scaled or estimated. £m a year:2025 by country (£m; people charged; households gaining or losing):
household_benefitsandhbai_benefitsrise by the gross payments andhousehold_taxby the charge, sogov_balanceimproves by £74.7m. Northern Ireland households gain £24.4m, because pensioners there not on a relevant benefit were paid nothing onmain.5c1dbebfaagainst95ae26c0b, same base): the 2025 charge falls from £271.1m on 1,703k people to £270.9m on 1,701k. Against the run of 2026-10-16 (basee7610ac52, branch26ff32421),95ae26c0bgives the same figures: the 89 commits ofmainmerged in since move nothing here.Memory and run time (the reason for d1669 = B)
From
26ff32421tod0920a9cd, landlords without finance costs compared both property routes too, so every simulation from 2025 worked out the means-tested benefits on both routes for the whole population. Ond0920a9cd, CI's Python shards reached 15.6 GB used of the runner's 16 GB, and shard 2 lost its runner. B puts backmain's shortcut for those landlords.One microsimulation. The impact run above loads the certified dataset and works out seven variables for 2025, 2026 and 2030 in one process. Measured with
/usr/bin/time -lon a shared host, so the table gives CPU time, not wall time:d0920a9cd, before B75ca17cdd, B61d0bea24, this PR60b09a4e5,mainB cuts the run's CPU time from 101.8 s to 20.5 s and its maximum resident set from 6.20 GB to 3.17 GB.
CI's Python shards. Each shard logs the runner's memory used (
free -m) once a minute. The table gives each shard's highest reading, in MB:aeb1b924d, merged withmain81cbb7089main, merge queue, run 39527691823 (2026-10-21)07985ed61(#2495:main's code at81cbb7089)main, merge queue, run 39546073082 (2026-10-21)629c87c2a(#2512 on81cbb7089)56a7fe29f, merged withmain69903500emain, merge queue, run 39509250381 (2026-10-20)d6a8b8217(#2443:main's code at69903500e)main, three more merge queue groups of 2026-10-20 22:35Z (runs 39509249829, 39509247105, 39509253707)7a8750c61,f669da059,4bd7ea66fd0920a9cdAt the final head, the shards peaked at 12,746 to 13,997 MB. Against
main's own code at the same commit (07985ed61), the four shards differ by −147, +102, +401 and +19 MB. At56a7fe29f, againstd6a8b8217, they differed by −140, −49, +493 and −460 MB.main's merge queue groups on 2026-10-20 and 2026-10-21 peaked at 12,574 to 14,391 MB. Before B,d0920a9cdpeaked 1.6 to 3.0 GB higher, shard by shard. The readings come once a minute, so a peak between two readings is missed.Limits
main. While the charge has effect, that can cost more where the allowance's lower tax ends Pension Credit and with it the exemption. Review r7's lone pensioner pays £5,227.40, including the £200 charge, where expenses would give £5,187.40. The YAML case andtest_a_landlord_without_finance_costs_takes_the_allowance_even_where_it_costs_morepin this.adjusted_total_income.taxable_miscellaneous_income), so a loss on one miscellaneous transaction netted into it also lowers total income, although relief for it belongs at Step 2 (ITA 2007 s.152). Review r10's case: a £6,000 profit and a £2,000 loss on separate transactions, with £30,000 of pension, are £36,000 of total income in law (charged), but £34,000 in the model (not charged).gov.contrib.policyengine.disable_simulated_benefits, household income takes the reported winter payment (winter_fuel_allowance, orpawhpin Scotland while PAWHP is paid), while the charge stays on each person's simulated payment.Axiom
axiom: TheAxiomFoundation/rulespec-uk#420 queued
The legal id is
uk/statute/ukpga/2003/1/681I. s.681I is not in the corpus and no rulespec-uk module encodes it. The issue carries (comment) the module path, the verbatim law, the required outputs, areview_findingand companion tests from the same ground truth as the YAML here. The encoder blocker is recorded in this comment: the signed encoder runs from a corpus citation, anduk/statute/ukpga/2003/1/681Ihas not been ingested (axiom-corpus2137e79be).Notes for merging
61d0bea24keeps both sides inadjusted_total_income.py: this PR's Step 1 fromtotal_income_charged_to_taxwith its net pay add-back, and Restrict the ordinary ATI pension uplift to relief at source #2488'srelief_limit. The net pay deduction stays within Restrict the ordinary ATI pension uplift to relief at source #2488's relieved contributions, because the limit is at least relevant earnings, of which taxable employment income is part. The reference intests/test_total_income_net_pay_properties.pynow takes Restrict the ordinary ATI pension uplift to relief at source #2488's limit for invariant 3.pension_contributions_relief, Pension contribution relief is capped at wages and profit and leaves out statutory pay #2371) merged on 2026-10-19 and came in with the merges ofmain.aeb1b924ddropped the sentence on Pension contribution relief is capped at wages and profit and leaves out statutory pay #2371 fromnet_pay_pension_relief's documentation, as planned.Review
Draft until the hub's review. Max ruled d1669 = B on 2026-10-19. The UK hub adopted this PR on 2026-10-20 after the owner session stopped responding. The changes since review r11 are:
75ca17cdd, written by the owner before the ruling;main:5297da427(61b58544d),567379a28(79fa31727, Means-test the Council Tax Reduction applicant and partner, not the benefit unit's claimant #2078),61d0bea24(60b09a4e5, Restrict the ordinary ATI pension uplift to relief at source #2488 and Uprate three more columns, and guard the gap that hid them #1863) and56a7fe29f(69903500e: Fix program registry model references #2443, Require complete runtime identity for aggregate-impact data comparisons #2506 and Apply 2022–23 annual employee NI for full-year category A directors #2493, no conflict);ab69a95a9;cc2961429: two test files frommainupdated for the charge, after CI's YAML job failed on them at56a7fe29f(2 of 3,959 cases). Include other investment income in total income #2184's £35,000 boundary cases (issuetotal_incomeleaves outother_investment_income, whichincome_taxcharges #2174) inwinter_fuel_allowance.yamlnow check the charge: at £35,001 the pensioner is paid £200 and charged £200, wheremainpaid nothing. Include SSP and SMP in pension-relief earnings #2464's age-75 case instatutory_pay_pension_relief.yamlnow expects £6,086 of income tax, which is £5,886 before the £200 charge (total income £42,000, nothing relieved from age 75);aeb1b924d: drops the sentence innet_pay_pension_relief's documentation on Pension contribution relief is capped at wages and profit and leaves out statutory pay #2371. Include SSP and SMP in pension-relief earnings #2464, which came in with the merges ofmain, fixed it:pension_contributions_reliefnow counts statutory sick and maternity pay as relevant UK earnings.The conflicts and their resolutions are listed in the hand-off comment. These changes need a fresh independent review.
mainhas moved on since the last merge, to81cbb7089(#2462, #2477, #2482, #2495). It merges with this branch with no conflict (git merge-tree), and CI on a pull request runs on the merge commit withmain, so the run onaeb1b924dcovers it. One change touches what this PR reads: #2462 makessalary_sacrifice_returned_to_incomean input that defaults to zero, soemployee_pension_contributions, whichnet_pay_pension_reliefreads, no longer includes the salary-sacrifice excess above the NI cap.Review r8 approved
26ff32421, and review r9 requested the statutory pay fix on5c1dbebfa. Review r10 (GPT-6.1 Sol) approveded0b799eawith body fixes, now made;d0920a9cdqualifies two comments and the fixed changelog fragment after it; review r11 (GPT-6.1 Sol) approvedd0920a9cd, with one body fix, now made.🤖 Generated with Claude Code