Skip to content

Charge the winter fuel payment back through income tax, person by person - #2066

Merged
MaxGhenis merged 45 commits into
mainfrom
winter-fuel-payment-charge
Oct 25, 2026
Merged

MaxGhenis merged 45 commits into
mainfrom
winter-fuel-payment-charge

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

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 charged
Reproducer: 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 main up to 6fa9fb8f5 (2026-10-24, head 16ecb1409), and this PR targets main.

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

  • P is liable if entitled to a winter fuel payment for the qualifying week and P's total income for the tax year exceeds £35,000 (s.681I(1)).
  • The charge equals the payment (s.681I(3)). There is no taper.
  • P is not liable if entitled to Income Support, income-based JSA, Pension Credit, income-related ESA or Universal Credit on any day in the qualifying week (s.681I(4), (5)).
  • "Winter fuel payment" covers payments under SI 2025/969, SSI 2024/351 and NISR 2025/142 (s.681I(6)(a)).
  • Total income is the sum of the amounts of income on which P is charged to income tax (ITA 2007 s.23 Step 1), before the Step 2 reliefs. Each amount is worked out under the rules for that kind of income, so it is after employment expenses (ITEPA 2003 s.11), capital allowances (CAA 2001 s.247), the trading and property allowances (ITTOIA 2005 Pts 7A and 7B), and without exempt income such as ISA interest (ITTOIA 2005 s.694). Employee pension contributions under a net pay arrangement come off employment income (FA 2004 s.193(2)), so total income is after them; contributions relieved at source (s.192) are not deducted. Trade loss relief, including losses brought forward, comes at Step 2.
  • The charge is additional tax at Step 7 of the income tax calculation: ITA 2007 s.30(1) lists "Chapter 9 of Part 10 of ITEPA 2003 (winter fuel payment charge)", and s.23 Step 7 adds it after the Step 6 tax reductions.

What was wrong

On main a 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.

  1. The charge looks at P's own total income. The test lets a whole household through on one low-income member, and charges nobody whose household has one.
  2. The boundary is "exceeds £35,000". The test excludes income of exactly £35,000.
  3. Northern Ireland pays everyone of pensionable age who meets the other conditions from 2025 (NISR 2025/142 reg 2) and the charge applies there. The model pays NI pensioners not on a relevant benefit nothing.
  4. PAWHP is not charged at all.

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.

  • The statute makes it income tax. s.681I(1) creates "a charge to income tax", and ITA 2007 s.30 makes it Step 7 additional tax. The regulations still pay everyone of pensionable age who meets their other conditions in full (SI 2025/969 reg 2; NISR 2025/142 reg 2; SSI 2024/351 as amended).
  • The model already treats the neighbouring charge this way. The high income child benefit charge (ITEPA Pt 10 Ch 8, also in s.30) is CB_HITC inside income_tax, with child_benefit kept gross.
  • Each government's line stays its own. gov_spending counts what DWP, the Department for Communities and Social Security Scotland pay, and gov_tax counts the UK income tax charge. Netting would show an income tax receipt as lower Scottish spending on PAWHP.
  • Entitlement comes before the charge. The shared amounts depend on "living with a person entitled to a winter fuel payment". A person charged in full is still entitled, so the gross payment has to exist in the model either way.

What the choice changes, against netting:

  • household_net_income and hbai_household_net_income are the same under both. Each counts the payment and deducts the tax.
  • The split moves. household_benefits, hbai_benefits and gov_spending carry the gross payment. household_tax, income_tax and gov_tax carry 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 deduct income_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: with pension_credit_income deducting income_tax, the core raises CycleError on income_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 own uc_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_allowance compares each route's income_tax, as on main, 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 main assume 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:

    • One landlord per household, not per benefit unit. A couple's Pension Credit decides whether the partner is entitled to a payment, which changes another benefit unit's shared payment and so its charge. On main landlords 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.
    • Landlords without finance costs keep 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. From 26ff32421 to d0920a9cd these 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 pawhp in household_benefits, hbai_household_net_income and hbai_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_esa and is_on_universal_credit. Since #2027 and #2038 these name the person entitled:

  • a couple's Income Support, income-based JSA, income-related ESA or Pension Credit is the claimant's, the member it is paid to (for Pension Credit, a member who has reached the qualifying age), not the partner's;
  • both joint claimants are entitled to Universal Credit (WRA 2012 s.1(2)(b)), but not a partner who cannot be a joint claimant;
  • anyone else in the benefit unit is entitled only to an award of their own.

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's winter_fuel_payment plus pension_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 holds employment_income gross of pension contributions, takes employee contributions as paid under a net pay arrangement and gives their relief through allowances, 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.
  • New parameters under 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_benefits is 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_test is 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 sets require_benefits to true. Its limit now admits total income equal to it. A saved reform that only moved maximum_taxable_income no longer does anything: the lever for the threshold is now gov.hmrc.income_tax.charges.winter_fuel_payment.income_threshold.
  • Five means-test formulas read income_tax_before_winter_fuel_payment_charge (listed above). uses_property_allowance keeps comparing income_tax; its docstring now says that includes the charge.

