Repository navigation
Fix benefit cap exemption defects (#1818) - #1820
vahid-ahmadi wants to merge 7 commits into
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
|
Independent review (GPT-6.1 Sol, UK hub, 2026-10-12): REQUEST_CHANGES at @vahid-ahmadi: two blockers, each reproduced with a household (earnings floored after combining both partners' deductions; pension contributions deducted after 75), and one should-fix (AFIP recipient scope). The full review is below. Would you like to fix these yourself, or should the hub push the fixes as new commits on your branch, without rewriting yours? Say which here. If there's no reply by tomorrow, the hub will open them as a separate PR against your branch for you to merge or not. CI: the shard a runner shutdown killed has been re-run. VERDICT: REQUEST_CHANGES
The £846/£881 thresholds match DWP’s table. The rebase retains reg. 3(3) masking and routes AFIP through it. 20 benefit-cap YAML cases and all 6 capital-income Python tests passed, serially after checking memory. The removed strict xfail passes for the intended dividend-tax correction; its assertions were preserved. No skips or new xfails were added. The saved impact outputs reconcile to +£10.248m cap reduction, +4,017.7 capped benefit units and −£10.256m UC, supporting the hub’s rounded figures. Both runs used the same dataset, and their cap formulas match this head. When #2076 merges main, preserve the dated threshold and corrected earnings calculation—including the fixes above. Its current earnings rewrite restores £10,152 and total income tax. Retain AFIP and claimant masking while tightening recipient scopes, move AFCS/ESA out of
|
A child's Armed Forces Independence Payment excepted the whole benefit unit, because AFIP was summed over every member. HB Regs 2006 reg 75F(1)(ea) names the claimant, partner or a young person they are responsible for, not a child under 16; for Universal Credit AFIP is within "attendance allowance" (UC Regs 2013 reg 2), which excepts only where a claimant receives it (reg 83(1)(c)). AFIP now counts for the claimant, partner or a Housing Benefit young person. The child test now expects false, with a new young-person test. The AFCS test drops its is_benefit_cap_exempt_other assertion, which #2076 removes when it moves AFCS into is_benefit_cap_exempt_health_disability, and asserts is_benefit_cap_exempt instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed 70e426c, fixing two test expectations found while reviewing #2076, and the formula behind one of them. A child's AFIP no longer lifts the cap. The test expected true because AFIP was summed over every member of the benefit unit, so the formula was wrong as well as the test. HB Regs 2006 reg 75F(1)(ea) names "the claimant, the claimant's partner or a young person", not a child. For Universal Credit, AFIP sits inside "attendance allowance" (UC Regs 2013 reg 2), and reg 83(1)(c) excepts only where "a claimant is receiving" it. AFIP now counts for the claimant, the partner or a Housing Benefit young person. The 10-year-old case expects false, and a new case covers a 17-year-old qualifying young person (true). The old formula fails the corrected child test. AFCS test. Changelog updated to match. Tests: benefit cap YAML 21 passed, dwp YAML 458 passed, full YAML policy suite 3071 passed, the 17 pytest files that touch the benefit cap 210 passed and 1 xfailed. Ruff clean. @juaristi22 could you review when you have a moment? |
juaristi22
left a comment
There was a problem hiding this comment.
Reviewed 70e426c01768ceb08c379cd877154c0d6bc31819. I am requesting changes on two benefit-cap exceptions:
-
is_benefit_cap_exempt_health_disability.py:67-75puts the young-person AFIP route into the shared exemption used for Universal Credit. HB reg. 75F(1)(ea) allows a qualifying young person, but UC reg. 83(1)(c) requires a claimant receiving attendance allowance (which includes AFIP under reg. 2). A UC claimant whose dependent young person alone receives AFIP is therefore wrongly exempted. Please separate the UC and HB routes and test both final awards; the new YAML case currently asserts the shared result. -
is_benefit_cap_exempt_earnings.py:37-42subtractsearned_income_tax, which includes tax on private pension income. UC regs. 55(5)(b) and 57(2) require tax in respect of employment or self-employment. On this head, £10,300 wages plus £5,000 private pension produces £546earned_income_taxversus £0uc_income_tax_on_earnings, incorrectly removing the earnings exception. Please use the earnings-specific tax measure and add a regression case.
The changed benefit-cap YAML suite passed 13 cases locally, but it does not catch these outcomes. Please also add the required axiom: status to the PR description. Full hosted CI was pending at review time.
|
@MaxGhenis thanks, I'll fix these myself on this branch today. |
The reg 82 earnings exception now sums each assessed claimant's uc_individual_earned_income_before_mif. Each person's earnings are floored at nil before the couple's are added (UC Regs 2013 reg 57(2)), only tax and NI in respect of employment or self-employment are deducted (reg 55(5)(b), so no tax on private pension income), and contributions paid after 75 are not deducted (FA 2004 s.188(3)(a)). Armed forces independence payment now counts only for a claimant on Universal Credit (reg 2, reg 83(1)(c)), and for the claimant, partner or a young person on Housing Benefit (HB Regs 2006 reg 75F(1)(ea)). Adds regressions for each, with final UC and HB awards, and AFIP to the single-claim property tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Pushed e9b20e5 on top of 70e426c (no rebase, as the branch merges cleanly). I checked each point against legislation.gov.uk and agree with all of them, so there are no push-backs on the law.
@MaxGhenis (hub review at 4a5174c)
Reproducer. The description has a Coordination. The earnings test is now what #1999 planned (per-claimant earned income, no Class 3, no tax on pensions), so #1999's diff on top of this should shrink to the pre-2020 threshold values and the #1986 childcare and CTR changes. One thing for #2076: its Tests (fresh venv, policyengine-core 3.33.2, worktree import path confirmed):
@juaristi22 @MaxGhenis could you re-review? |
…dants' holdings Addresses the GPT-6.1 Sol round-3 review at e6ebb9c. - Reg 57(2) step 3 deducts income tax and NI paid in respect of any trade from the combined trading result before the nil rule. Earned income took all tax and NI on earnings off the total after flooring the trade, so a company loss that cancelled a taxed trade left that trade's tax coming off pay (12,000 pay, 20,000 own-trade profit, -20,000 company income gave 7,668.20 instead of 12,000). New uc_income_tax_on_trade (tax on earnings less the tax on the other earnings alone, which take the lower slice as PAYE codes give the allowance to an employment) and uc_national_insurance_on_trade (Class 2 and 4), each capped at the earnings figure in use, which can be an input. uc_income_tax_on_earnings is unchanged; its computation moved to utils/uc_income_tax.py so both take slices of one computation. - is_benefit_cap_exempt_earnings is main's again. A company term there carried signed losses and dividend tax into the test; #1820 rebases the test on uc_individual_earned_income_before_mif, which includes company earnings, and on the 16-hour National Living Wage threshold. The two cap YAML cases (one of which blessed main's out-of-date 10,152) go. - household_uc_unreported_company_holdings becomes household_uc_pool_company_holdings: every holding except a reporting unit's claimant's or partner's leaves the household pool, so a dependant's holding in a reporting family no longer counts for an unrelated unreported claimant. - Tests: the review's deduction-order case and its control, the reporting-family dependant holding case; properties now check the reg 57(2) order, an independent no-tax identity at low incomes, LCWRA owners get no floor, and disregarded holdings count for no unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merge-order note from the UK hub (2026-10-13). #1965 (UC reg 77 for company owner-managers) merged today as This PR's head One more thing to be aware of: main's release tests currently fail on 🤖 Generated with Claude Code |
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.
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 ce3e0bc. Three 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.What this PR still changes relative to main is the earnings threshold, plus regression tests.
The earnings threshold was hardcoded
is_uc_benefit_cap_exempt_earningson main compares earned income with a literal10_152(£846 a month, the 2025-26 value). It is now a dated monthly parameter,gov.dwp.benefit_cap_earnings_exemption: 16 hours at the reg 4 rate × 52 ÷ 12, with the fraction of a pound disregarded (UC Regs 2013 reg 82).Each matches the figure DWP published. 2025-26 is unchanged; 2026-27 rises to £881, which caps more households earning between the two figures.
test_benefit_cap_lha_scheme_split_properties.py(from #2089) re-adds the old shared earnings formula as a reference, with the10_152literal, and compares it with the new schemes at 2026. Its reference now reads the same parameter, so it keeps testing the split rather than 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), which pass on main too. Run against main's earnings formula, the reproducer fails and the other 9 pass.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 ce3e0bc (fresh venv):
gov/dwpYAML: 606 passed. Full YAML policy suite: 3,587 passed (run before the last, non-overlapping main merge).test_uc_single_claim_properties.py,test_benefit_cap_specified_benefits_properties.pyandtest_benefit_cap_lha_scheme_split_properties.py: 351 passed, 1 xfailed (the existing strict one for Marriage Allowance is booked on the recipient as the transferor's unused allowance, not the fixed s.55B transfer #1947)..github/check_fix_pr.pyagainst this body and head: PASS.Refs #1818.
🤖 Generated with Claude Code