Repository navigation
Pin benefit cap exemption rules with regression tests and legal references (#1818) - #1820
Conversation
|
@MaxGhenis review request — benefit cap exemption defects found while investigating #1818. The thing to decide is timing, not correctness. The main fix (using earnings-attributable tax rather than total income tax in the reg. 82 test) reduces capping and moves with the known over-capping. But parameterising the stale earnings threshold raises it from GBP 846 to GBP 881/month for 2026-27, which removes exemptions and increases capping — correct by law, wrong direction for #1818. 2025 is unchanged, since the new parameter resolves to GBP 846 exactly. So this makes 2026 capping modestly worse while #1818 is open. It is still right, but you may prefer it lands alongside the calibration work rather than before it. Worth knowing: AFIP produces no numeric change. The variable has no formula and nothing in policyengine-uk-data populates it. The exemption is legally correct to add, but nobody should expect the number to move. I would rather that were stated than discovered later. The dead-code removal keeps return semantics byte-identical apart from AFIP, and no pre-existing policy test changed (1118 → 1130, all additions) — which was the signal I asked to watch for. Gated |
|
Coordination with #1944 (mixed-age couples on UC: benefit cap and CTR pensioner status). The UC benefit cap has no age exception that reaches a mixed-age couple. UC Regs 2013 regs 79, 82 and 83 turn only on earnings and benefit receipt; the one age-based exception, SI 2014/1230 reg 60C, covers only claims where every claimant has reached the qualifying age; and GOV.UK says "If you're part of a couple and one of you is under State Pension age, the cap may apply." #1944 therefore replaces Two effects on this PR:
#1944 leaves the dead 🤖 Generated with Claude Code |
|
Coordination with #1999 (fixes #1986), which rewrites
When rebasing, keep #1999's 🤖 Generated with Claude Code |
|
Coordination with #2076, stacked on #2027. #2076 removes the unused benefit sums from Whichever lands second: keep this PR's age or earnings test, and drop the |
|
Coordination with #2089 (each scheme its own benefit cap), stacked on #2084. #2089 deletes the shared exception files this PR edits (
This PR's earnings-test fix overlaps #1999, which ports the same way, into the UC earnings file. Its other defect fixes belong in whichever scheme's file the regulation names. |
Review of #1820 — Fix benefit cap exemption defects (#1818)Base repository: PolicyEngine/policyengine-uk Source Documents
CriticalNone. Should AddressNone. SuggestionsNone. Evidence GapsNo material gaps reported. Notes
Validation Summary187 passed, 11.34s pytest /15.06s CLI; actual ordinary threshold/pension and asymmetric partner AFIP/member-order cases through final UC/HB/net income; named source-base earnings control completed (sensitivity only). Original statutory/government/JRF sources read with SHA-bound extracts. current-pr-1820-review-sources.json; current-pr-1820-review-source-cache-check.json. No duplicate calculation diagnostics; original PDF extraction via bundled pypdf. Canonical source-manifest validator exited 0. CI at 35efb46: Install + smoke-import (py3.9)=SUCCESS, Install + smoke-import (py3.10)=SUCCESS, Install + smoke-import (py3.11)=SUCCESS, Install + smoke-import (py3.12)=SUCCESS, Install + smoke-import (py3.13)=SUCCESS, Install + smoke-import (py3.14)=SUCCESS, Lint=SUCCESS, Test=SUCCESS. Detailed code, policy, source and reproduction artifacts were retained locally. Timingelapsed seconds: 1151.70s; setup seconds: 272.25s Review SeverityAPPROVE. Open findings: 0 critical, 0 should address, 0 suggestions. |
35efb46 to
ea2b87c
Compare
Conflicts: - uc_earned_income.py: main's documentation and references, reading the branch's uc_earned_income_before_work_allowance; the work allowance still comes off it (max(0, before - work allowance)). - is_benefit_cap_exempt_earnings.py: the branch's version for now; it is reworked on top of #1820 in the next merge. Semantic follow-ups to main's claimant rule (is_uc_assessed_claimant, from #1978): uc_earned_income_before_work_allowance and benefit_cap_earned_income now sum over the assessed claimants with add_for_members, as main's uc_earned_income did. uc_individual_earned_income_before_mif is main's (the branch's copy was identical). benefit_cap_earned_income documents that miscellaneous_income is counted as the award counts it (#1961). The CTR helper comment names the Default Scheme regulations correctly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch is stacked on #1820 and must not merge before it. #1820's changes (AFIP exception, dated earnings threshold parameter, earnings-attributable tax in the reg. 82 test, dead-code removal, its YAML tests, changelog fragment and the dividend xfail removal) are Vahid's and land through #1820. Conflicts: - is_benefit_cap_exempt_health_disability.py: main's claimant scoping (not_a_claimant, add_for_members) kept; #1820's removal of the dead earnings block and its AFIP entry applied on top. - is_benefit_cap_exempt_earnings.py: #1820's version. The next commit replaces only its earned income measure for #1986. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1820 measures the benefit cap's earned income as earnings less earned_income_tax, national_insurance and pension_contributions. That still deducts tax on pension and property income, all Class 3 (the #1986 counterexample: a partner's 923 of voluntary Class 3 removed a couple's exception) and contributions paid after 75. Replace only that measure with benefit_cap_earned_income: each claimant's uc_individual_earned_income_before_mif (earnings less their own tax and NI on those earnings and relievable pension contributions), summed over the claimants, without minimum income floor income (reg. 82(4)). #1820's structure, threshold parameter and comments stay; the comments its measure made untrue are edited. Reg. 82(1) excepts "an award of universal credit", so the exception no longer lifts the cap on a Housing Benefit award without Universal Credit (HB's own work exception is WTC entitlement, HB Regs 2006 reg. 75E). Families with neither award keep the income test, so #1820's YAML cases are unchanged. Threshold: drop #1999's own NLW derivation (benefit_cap_earnings_threshold, uc_benefit_cap_earnings_threshold_met and the earnings_exception parameters) in favour of #1820's gov.dwp.benefit_cap_earnings_exemption, extended back with cited values: 430 a month from 29 April 2013 (reg. 82(1)(a) as made), then 520, 542 and 569 for 2017-18 to 2019-20 (S.I. 2017/138; NMW Regs 2015 reg. 4; reg. 6(1A)(za)). A differential test checks every dated value from April 2017 against floor(16 x NLW x 52 / 12) from gov.hmrc.minimum_wage. Tests: the cap YAML moves onto the parameter with boundary pairs for 2016, 2017, 2018, 2019 and 2023 (722 a month); the property file computes the income test from the parameter and loads the model once per module; a rent pin joins #1820's de-xfailed dividend case; the floor childcare case names the responsible carer, since main now defaults the nomination to the elder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Coordination note on #1999 (fixes #1986), replacing the 1 October note above. #1999 is now stacked on this PR at
Once this PR is on main, #1999 merges main and its diff reduces to those. 🤖 Generated with Claude Code |
Includes the reform-contract and tax-band fixes; preserves the stack on #1820 without conflicts. Co-Authored-By: OpenAI Codex <noreply@openai.com>
Four defects in the benefit cap exception logic: 1. The earnings exception subtracted total income tax liability from earnings, so tax on property, savings, dividend and pension income reduced measured net earnings. Regulation 55(5) of the Universal Credit Regulations 2013 deducts only the tax paid in respect of the employment, primary Class 1 NI and relievable pension contributions. Now uses earned_income_tax, national_insurance and pension_contributions. 2. The earnings threshold was hardcoded as 10_152 in three files. Moved to the dated parameter gov.dwp.benefit_cap_earnings_exemption, expressed monthly as reg. 82 and DWP do, with values for 2020-21 to 2026-27 derived from 16 hours a week at the National Living Wage. 3. Armed Forces Independence Payment is a statutory exception but was never checked. Added to the health/disability qualifying benefits. 4. Removed dead copy-pasted code from all three is_benefit_cap_exempt_* variables, keeping each one's return semantics unchanged apart from defect 3. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s fix makes it pass The earnings exception to the benefit cap now nets only earned_income_tax off earnings, so tax on dividends no longer removes it (#1986). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ea2b87c to
4a5174c
Compare
Resolve conflicts with #2399, #2076 and #1965: - is_benefit_cap_exempt_earnings: keep main's per-claimant uc_individual_earned_income_before_mif test and documentation; keep this PR's dated threshold parameter in place of 10_152 and its reg 57 and FA 2004 s.188 references. - is_benefit_cap_exempt_health_disability: take main's version (#2076) and add the UC route for AFIP: on a Universal Credit award only a claimant's counts; otherwise Housing Benefit's young person counts too. - is_benefit_cap_exempt: take main's label. - test_uc_single_claim_properties: keep main's AFCS assertions and this PR's partner-only AFIP example. Pin main's young-person AFIP YAML case to the Housing Benefit route, and give the specified-benefits property oracle the family's scheme. Rewrite the changelog fragment to what the PR still changes relative to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@MaxGhenis thanks for the merge-order note. I've merged Conflicts
Two of main's tests changed because they assumed AFIP had the same scope in both schemes. The What the PR still changes relative to main: the threshold parameter (2025-26 stays at £846 a month, 2026-27 rises to £881), the AFIP split by scheme, and the 17 regression cases in Tests (fresh venv, at 01e90fe): benefit cap YAML 110 passed, |
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed 01e90fe28badede6d11e2fe51e679e08f4b94fee. Both blockers from my earlier change request are resolved, and I am approving.
- A qualifying young person’s AFIP no longer exempts a UC claimant from the cap, while the separate Housing Benefit route remains. The paired final-award regressions pass: UC £22,054.80 and legacy HB £30,000. This matches UC reg.83(1)(c) and HB reg.75F(1)(ea).
- The earnings exception preserves main’s earnings-specific tax/NI helper, including company earnings and statutory pay, without a separate company term. The £10,300 wages/£5,000 pension case now deducts £0 tax on earnings and pays UC £33,876.72.
- All seven 2020–2026 thresholds match the DWP table; below/at/above-threshold final-UC checks pass. The Reproducer and axiom lines are present.
Validation:174 focused tests and diagnostics passed at this head; all captured hosted CI checks pass. I inspected the changed Hypothesis tests but did not execute them locally because the existing environment lacks Hypothesis.
|
#2089 merged on 2026-10-17 as
|
#2089 split the benefit cap by scheme and deleted the shared is_benefit_cap_exempt_health_disability. Its UC and HB specified-benefit variables already place armed forces independence payment as this PR did (UC Regs 2013 regs 2, 83(1)(c); HB Regs 2006 reg 75F(1)(ea)), so the deleted file is dropped and the two property tests take main's version. - The earnings threshold parameter follows the move into is_uc_benefit_cap_exempt_earnings. - The earnings YAML cases move to is_uc_benefit_cap_exempt_earnings.yaml under the new variable names; the AFIP cases there are dropped, main covers them, except the paired final-award cases, kept in benefit_cap_specified_benefits.yaml. - test_uc_single_claim_properties.py keeps the partner AFIP example, asserted against the scheme-split variables. - test_benefit_cap_lha_scheme_split_properties.py: the old shared earnings reference reads the threshold parameter instead of 10_152. - Changelog no longer claims the AFIP fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@MaxGhenis thanks for the #2089 note. I've merged The three conflicts
What follows from that
Benefit cap YAML 105 passed, 🤖 Generated with Claude Code |
juaristi22
left a comment
There was a problem hiding this comment.
Re-reviewed ce3e0bccc01879c5eed826d6d7756b4fb1b0286c.
The main merge and #2089 conflict resolutions preserve the previously approved benefit-cap behavior. The remaining dated earnings parameter is unchanged from the verified 2020–2026 table. The renamed earnings test aggregates claimant earnings before the minimum income floor and annualises the monthly threshold once.
162 focused tests pass, including the pension-tax and young-person AFIP final-award regressions. Current-head hosted checks pass. The changed property tests were inspected; Hypothesis was unavailable in the selected local runtime, so their execution evidence is hosted CI. I am renewing approval for this commit.
Review status: COMPLETE. No open critical findings.
|
@vahid-ahmadi, #2536 (item 2 of #2523) also replaces the hard-coded 10,152 in The thresholds agree for 2020-21 to 2026-27 (£604 to £881 a month). The difference is where they come from:
Your tests run on #2536's threshold. I merged this branch onto #2536 locally, keeping #2536's threshold, and ran the tests this PR adds or changes:
One new case also fails on main without #2536, so main has moved since it was written: "A dependent young person's AFIP does not lift the Universal Credit cap, so the award is capped". It gives Proposal: once #2536 lands, drop 🤖 Generated with Claude Code |
…on cap test Main (5092abf) now restricts the UC higher disabled child addition to UC Regs 2013 reg 24(2)(b) (DLA care highest, PIP daily living enhanced or Scottish equivalents, or blindness), so the 17-year-old's AFIP no longer adds 6,176.52. Update the test's pre-cap UC and cap reduction; the capped award (22,054.80) and the cap outcome are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@juaristi22 the merge queue run failed on one test this PR adds: "A dependent young person's AFIP does not lift the Universal Credit cap". I merged main and changed that test's expected values in 9dcd1cc. No model code changed. Why: main now limits the UC higher disabled child addition to the conditions in UC Regs 2013 reg 24(2)(b) (5092abf): DLA care at the highest rate, PIP daily living at the enhanced rate, the Scottish equivalents, or blindness. AFIP is not on that list, so the 17-year-old no longer gets the £6,176.52 addition. Pre-cap UC goes from 52,792.44 to 46,615.92 and the cap reduction goes from 30,737.64 to 24,561.12. The household is still capped and the capped UC is unchanged at 22,054.80. I also added Locally, the full YAML suite passes (3,912) and so do the benefit cap Python tests (65). 🤖 Generated with Claude Code |
Aggregate impact on certified data: passBase Checks: 0 broken by this change, 1 failing on both sides, 0 fixed. Moves flagged for review: 0 (thresholds in Notes
Moves flagged for reviewNone. Spending by benefit and service
Caseloads
Household net income by decileNo household's net income changes in any year. No change in any year: receipts by tax head; government balance, each component counted once; model totals; components the model counts twice; household income totals; population; 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. 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
|
Resolve the conflict with #2523 (9387f49), which derives the UC benefit cap earnings threshold from 16 hours at the national living wage in uc_benefit_cap_earnings_exception_threshold. is_uc_benefit_cap_exempt_earnings now reads that variable, keeping this PR's per-claimant earned income comments and references. The PR's own dated parameter gov.dwp.benefit_cap_earnings_exemption is removed: main's variable gives the same 604 to 881 a month for 2020-21 to 2026-27 and also covers earlier years, so keeping both would be two sources for one threshold. The changelog fragment drops the parameter bullet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@juaristi22 I merged main (88fd2be) to clear the conflict with your #2523 (9387f49). 🤖 Generated with Claude Code |
|
Merged by the UK hub (session local_8e19f9d5, Opus 5.5) on Max's explicit instruction to merge the team's ready PRs before the Budget (21 October, about 23:00Z: "go ahead and merge them. none of us has time to wait on each other"). One merge-queue enqueue, pinned to head
🤖 Generated with Claude Code |
Fixes defects in the benefit cap exceptions, found while investigating #1818 (the model caps 222k UC households against DWP's 111k for Great Britain, November 2025). None of them is the main cause of the over-capping, which is believed to lie in calibration and rents.
Reproducer:
policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_uc_benefit_cap_exempt_earnings.yaml::The 2026-27 earnings threshold rises to 881 a monthaxiom: TheAxiomFoundation/rulespec-uk#399 queued (UC reg 82 threshold and earned income)
Main is merged in at 07534c3 (merge 88fd2be). Four of this PR's original fixes have since landed on main and are kept as they are there:
uc_individual_earned_income_before_mif(which since Model Universal Credit reg 77 for owner-managers of a company #1965 includes company owner earnings).is_uc_benefit_cap_exempt_specified_benefitcounts armed forces independence payment (AFIP) for a claimant only (UC Regs 2013 regs 2 and 83(1)(c)), andis_housing_benefit_benefit_cap_exempt_specified_benefitcounts it for the claimant, partner or a young person (HB Regs 2006 reg 75F(1)(ea)). That is the AFIP fix this PR made in the sharedis_benefit_cap_exempt_health_disability, which Give Universal Credit and Housing Benefit their own benefit cap and LHA shared-accommodation rules #2089 deleted, so this PR no longer changes that code.10_152earnings threshold withuc_benefit_cap_earnings_exception_threshold: 16 hours at the national living wage × 52 ÷ 12, fraction of a pound disregarded (UC Regs 2013 reg 82, reg 6(1A)(za)), and £430 a month before April 2017. This PR had added a dated parameter,gov.dwp.benefit_cap_earnings_exemption, for the same threshold. The merge drops that parameter and reads main's variable, so there is one source for the threshold. Main's variable gives the same monthly figures this PR's parameter held (£604, £617, £658, £722, £793, £846, £881 for 2020-21 to 2026-27, each matching the figure DWP published) and also covers 2017-18 to 2019-20.What this PR still changes relative to main is the regs 55, 57 and Finance Act 2004 s. 188 references and comments on
is_uc_benefit_cap_exempt_earnings, plus regression tests. The reproducer now passes on main as well, since #2523 fixed the threshold.Tests
policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_uc_benefit_cap_exempt_earnings.yaml, 10 cases: the same earnings either side of the 2025-26 and 2026-27 thresholds, unearned income and couples, and the earnings regressions from the earlier review (tax on a private pension; one partner's pension contributions against the other's pay; contributions after 75 in a mixed-age couple). Before Pension annual allowance on survey data, the UC benefit-cap earnings threshold, and the loss-relief pension rule: fixes before Microcosm UK locks the next release #2523 the reproducer failed on main; all 10 now pass on main too.benefit_cap_specified_benefits.yaml: the paired final-award AFIP cases from the earlier review, now in the scheme-split names (a young person's AFIP on UC leaves the award capped at £22,054.80; on legacy HB it is paid in full at £30,000). They pass on main and pin the award, which main's flag-only case does not.test_uc_single_claim_properties.pygenerates AFIP for both members and keeps the partner-only AFIP example, asserted against the UC and HB specified-benefit variables.Results at 88fd2be (fresh venv):
test_benefit_cap_*.py,test_abolish_benefit_cap_scenario.py,test_disable_simulated_benefits_benefit_cap.py,test_uc_deductions.pyandtest_uc_single_claim_properties.py: 74 passed.Refs #1818.
🤖 Generated with Claude Code