Invariants

  1. The charge is 0 or the whole payment, never more than the payment.
  2. income_tax − income_tax_before_winter_fuel_payment_charge = winter_fuel_payment_charge, for every person.
  3. With benefit receipt held fixed, nobody's payment depends on anybody's income: incomes change only the charge. (Income can change receipt of Pension Credit or Universal Credit, and receipt does change the payments, by design.)
  4. No charge on anyone entitled to a relevant benefit or on anyone not entitled to a payment, and, under the baseline parameters, none before 2025 (a reform can switch the charge on earlier; one reform test does).
  5. With benefit receipt held fixed, the charge is nondecreasing in total income: nothing up to and including £35,000, the whole payment above it. So net income has a cliff at £35,000 by design: £1 more income can cost the whole payment.
  6. No means test reads the charge: they deduct income tax before it. A property allowance election compares tax including the charge, so it can change a landlord's tax before the charge, and through it Pension Credit (the YAML cases above).
  7. A trading loss moves the charge only where it changes who is on a relevant benefit or the person's total income, by displacing the trading allowance or by changing which property route the election picks: the relief itself comes at Step 2, after total income. (Review r8's case, with no relevant benefit: a 70-year-old with £31,900 of pension, £3,500 of property receipts (£3,300 of profit) and £2,500 of finance costs. Without a loss the election keeps expenses: total income £35,200, £4,226 of tax including the £200 charge, against £4,366 with the allowance. With a £21,000 trading loss, expenses would leave no tax before the charge but total income still £35,200, so £200 in all; the allowance gives total income of £34,400 and £166 of tax with no charge, so the election switches to it and the charge goes.)
  8. If the property allowance election settles, no landlord with finance costs to relieve can lower their own income tax, charge included, by switching route alone with every other route in the household held fixed. Landlords without finance costs take the allowance without comparing, and while the charge has effect that can cost them more (an intended limit, d1669 = B; review r7's case is pinned). It stops after three rounds whether or not it has settled, so this holds only for households that settle within three. No household has been found that does not: review r8 tried all 16 route combinations in 512 four-landlord households, and all settled by the second round.
  9. Total income is after the employee pension contributions under a net pay arrangement, up to taxable employment income (statutory sick and maternity pay included) and none from age 75. Contributions relieved at source never change that deduction, and with the property allowance election held fixed they never change total income. They can change the route the election picks, and through it total income and the charge. In review r10's case, a 70-year-old with £31,900 of pay, £3,500 of property receipts and £2,500 of finance costs keeps expenses (total income £35,200, charged £200); £21,000 of contributions relieved at source make the allowance the better route (total income £34,400, no charge). For the same inputs, this PR leaves adjusted total income as it was, up to float32 rounding.

Tests

  • YAML, worked by hand from the law: 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 inside income_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.
  • Reform tests: 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 in winter_fuel_allowance/taxable_income_test.yaml (still works when a reform requires a relevant benefit; inert otherwise).
  • Hypothesis (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 and income_tax less 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.
  • Mutation, after the first merge of main: eight mutants that each point one reader at income_tax or drop pawhp from HBAI. Seven are killed by the YAML suites, the Hypothesis properties or a CycleError. The Council Tax Reduction one was killed once a YAML case was added. The eighth, the property allowance election comparing income_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.
  • Mutation, before the merge of 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 of income_tax; Winter Fuel Payment still requiring a benefit; tax credits exempting). All ten fail at least one case.
  • Means-test tests: four existing files entered income_tax to drive a means test (Housing Benefit applicable income and net earnings, the Income Support family definitions, Pension Credit income). They now enter income_tax_before_winter_fuel_payment_charge, with the same expected values. So do two cases of main's later Income Support statutory pay file (statutory_pay.yaml Cases 6 and 8, commit 16ecb1409). Thirteen Housing Benefit cases still enter income_tax (ten from main's Exclude disregarded income from Housing Benefit assessment #2211, three in housing_benefit_trading_loss.yaml); they pass for the right reason because the worked pre-charge tax is £0, and a post-Budget follow-up converts them. A Housing Benefit net earnings case checks that the charge does not come off a charged pensioner's earnings (8,628.50, where income_tax would give 8,578.50).
  • Property tests that compare a means test with income tax now compare with the tax before the charge, as the means tests deduct it: the UC earnings deduction identity, the Council Tax Reduction Pension Credit routes, the CTR carer income identities, the Housing Benefit savings-credit-only identities, the pre-UC earned income deducts the whole benefit unit's tax, including tax on pensions and other unearned income #1942 UC reference, and the trade loss properties (means tests see the loss only through tax; relief taxes like an income cut; a zero-profit trade against a loss). Two of the first group were false inside their own generators until the fix: a mixed-age couple's pensioner earning £40,000 off UC, and a pensioner with £39,500 of total income claiming CTR. Both counterexamples are now plain tests that also assert the charge is levied. The trade loss properties also check invariant 7. The zero-profit property pins a pensioner whose loss ends Pension Credit (£217.48) and so brings in the £200 charge, and one charged on both sides. "Income tax falls with the loss by at most the relief" now holds the tax before the charge: review r6 found an 80-year-old with £19,200 of pension and £30,000 of dividends whose £1,100 loss ends a £3.12 Pension Credit award and so brings in the £300 charge. That case is pinned.
  • Property allowance election (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 own income_tax by 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, by test_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 checks main's shortcut, which holds because the couples are 40 and so are never charged. At d0920a9cd, before B, electing per benefit unit failed the pensioner property. That mutant was not re-run after B.
  • Pension contributions in total income (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 at pension_contributions_relief (which also fails the statutory sick pay YAML case).
  • Tests that need a variable with a formula: income_tax now only adds, so test_variable_code_sharing and test_rule_comparison use income_tax_before_winter_fuel_payment_charge.
  • Tests that entered total income for the charge now enter 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 reads total_income, enter a private pension instead, which feeds both.
  • Local (locked venv, core 3.33.2, Python 3.13):
    • on cc2961429: the two YAML files from main that CI's YAML job failed on at 56a7fe29f (see Review), winter_fuel_allowance.yaml (14 cases) and statutory_pay_pension_relief.yaml (12): 26 passed, and a mutant of each new charge expectation fails;
    • on 56a7fe29f (2026-10-21): the charge YAML (39 cases), its reform tests charge_parameters.yaml (5) and taxable_income_test.yaml (18), Restrict the ordinary ATI pension uplift to relief at source #2488's adjusted_total_income_pension_relief.yaml (14) and Apply 2022–23 annual employee NI for full-year category A directors #2493's ni_class_1_full_year_director_2022.yaml (10), run with policyengine-core test as CI's YAML job runs them: 86 passed;
    • on 56a7fe29f: every Python test file this PR changes, 11 files and 65 tests, including test_property_income_properties.py with the pinned r7 limit and test_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 new test_program_registry.py and test_ni_full_year_director_2022.py: 110 passed;
    • on 61d0bea24 (2026-10-20, before the last merge of main): the income tax, Winter Fuel Payment and Pension Credit YAML directories, the Winter Fuel Payment and charge reform tests, pawhp.yaml and winter_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.
  • Latest merge of main (9a3eb8fed, main 6fa9fb8f5): main had moved Housing Benefit's income sum into hb_income_before_deductions, so the one conflict was resolved by applying this PR's tax change there. 16ecb1409 then fixes main's new Income Support cases as above. An independent delta review approved 16ecb1409. CI at that head is the gate for landing.
  • CI at the earlier head 3a3800e24 (run 40046674035, on its merge commit with main 29c5a0a98): every check passed, including Test / Result and Aggregate impact (gh pr checks exit 0 at 01:47Z).
  • CI: at the earlier head aeb1b924d (run 39550799793, on its merge commit with main 81cbb7089), all 15 checks passed (gh pr checks exit 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. At 56a7fe29f (run 39542312053), the Python shards had passed too, but the YAML job failed 2 of 3,959 cases, both in test files from main that assumed no charge; cc2961429 fixes 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, commit 2526edbb), whose SHA-256 056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784 was checked before each run. Base main at 60b09a4e5 (the commit merged in) against this PR at 61d0bea24, 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:

Year Variable main £m this PR £m change £m main recipients (k) this PR recipients (k)
2025 Income tax 299,379.3 299,674.2 +294.9 40,076 40,076
2025 Winter fuel payment charge 0.0 294.9 +294.9 — 1,788
2025 Winter Fuel Payment 1,696.8 1,872.2 +175.5 9,839 10,769
2025 Pension Age Winter Heating Payment 190.0 190.0 +0.0 994 994
2025 Pension Credit 6,660.9 6,660.9 +0.0 1,490 1,490
2025 Household net income 1,658,129.5 1,658,010.1 -119.4 31,167 31,167
2025 (people using the property allowance, k) 41.4 41.4
2026 Income tax 319,142.7 319,453.8 +311.0 41,176 41,176
2026 Winter fuel payment charge 0.0 311.0 +311.0 — 1,867
2026 Winter Fuel Payment 1,679.3 1,864.0 +184.7 9,706 10,680
2026 Pension Age Winter Heating Payment 197.4 197.4 +0.0 983 983
2026 Pension Credit 7,035.9 7,035.9 +0.0 1,489 1,489
2026 Household net income 1,706,643.1 1,706,516.7 -126.3 31,285 31,285
2026 (people using the property allowance, k) 40.4 40.4
2030 Income tax 385,484.3 385,881.0 +396.8 42,969 42,969
2030 Winter fuel payment charge 0.0 396.8 +396.8 — 2,461
2030 Winter Fuel Payment 1,547.5 1,806.9 +259.4 8,871 10,359
2030 Pension Age Winter Heating Payment 215.4 215.4 +0.0 990 990
2030 Pension Credit 7,341.8 7,341.8 +0.0 1,454 1,454
2030 Household net income 1,850,301.4 1,850,164.1 -137.4 31,808 31,808
2030 (people using the property allowance, k) 38.9 38.9
  • Income tax rises by exactly the charge in each year. Income tax before the charge is the same as main's income tax, up to £100 of float rounding on a £299bn total.
  • The charge raises £294.9m in 2025 from 1.79m people, £311.0m from 1.87m in 2026 and £396.8m from 2.46m in 2030. The model holds the £35,000 threshold at that amount in every year (the Treasury may change it by order, s.681J).
  • The Winter Fuel Payment rises by £175.5m in 2025, £184.7m in 2026 and £259.4m in 2030, because everyone of pensionable age in England, Wales and Northern Ireland who meets the other conditions is now paid.
  • PAWHP and Pension Credit do not move: their totals and recipient counts are identical to the penny in each year. So does the number of people using the property allowance (41.4k in 2025).
  • Household net income falls by £119.4m in 2025: £175.5m more paid, less £294.9m charged. It falls by £126.3m in 2026 and £137.4m in 2030.
  • d1669 = B changes no record. The B commit 75ca17cdd, against the reviewed head d0920a9cd it sits on, both on this dataset: no person's, benefit unit's or household's income_tax, winter_fuel_payment_charge, winter_fuel_payment, pension_age_winter_heating_payment, uses_property_allowance, pension_credit or household_net_income differs by 1p in 2025, 2026 or 2030. That run kept per-record arrays only locally and deleted them after the comparison.
  • CI's Aggregate impact report agrees. Its run on 56a7fe29f (39542311989) compares the merge commit 5e1793b2c with main 69903500e. Its run on aeb1b924d (39550799800) compares the merge commit 5a8fbd04c with main 81cbb7089. 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 on main too. 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.
  • At the merged head 16ecb1409, CI's Aggregate impact run 40116160149 compares the merge commit 1f3ee3bf02 with main 6fa9fb8f55, on the same certified dataset (enhanced_frs_2024_25, policyengine-uk-data 1.58.1). Over 2025-26 to 2031-32, income tax rises £293.3m to £422.1m a year and Winter Fuel Payment spending £175.4m to £293.8m, with 0.78m to 1.30m more recipients; household net income falls £117.9m to £141.7m a year. No check is broken by the change; negative VAT on the data#565 records fails on both sides. Of the 27 flagged moves, the poverty one is 2031-32 relative poverty after housing costs: 64,876 more people (43,267 pensioners, 11,555 children, 10,054 working-age adults), the same people as at 3a3800e24 (64,923 entering, 47 pensioners leaving); in the other years every poverty rate moves by at most 0.01pp.
  • At the earlier head 3a3800e24, CI's Aggregate impact run 40046674016 compares the merge commit 08bdbf4c27 with main 29c5a0a98, on the same certified dataset:
    • income tax rises £293.3m (2025-26) to £422.1m (2031-32), and the Winter Fuel Payment £175.4m to £293.8m, with 784k to 1.30m more recipients;
    • household net income falls £117.9m to £128.3m;
    • 0 checks broken; the negative-VAT check fails on both sides, as on main;
    • 27 moves are flagged: the Winter Fuel Payment and income tax lines, six decile means (£108 to £179 a household), and one poverty figure.
    • The poverty flag: relative poverty after housing costs for pensioners in 2031-32, 12.38% to 12.75% (+43.3k). In that year relative poverty after housing costs rises for every group (+64.9k people: +43.3k pensioners, +11.5k children, +10.0k working-age adults), while absolute poverty after housing costs falls by 56 people. In every other year relative poverty after housing costs moves by under 0.003pp. The relative line is 60% of each simulation's own median (in_relative_poverty_ahc). The hub's trace confirms a higher line, not lower incomes at the bottom.
      • Method: the hub reran 2031-32 locally on the same certified dataset (enhanced_frs_2024_25, uk-data 1.58.1) for main 29c5a0a98 and for this head merged into it. The merge's tree is 611f63428, the same as CI's merge commit 08bdbf4c2.
      • Reproduction: on both sides, recomputing the flag from 60% of the person-weighted median of equivalised income after housing costs reproduces in_relative_poverty_ahc exactly.
      • The median and line: the median rises from £37,500.65 to £37,527.15 (+£26.50), so the line rises from £22,500.39 to £22,516.29 (+£15.90).
      • Who enters poverty: 64,923 people (43,315 pensioners, 11,555 children, 10,054 working-age adults). Every one of them has the same equivalised income after housing costs on both sides, to within half a penny. 47 pensioners leave.
      • Whose income changes: the income of 3,928 sampled households (2.97m weighted people) changes, and none of them enters relative poverty.
      • Status: the flag is a relative-line effect, so it doesn't block the merge.
    • The earlier run's flag (pensioners' relative poverty before housing costs in 2027-28, +0.20pp) does not recur at this head: that rate moves by −0.001pp.
  • These runs were not repeated after the last merge of main (56a7fe29f, main 69903500e), 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_effect is 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: base main at 88a524802 against this branch at 5c1dbebfa. Nothing is scaled or estimated. £m a year:

Year WFP base £m WFP branch £m PAWHP base £m PAWHP branch £m Charge £m Net winter payments base £m Net winter payments branch £m HNI change £m HBAI net income change £m gov_spending change £m gov_tax change £m gov_balance change £m
2024 441.3 441.3 34.7 34.7 0.0 476.0 476.0 +0.0 +0.0 +0.0 +0.0 +0.0
2025 1,701.4 1,897.6 201.1 201.1 270.9 1,902.5 1,827.8 -74.7 -74.7 +196.1 +270.9 +74.7
2026 1,690.2 1,886.1 208.2 208.2 278.7 1,898.4 1,815.6 -82.8 -82.8 +195.9 +278.7 +82.8
2027 1,644.3 1,847.7 211.2 211.2 296.3 1,855.6 1,762.7 -92.9 -92.9 +203.4 +296.3 +92.9
2028 1,619.5 1,832.8 215.5 215.5 302.3 1,835.0 1,745.9 -89.1 -89.1 +213.2 +302.3 +89.1
2029 1,615.7 1,840.8 220.7 220.7 316.0 1,836.4 1,745.5 -90.9 -90.9 +225.1 +316.0 +90.9
2030 1,604.4 1,849.1 226.2 226.2 339.9 1,830.6 1,735.3 -95.2 -95.2 +244.7 +339.9 +95.2

2025 by country (£m; people charged; households gaining or losing):

Year Country Gross base Gross branch Charge People charged HNI change Households gaining Households losing
2025 England 1,567.8 1,715.1 198.2 1,299k -50.9 155k 650k
2025 Wales 107.3 128.0 24.7 141k -4.1 0k 38k
2025 Scotland 201.1 201.1 44.1 236k -44.1 0k 228k
2025 Northern Ireland 26.3 54.5 3.8 24k +24.4 121k 0k
  • The charge raises £270.9m in 2025 from 1.70m people, and £339.9m from 2.20m in 2030. The model holds the £35,000 threshold at that amount in every year (the Treasury may change it by order, s.681J).
  • Gross spending on the Winter Fuel Payment rises £196.1m in 2025 (£1,701.4m to £1,897.6m), because everyone of pensionable age in England, Wales and Northern Ireland who meets the other conditions is now paid. PAWHP spending does not change, because this PR changes only PAWHP's documentation, not who is paid; the charge recovers £44.1m of it in 2025.
  • Household net income falls £74.7m in 2025: £196.1m more paid, less £270.9m charged. household_benefits and hbai_benefits rise by the gross payments and household_tax by the charge, so gov_balance improves by £74.7m. Northern Ireland households gain £24.4m, because pensioners there not on a relevant benefit were paid nothing on main.
  • Means-tested benefits: no benefit unit's Pension Credit, Universal Credit, Income Support, ESA, JSA, Housing Benefit, Council Tax Reduction, tax credits or benefit cap reduction moves by 1p or more in any year (the largest change is £0.0000). The means tests deduct income tax before the charge.
  • 2024 is unchanged in every line: the charge starts in 2025-26.
  • The net pay fix alone (5c1dbebfa against 95ae26c0b, 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 (base e7610ac52, branch 26ff32421), 95ae26c0b gives the same figures: the 89 commits of main merged in since move nothing here.
  • Poverty rates move by at most 0.01 percentage points in any year (the largest: pensioner poverty after housing costs in 2025, 13.21% to 13.20%).

Memory and run time (the reason for d1669 = B)

From 26ff32421 to d0920a9cd, 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. On d0920a9cd, CI's Python shards reached 15.6 GB used of the runner's 16 GB, and shard 2 lost its runner. B puts back main'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 -l on a shared host, so the table gives CPU time, not wall time:

Commit CPU time (s) Maximum resident set (GB) Peak memory footprint (GB)
d0920a9cd, before B 101.8 6.20 4.83
75ca17cdd, B 20.5 3.17 2.78
61d0bea24, this PR 23.3 3.19 2.89
60b09a4e5, main 20.4 2.63 3.14

B 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:

Run Commit Shard 1 Shard 2 Shard 3 Shard 4
this PR, final head, run 39550799793 (2026-10-21) aeb1b924d, merged with main 81cbb7089 13,514 12,879 13,997 12,746
main, merge queue, run 39527691823 (2026-10-21) 07985ed61 (#2495: main's code at 81cbb7089) 13,661 12,777 13,596 12,727
main, merge queue, run 39546073082 (2026-10-21) 629c87c2a (#2512 on 81cbb7089) 13,628 12,708 13,531 12,707
this PR, run 39542312053 (2026-10-21) 56a7fe29f, merged with main 69903500e 13,008 12,525 13,864 12,639
main, merge queue, run 39509250381 (2026-10-20) d6a8b8217 (#2443: main's code at 69903500e) 13,148 12,574 13,371 13,099
main, three more merge queue groups of 2026-10-20 22:35Z (runs 39509249829, 39509247105, 39509253707) 7a8750c61, f669da059, 4bd7ea66f 12,750–13,611 12,722–14,293 13,584–14,391 12,631–13,404
before B, run 39160946751 (2026-10-18) d0920a9cd 15,569 15,553 (runner lost) 15,554 15,579

At 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. At 56a7fe29f, against d6a8b8217, 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, d0920a9cd peaked 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

  • Property allowance election, landlords without finance costs (d1669 = B). They take the allowance whenever it lowers taxable property income, without comparing tax, as on 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 and test_a_landlord_without_finance_costs_takes_the_allowance_even_where_it_costs_more pin this.
  • Total income. The charge's total income is only as complete as the model's taxable income components. Statutory paternity pay is left out because the model does not yet tax it (Charge income tax on statutory paternity pay #2368). The model deducts deficiency relief from dividend income, and that deduction carries over, as it already did into adjusted_total_income.
  • Miscellaneous income is held as one net figure per person (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).
  • Reported benefits. With gov.contrib.policyengine.disable_simulated_benefits, household income takes the reported winter payment (winter_fuel_allowance, or pawhp in Scotland while PAWHP is paid), while the charge stays on each person's simulated payment.
  • Receipt in the qualifying week. As for the payments, "entitled to a relevant benefit on any day in the qualifying week" is read as a positive annual award.

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, a review_finding and 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, and uk/statute/ukpga/2003/1/681I has not been ingested (axiom-corpus 2137e79be).

Notes for merging

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:

The conflicts and their resolutions are listed in the hand-off comment. These changes need a fresh independent review.

main has moved on since the last merge, to 81cbb7089 (#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 with main, so the run on aeb1b924d covers it. One change touches what this PR reads: #2462 makes salary_sacrifice_returned_to_income an input that defaults to zero, so employee_pension_contributions, which net_pay_pension_relief reads, no longer includes the salary-sacrifice excess above the NI cap.

Review r8 approved 26ff32421, and review r9 requested the statutory pay fix on 5c1dbebfa. Review r10 (GPT-6.1 Sol) approved ed0b799ea with body fixes, now made; d0920a9cd qualifies two comments and the fixed changelog fragment after it; review r11 (GPT-6.1 Sol) approved d0920a9cd, with one body fix, now made.

🤖 Generated with Claude Code

R13 follow-up (r14)

At f23c3d6efe5c00df4944f6247f0596c4b2178f48, both P2 findings are fixed:

  • The fiscal partition declares income tax's NI eligibility read through the WFP charge's Pension Credit exemption. TV licence retains its NI read and drops its stale final-income-tax read. Tax and benefit arithmetic is unchanged.
  • The trading-loss property holds pre-cap UC alongside final UC, preserving Housing Benefit's entitlement passport, and pins the exact reduced Scottish family whose HB applicable income moved from £0 to £45,976.93. All existing holds and assertions remain.

Local validation in a frozen workspace venv with two foreground xdist workers: 51 Python tests passed (39 fiscal partition, 12 trading-loss properties) and 432 YAML cases passed (Pension Credit, WFP receipt/allowance/charge, statutory-pay pension relief and the listed WFP reforms). No failures, errors or skips. Changed Python Ruff format/lint, repository-wide Ruff format check and whitespace checks passed. Both batch starts met load1 <= 110 and free disk >= 60 GB decimal. These counts are local validation; the full head CI run follows the push.

Aggregate guard precision (r15)

Reproducer: policyengine_uk/tests/aggregate_impact/test_aggregate_impact_guard.py::test_a_large_tax_head_is_containment_only_when_it_responds_one_for_one

The counted-once checker compared a reader's response with a tolerance based on its tax level. This incorrectly classified exactly zero income-tax response as £1-for-£1 containment in 12 high-tax households in run 39928975233. The reader, baseline-held and first-point-held comparisons now scale their tolerance with the perturbation and observed response. A zero response cannot qualify at any positive perturbation size; genuine one-for-one containment remains detected, including behind another head's response. The linear-response screen uses the same scale. r13's fiscal declarations and pre-cap UC trading-loss hold are unchanged.

Tests were added before the fix: the original guard produces 3 failures and 36 passes, including the flat £300,000 case and the held-recheck regression. New controls cover £100,000 and undeclared readers; Hypothesis covers tax levels from £0 to £1bn and perturbations from £0.000001 to £1m.

Validation at 76108fafe74d9ee8d73e8d7a871aa723f39bfb20: 313 passed, no failures/errors/skips/xfails, 7 warnings, in 1,700.39 seconds. Foreground -n 2 --dist loadfile: aggregate_impact/ 262 passed (all 9 certified-data tests, including the former NI failure), test_fiscal_partition.py 39 passed, test_trading_loss_properties.py 12 passed. Changed-file Ruff format/check, repository-wide Ruff format check (1,949 files), and whitespace checks pass. Certified dataset pin/SHA verified by the test fixture. Resource gates passed; recorded tests used load1 below 110, free disk above 60 GB decimal, compressor below 30 GB, and BLAS/OpenMP threads at 2.

MaxGhenis and others added 2 commits October 2, 2026 07:45
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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

#2038 merged into main as 4012143, so this PR's base is now main. It branches from #2038 at 071d5170b. Since then #2038 gained the following, and this branch needs a merge of main before its tests and impact numbers count:

  • partner_receipt_counts ("paid to" versus couple receipt);
  • a payee-only is_on_pension_credit limited to members over the qualifying age;
  • paid_relevant_benefit / reports_paid_relevant_benefit for the couple's payee, replacing reports_relevant_benefit;
  • is_on_universal_credit excluding uc_is_ineligible_partner;
  • main's dated age test in winter_fuel_payment_eligible.

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 11, 2026 11:57
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>
MaxGhenis and others added 2 commits October 11, 2026 22:09
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>
MaxGhenis and others added 2 commits October 12, 2026 08:40
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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

#2047 merged into main on 2026-10-13 as e9e1f5d87. It adds the same pawhp line after winter_fuel_allowance in household_benefits.py, hbai_household_net_income.py and hbai_benefits.py that this PR adds. When this branch next merges main, keep one copy of each line. tests/code_health/test_household_income_lists.py (from #2047) and tests/code_health/test_aggregation_lists.py fail on a duplicate.

#2047's test_pawhp_household_income_properties.py already allows for the s.681I charge: net income falls by pawhp less the change in income tax.

…harge

# Conflicts:
#	policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py
MaxGhenis and others added 2 commits October 14, 2026 20:25
…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>
MaxGhenis and others added 6 commits October 15, 2026 14:55
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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub: I pushed the job's merge e72300cb5 to this branch as a fast-forward from aeb1b924d. The job's remaining targeted tests didn't run because disk was below Fleet ops' 55 GB floor, so CI at this head is the test evidence. The hub will dispatch an independent review at this head once CI reports. The job's own hand-off follows.

Round 3 continuation hand-off (the job could not post this itself):

Local head: e72300cb5546ee2d7edfd2bfbb39a6a43497fab0 (parents PR aeb1b924d, main fe0b99a36). The merge is committed, and each conflict starts from saved resolution c9e9e0da2. All three new Pension Credit earnings-tax reads use pre-charge tax; current main's statutory-pay sources, earnings disregards and notional self-employment deductions remain. New PC/CTR test oracles deduct the same pre-charge tax.

Passed locally: all 41 WFP-charge YAML cases (including r12's £1,740 reproducer and the £400 employment-tax / £31,054 PC-income case); uvx ruff format --check . (1,947 files). Locked Python 3.13.15 / core 3.33.2.

Remaining targeted tests could not start: disk remained around 53.6 GB, below Fleet ops' paced 55 GB floor. The trading-loss launcher waited without spawning pytest and was stopped. The PR stays draft. Current-head full suites, aggregate impact and CI memory are not available; existing impact evidence remains historical.

The hub can resume from .job/adopt-2066-r3c.bundle in job 20261022-103023-adopt-uk2066-r3c; .job/HANDOFF.md and .job/RESOLUTIONS.md contain recovery commands and resolution details. Git metadata is workspace-local because the linked metadata is sandbox read-only. No self-review or merge, no remaining test process, and job uv/Hypothesis caches removed.

MaxGhenis and others added 2 commits October 23, 2026 20:02
Resolves three conflicts with #2368, which taxes statutory paternity pay
and counts it as Housing Benefit earnings: both docstrings now name
paternity pay and the tax before the winter fuel payment charge, and the
net earnings tests keep #2368's two paternity pay cases and this branch's
charge case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#2368 taxes statutory paternity pay, so it is in total income, and with
this branch's charge a pound of it can take total income over £35,000 and
raise income_tax by the whole winter fuel payment. The bound in
test_statutory_paternity_pay_income_tax_properties.py now reads
income_tax_before_winter_fuel_payment_charge, names the charge as an
intended cliff next to Marriage Allowance, and a new case checks the
cliff: £35,000 of pension and savings plus £1 raises income tax by
£200.20, 20p before the charge.

total_income_charged_to_tax no longer says the model does not tax
statutory paternity pay, and the changelog and the Pension Credit
statutory pay test's docstring now say that it does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis force-pushed the winter-fuel-payment-charge branch from 82a940a to b1505cd Compare October 24, 2026 00:02
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged current main into this branch. #2368 (statutory paternity pay taxed, and counted as HB earnings) conflicted in three files, so the merge queue had marked this PR UNMERGEABLE. I took it out of the queue to push. Head is now b1505cdf4.

  • cad06c5a9: the merge. Both sides are kept: the two docstrings name paternity pay and the pre-charge tax, and housing_benefit_net_earnings.yaml keeps Charge income tax on statutory paternity pay #2368's two paternity cases and this branch's charge case. The PR's diff against main is unchanged apart from those docstrings. The hub's independent check confirmed every auto-merged file matches git's own merge.
  • b1505cdf4: two semantic breaks that showed up as no text conflict, found by that check.
    • Charge income tax on statutory paternity pay #2368's property test bounded a pound of paternity pay at a pound of income_tax. With the charge, a pound that takes total income over £35,000 costs the whole £200 payment. The bound now reads income_tax_before_winter_fuel_payment_charge and names the s.681I cliff as an intended exception, as Marriage Allowance already is. A new case runs the counterexample: a 70-year-old with £35,000 of pension and savings, plus £1 of paternity pay, pays £200.20 more income tax, and 20p more before the charge.
    • total_income_charged_to_tax said the model "does not yet tax statutory paternity pay", which is false since Charge income tax on statutory paternity pay #2368; that clause is removed.
    • The changelog and the Pension Credit statutory pay test's docstring now say that income tax sees paternity pay.

No model formula changes. I ran nothing locally (UK Budget certification reservation); CI is running, and a delta check is in progress. I'll re-queue the PR pinned to this head once both pass.

…nt 2

The delta review found two more cliffs that break invariant 2's pound-for-a-pound
bound: a pound across the basic rate limit halves the Personal Savings
Allowance (ITA 2007 s. 12B; £100.40 of tax on wages of £48,270 and £2,000 of
savings interest), and a pound across a £200 step adds 1% of the year's Child
Benefit (ITEPA 2003 s. 681C(3)(b)). Both are on main and wages cross them the
same way. The bound now skips draws where the recipient's savings allowance
changes and leaves the Child Benefit charge out, and a test crosses each cliff.

Also corrects invariant 4's caveat (relevant UK earnings include statutory
pay) and the Pension Credit test's comment on the pay it draws.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

UK hub pushed 3a3800e24 (fast-forward over b1505cdf4; tests only). It answers the delta review of the main merge.

  • Invariant 2 of test_statutory_paternity_pay_income_tax_properties.py (paternity pay raises pre-charge tax by at most a pound): the review found two more cliffs that the generator can reach. Both are already on main, and wages cross them the same way.
    • A pound across the basic rate limit cuts the Personal Savings Allowance from £1,000 to £500 (ITA 2007 s. 12B): £100.40 of tax.
    • A pound across a £200 step of the High Income Child Benefit Charge adds 1% of the year's Child Benefit (ITEPA 2003 s. 681C(3)(b)).
  • The docstring now names all four cliffs: Marriage Allowance, the savings allowance, the Child Benefit charge and the winter fuel payment charge.
  • The assertion now skips draws where the recipient's savings allowance changes, and bounds pre-charge tax less the charge. Two deterministic tests cross each new cliff: wages £48,270 with £2,000 of savings interest; and wages £50,000, one child, paternity pay £10,199 to £10,200.
  • Also corrected:
    • invariant 4's caveat: relevant UK earnings include statutory pay through taxable_employment_income (utils/pensions.py), so the wages-cover-contributions restriction is conservative;
    • the Pension Credit test's comment on the pay invariant 1 draws.
  • Checks: no model code changes, and nothing has run locally while the host is reserved, so CI at this head is the first run. A delta review is under way, and the audit follows CI and its verdict.

Main moved Housing Benefit's income sum into hb_income_before_deductions.
The resolution takes main's version and applies this branch's change
there: the tax deducted is income tax before the winter fuel payment
charge, plus National Insurance.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis removed this pull request from the merge queue due to a manual request Oct 24, 2026
Main's statutory pay cases for Income Support set income_tax as an input
to isolate the deduction. Income Support now deducts income tax before
the winter fuel payment charge, so the cases set that variable instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis added this pull request to the merge queue Oct 24, 2026
Merged via the queue into main with commit 6a08106 Oct 25, 2026
20 checks passed
@MaxGhenis
MaxGhenis deleted the winter-fuel-payment-charge branch October 25, 2026 00:57
@MaxGhenis

Copy link
Copy Markdown
Collaborator Author

Merged by the UK hub (Claude session 8e19f9d5) at head 16ecb1409, through the merge queue with the head pinned. That head is the reviewed head 3a3800e24 plus two hub commits: 9a3eb8fed merges main 6fa9fb8f5 (main had moved Housing Benefit's income sum into hb_income_before_deductions; the resolution takes main's version and applies this PR's change there), and 16ecb1409 changes main's new Income Support statutory pay cases to enter income_tax_before_winter_fuel_payment_charge, which Income Support now deducts, in place of income_tax (CI's Test / YAML at 9a3eb8fed had failed Case 8, 2,850 against 2,750).

  • What it does: it charges the Winter Fuel Payment back through income tax, person by person, as the 2026 Act's charge requires: £200 recovered from people with income above £35,000. Means tests deduct pre-charge tax.
  • Reviews:
    • Round 15 (GPT-6.1 Sol) approved 76108fafe. It covered the charge, the Pension Credit income fix, the fiscal-partition declarations and the aggregate checker's tolerance fix (0.02 × |expected| + 2e-6 × |observed|).
    • Main merge. cad06c5a9 merged main f66f0a205, because Charge income tax on statutory paternity pay #2368 (paternity pay taxed and counted as Housing Benefit earnings) conflicted in three files. Each side's change was kept.
    • The independent check of the merge requested changes twice. Both rounds were fixed, in tests and documentation only:
      • b1505cdf4: Charge income tax on statutory paternity pay #2368's paternity-pay property had a pound-for-a-pound bound, which the winter fuel payment cliff broke. The bound now uses pre-charge tax, and a test crosses that cliff. A stale "does not yet tax paternity pay" sentence went.
      • 3a3800e24: the same bound also broke on the Personal Savings Allowance halving (ITA 2007 s. 12B) and the Child Benefit charge's whole-point steps (ITEPA 2003 s. 681C(3)(b)), both already on main. It now skips savings-allowance crossings and nets the charge, and a test crosses each cliff.
    • APPROVE at this exact head (in-session Opus 5.5, because the Codex lanes were closed). It found the bound true for every draw the generator can make, and the new tests' values right: £100.40, and £13 + £0.40.
  • Delta review r16 (in-session Opus 5.5; the Codex lanes were closed): APPROVE at 16ecb1409 (REVIEW_2066_R16.md). The PR's change appears once after the merge; every means test that deducts income tax (HB, IS, Pension Credit, CTR) reads it before the charge; mutating the HB or IS reader back to income_tax is caught (89 and 9 failures). The test-input change keeps main's meaning and restores Case 6's power.
  • CI: every check passed at 3a3800e24, including Test / Result and Aggregate impact, and again at 16ecb1409 before the merge queue took it. (At 9a3eb8fed, Test / YAML failed as above and the Python shards were cancelled.)
  • Impact (certified data, enhanced_frs_2024_25 on uk-data 1.58.1; CI's Aggregate impact run 40046674016, this head merged into main 29c5a0a98):
    • income tax rises £293.3m in 2025-26, growing to £422.1m in 2031-32;
    • Winter Fuel Payment spending rises £175.4m to £293.8m, and its recipients by 784k to 1.30m;
    • household net income falls £117.9m to £128.3m;
    • 0 checks broken; sign:vat fails on both sides, as on main;
    • relative poverty after housing costs moves by under 0.003pp in every year but 2031-32. That year it rises for every group: +43.3k pensioners (12.38% to 12.75%, the one poverty flag), +11.5k children and +10.0k working-age adults. Absolute poverty after housing costs falls by 56 people that year. The relative line is 60% of each simulation's own median (in_relative_poverty_ahc). The hub's local trace of 2031-32, at the same merge tree as CI, confirms a higher line. The median rises £26.50, so the line rises £15.90. All 64,923 people who enter relative poverty have unchanged income, and the 3,928 sampled households whose income changes don't enter. The PR body has the details.
  • At the merged head 16ecb1409, CI's Aggregate impact run 40116160149 compares the merge commit 1f3ee3bf02 with main 6fa9fb8f55, on the same certified dataset (enhanced_frs_2024_25, policyengine-uk-data 1.58.1). Over 2025-26 to 2031-32, income tax rises £293.3m to £422.1m a year and Winter Fuel Payment spending £175.4m to £293.8m, with 0.78m to 1.30m more recipients; household net income falls £117.9m to £141.7m a year. No check is broken by the change; negative VAT on the data#565 records fails on both sides. Of the 27 flagged moves, the poverty one is 2031-32 relative poverty after housing costs: 64,876 more people (43,267 pensioners, 11,555 children, 10,054 working-age adults), the same people as at 3a3800e24 (64,923 entering, 47 pensioners leaving); in the other years every poverty rate moves by at most 0.01pp.
  • Follow-ups:
    • 13 HB cases enter income_tax, which HB no longer deducts (10 from main's Exclude disregarded income from Housing Benefit assessment #2211 in housing_benefit_applicable_income.yaml, 3 in housing_benefit_trading_loss.yaml). They still pass for the right reason, since the worked pre-charge tax is £0, but they should enter income_tax_before_winter_fuel_payment_charge to isolate the deduction. The hub's post-Budget follow-up PR does this.
    • #2606, post-Budget: an undeclared tax head can hide a £1 containment above about £490,000 of baseline.
    • The property test's personal allowance margin relies on even-pound base amounts; a comment goes in the hub's follow-up PR.
    • utils/pensions.py:30 names only SSP and SMP.
  • Why now: current law for the Budget baseline, and Max asked on 22 Oct to clear the UK PR backlog before the Budget.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model Winter Fuel Payment high-income recovery separately from entitlement

1 participant