Skip to content

Fix benefit cap exemption defects (#1818) - #1820

Open
vahid-ahmadi wants to merge 7 commits into
mainfrom
fix/benefit-cap-exemption-defects
Open

vahid-ahmadi wants to merge 7 commits into
mainfrom
fix/benefit-cap-exemption-defects

Conversation

@vahid-ahmadi

@vahid-ahmadi vahid-ahmadi commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 month

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

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_earnings on main compares earned income with a literal 10_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).

Tax year NLW × 16 × 52 ÷ 12 Threshold
2020-21 £8.72 £604.59 £604
2021-22 £8.91 £617.76 £617
2022-23 £9.50 £658.67 £658
2023-24 £10.42 £722.45 £722
2024-25 £11.44 £793.17 £793
2025-26 £12.21 £846.56 £846
2026-27 £12.71 £881.23 £881

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 the 10_152 literal, 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.py generates 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):

  • Ruff: format and check clean on the changed Python files.
  • Benefit cap YAML: 105 passed. gov/dwp YAML: 606 passed. Full YAML policy suite: 3,587 passed (run before the last, non-overlapping main merge).
  • The 28 pytest files that touch the benefit cap or AFIP, including test_uc_single_claim_properties.py, test_benefit_cap_specified_benefits_properties.py and test_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.py against this body and head: PASS.

Refs #1818.

🤖 Generated with Claude Code

@vahid-ahmadi
vahid-ahmadi requested a review from MaxGhenis August 13, 2026 13:05
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

@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 Test job passes.

@MaxGhenis

MaxGhenis commented Sep 30, 2026 •

Copy link
Copy Markdown
Collaborator

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 has_pensioner in is_benefit_cap_exempt_other with a new housing_benefit_pension_age_regulations_apply. Under HB Regs 2006 reg 5, pension-age HB has no cap, and a family on UC is never under it.

Two effects on this PR:

  1. is_benefit_cap_exempt_other.py conflicts. Keep Apply the benefit cap to mixed-age couples on Universal Credit, and keep them off pensioner Council Tax Reduction #1944's formula; the AFCS and contributory-ESA terms are unchanged.
  2. This PR's new comment in is_benefit_cap_exempt_earnings says households with pension income are "generally already exempt through the state pension age exception". After Apply the benefit cap to mixed-age couples on Universal Credit, and keep them off pensioner Council Tax Reduction #1944, that no longer holds for a mixed-age couple on UC, which is capped unless an earnings, health or disability exception applies. Please reword it when rebasing.

#1944 leaves the dead has_pensioner blocks in _earnings and _health_disability for this PR to remove. Your "State pension age excepts from the benefit cap" case (single, aged 70) still passes on #1944.

🤖 Generated with Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator

Coordination with #1999 (fixes #1986), which rewrites is_benefit_cap_exempt_earnings. It covers this PR's defects 1 and 2:

When rebasing, keep #1999's is_benefit_cap_exempt_earnings.py, and drop benefit_cap_earnings_exemption.yaml and the earnings cases that duplicate #1999's benefit_cap_earnings_exception.yaml. AFIP and the dead-code removal in _health_disability and _other still stand. #1999 leaves those two files alone, and #1944 rewrites _other.

🤖 Generated with Claude Code

@MaxGhenis

Copy link
Copy Markdown
Collaborator

Coordination with #2076, stacked on #2027.

#2076 removes the unused benefit sums from is_benefit_cap_exempt_other.py and is_benefit_cap_exempt_earnings.py. It also moves the afcs and ESA terms out of is_benefit_cap_exempt_other into is_benefit_cap_exempt_health_disability, limited to the claimant and partner. Before, any member's AFCS or contributory ESA lifted the cap.

Whichever lands second: keep this PR's age or earnings test, and drop the afcs and esa_support_component terms and the dead QUAL_* lists from _other and _earnings.

@MaxGhenis

Copy link
Copy Markdown
Collaborator

Coordination with #2089 (each scheme its own benefit cap), stacked on #2084.

#2089 deletes the shared exception files this PR edits (is_benefit_cap_exempt*.py) and gives each scheme its own:

  • UC: universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py (reg 82) and _specified_benefit.py (reg 83);
  • HB: housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_*.py (75E, 75F, reg 5).

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.

@juaristi22

Copy link
Copy Markdown
Collaborator

Review of #1820 — Fix benefit cap exemption defects (#1818)

Base repository: PolicyEngine/policyengine-uk
PR number: 1820
Reviewed head SHA: 35efb46
Merge base SHA: 3d12d26
Mode: full
Scope: changed behavior and affected dependencies
Source evidence: Official sources are linked below; local source copies and validation artifacts were retained.
Review status: COMPLETE

Source Documents

Critical

None.

Should Address

None.

Suggestions

None.

Evidence Gaps

No material gaps reported.

Notes

  • Snapshot imports verified; no private population run.
  • Property-income capital premise is unestablished; sensitivity only, not a confirmed finding.
  • Changed threshold/earnings/AFIP rules corroborated; inherited mixed-age rule and residual pension tax are explicit scope notes.

Validation Summary

187 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.

Timing

elapsed seconds: 1151.70s; setup seconds: 272.25s

Review Severity

APPROVE. Open findings: 0 critical, 0 should address, 0 suggestions.

@vahid-ahmadi
vahid-ahmadi force-pushed the fix/benefit-cap-exemption-defects branch from 35efb46 to ea2b87c Compare October 8, 2026 15:12
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
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>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
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>
MaxGhenis added a commit that referenced this pull request Oct 11, 2026
#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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator

Coordination note on #1999 (fixes #1986), replacing the 1 October note above.

#1999 is now stacked on this PR at ea2b87cec and depends on it: it stays a draft and will not merge before this PR does. It keeps this PR's threshold parameter, the AFIP exception and the exception-file changes as they are. On top it adds only:

Once this PR is on main, #1999 merges main and its diff reduces to those.

🤖 Generated with Claude Code

MaxGhenis added a commit that referenced this pull request Oct 12, 2026
Includes the reform-contract and tax-band fixes; preserves the stack on #1820 without conflicts.

Co-Authored-By: OpenAI Codex <noreply@openai.com>
vahid-ahmadi and others added 2 commits October 12, 2026 08:22
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>
@vahid-ahmadi
vahid-ahmadi force-pushed the fix/benefit-cap-exemption-defects branch from ea2b87c to 4a5174c Compare October 12, 2026 07:15
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at 4a5174c. The threshold parameter (benefit_cap_earnings_exemption.yaml), the AFIP exception and the exception files keep the same names and paths, so #1999 can merge main cleanly.

@MaxGhenis

Copy link
Copy Markdown
Collaborator

Independent review (GPT-6.1 Sol, UK hub, 2026-10-12): REQUEST_CHANGES at 4a5174c8c.

@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
Head reviewed: 4a5174c

  1. [blocker] is_benefit_cap_exempt_earnings.py:42 floors earnings after combining claimants’ deductions. In 2026, one spouse earning £10,600 and a self-employed spouse earning £100 with £3,000 pension contributions produce £7,700 here. Legally, the latter’s earnings floor at zero, leaving £10,600 and an exemption. Reproduced with £5,638/year wrongly capped. Sum individually floored earnings, preferably using uc_individual_earned_income_before_mif with the assessed-claimant mask, and add a cross-partner deduction property test. UC reg. 57(2).

  2. [blocker] is_benefit_cap_exempt_earnings.py:40 deducts pension contributions after age 75, although these are not relievable. A mixed-age couple with the older partner earning £11,000 and contributing £1,000 wrongly loses its 2026 exemption; reproduced with £16,631/year capped. Apply the existing relievability age guard, or reuse the individual earnings helper, and add a mixed-age regression. Use each rule's own Universal Credit earned income in the benefit cap, targeted childcare and local CTR #1999’s planned correction does not fix this head. Finance Act 2004 s.188(3)(a).

  3. [should-fix] is_benefit_cap_exempt_health_disability.py:46 applies AFIP to every unmasked member. UC defines AFIP as attendance allowance, whose paid-receipt exception requires a claimant. A dependent 17-year-old’s AFIP incorrectly exempts the family. Give AFIP separate statutory recipient scopes for UC and HB; replace the child-positive test:151, and add AFIP to the single-claim masking property tests. UC definition, reg. 83(1)(c).

  4. [nit] Correct two documentation claims: 1818.fixed.md:1 says pension-income tax no longer erodes earnings, although that approximation remains; remove “and pension.” test_uc_income_from_capital_properties.py:38 says property income remains in the earnings-tax base, although taxable_property_income is explicitly excluded; correct that explanation.

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 _other, and rerun the cap and single-claim property tests.

gh could not connect; the diff and comments were read through the GitHub connector. No files were edited or GitHub writes made.

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>
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

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. is_benefit_cap_exempt_other: true was an output assertion, not an input stub, so the test did run the formula. But #2076 moves AFCS out of _other into _health_disability, so that line would break when it lands. It now asserts _health_disability and is_benefit_cap_exempt, with a comment citing UC reg 83(1)(d)-(e) and HB reg 75F(1)(d) and 75F(2)(a) (a guaranteed income payment is a war pension). AFCS still counts for any member here except a non-joint-claimant partner; #2076 limits it to the claimant and partner.

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 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 70e426c01768ceb08c379cd877154c0d6bc31819. I am requesting changes on two benefit-cap exceptions:

  1. is_benefit_cap_exempt_health_disability.py:67-75 puts 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.

  2. is_benefit_cap_exempt_earnings.py:37-42 subtracts earned_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 £546 earned_income_tax versus £0 uc_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.

@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

@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>
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

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.

@juaristi22

  1. AFIP young-person route in the UC exception. Fixed in e9b20e5. AFIP now has a separate route for each scheme. For a family with a UC award, only an assessed claimant's AFIP counts, because AFIP is "attendance allowance" under UC reg 2 and reg 83(1)(c) needs "a claimant is receiving" it. Otherwise the HB route applies: claimant, partner or young person (HB reg 75F(1)(ea)). I picked the route on universal_credit_pre_benefit_cap > 0, as benefit_cap_couple does in Lift the benefit cap only for the support component and the people HB reg 75F and UC reg 83 name, and cap only the family's benefits #2076. Two new cases check the final awards for the same family, where only the 17-year-old gets AFIP. On UC the cap applies, cutting the award from £52,792.44 to £22,054.80 (this fails on 70e426c). On legacy HB the cap does not apply and the full £30,000 is paid. That case passes both before and after, as a control. Without the AFIP it would be capped by £7,945.
  2. earned_income_tax includes tax on private pension income. Fixed in e9b20e5. The exception now adds up each assessed claimant's uc_individual_earned_income_before_mif, which deducts uc_income_tax_on_earnings and uc_national_insurance_on_earnings (so no voluntary Class 3 either). Your case is a regression with two children in London in 2025: £546 against £0, as you found. With the earnings exception the family's UC is now £33,876.72. On 70e426c it was capped by £10,805.32.
  3. axiom: line. Added to the description. It points to rulespec-uk Bump version #399 (reg 82 threshold and earned income) and Add semver actions #425 (reg 83 and reg 75F, including AFIP), both queued.

@MaxGhenis (hub review at 4a5174c)

  1. Earnings floored after combining deductions (blocker). Fixed in e9b20e5 by the same change as María's point 2. As you suggested, I used uc_individual_earned_income_before_mif with the is_uc_assessed_claimant mask, so each person's earnings are floored at nil before the couple's are added (UC reg 57(2)). It is read before the minimum income floor, as reg 82(4) requires. New case: £10,600 of wages plus a self-employed partner with £100 of profit and £3,000 of contributions, 2026. This is now exempt and pays UC of £31,074.08. On 70e426c it was capped by £8,088.48.
  2. Pension contributions after 75 (blocker). Fixed in e9b20e5. The helper already applies the pension_contributions_relief_age_limit guard (FA 2004 s.188(3)(a)). New mixed-age case: the 76-year-old earns £11,000 and contributes £1,000, with a 40-year-old partner, 2026. The test also checks that both are assessed claimants and that the pension-age HB route does not apply, so it passes for the right reason. It is now exempt and pays UC of £42,066.40. On 70e426c it was capped by £19,080.80.
  3. AFIP recipient scope (should-fix). Covered by María's point 1 above. The child-positive case was already replaced in 70e426c. test_uc_single_claim_properties.py now generates AFIP for both members, includes it in PARTNER_OWN, and adds a partner-only AFIP example asserting it does not lift a single claim's cap but does lift the joint claim's. This is coverage rather than a reproducer: the reg 3(3) mask already handled it.
  4. Documentation (nit). test_uc_income_from_capital_properties.py no longer says property income can switch the cap on, since it cannot now. In the changelog I kept "pension" rather than removing it, because that claim is now true. It now names the helper and describes the per-person floor and the age-75 rule.

Reproducer. The description has a Reproducer: line naming seven cases in is_benefit_cap_exempt.yaml: the four new ones above plus the original defect 1-3 cases. Main's .github/check_fix_pr.py passes locally against the PR diff and this head: "Fix PR has valid reproducer evidence. Changed executable tests: 3."

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 HOUSING_BENEFIT_YOUNG_PERSON_BENEFITS comment still gives AFIP the HB young-person scope "either scheme", which is the case María flagged. When it merges main, keep the UC route here.

Tests (fresh venv, policyengine-core 3.33.2, worktree import path confirmed):

@juaristi22 @MaxGhenis could you re-review?

@MaxGhenis MaxGhenis added the ci-recheck Re-triggers Fix evidence without changing the PR (UK hub, 2026-10-12) label Oct 12, 2026
MaxGhenis added a commit that referenced this pull request Oct 13, 2026
…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>
@MaxGhenis

Copy link
Copy Markdown
Collaborator

Merge-order note from the UK hub (2026-10-13). #1965 (UC reg 77 for company owner-managers) merged today as de511929d. Its hand-off asked that whichever of #1965 and this PR lands second checks one thing: is_benefit_cap_exempt_earnings should read uc_individual_earned_income_before_mif and add no separate company term. That variable now includes company earnings.

This PR's head e9b20e5d4 already does that, so nothing needs changing for #1965. Merge main in when you next push, so CI runs against the combined code.

One more thing to be aware of: main's release tests currently fail on test_uc_statutory_pay_properties.py::test_award_never_rises_with_a_claimants_statutory_pay. Statutory sick pay can end the cap's earnings exemption in a family with pension income. A hub job is fixing that in the same variable; it will keep its change consistent with this PR and link it here.

🤖 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>
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

@MaxGhenis thanks for the merge-order note. I've merged main into this branch (01e90fe, a merge, no rebase or force-push), so CI now runs on the combined code.

Conflicts

Two of main's tests changed because they assumed AFIP had the same scope in both schemes. The benefit_cap_specified_benefits.yaml case "A young person's armed forces independence payment lifts the cap (HB reg 75F(1)(ea))" now enters universal_credit_pre_benefit_cap: 0, so it tests the Housing Benefit route its name cites. The reference in test_benefit_cap_specified_benefits_properties.py now takes the family's scheme.

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 is_benefit_cap_exempt.yaml. Run on main's code, two of them fail (the 2026-27 threshold and the young person's AFIP on UC), so the Reproducer: line now names just those two. I've updated the description and changelog fragment to match.

Tests (fresh venv, at 01e90fe): benefit cap YAML 110 passed, gov/dwp YAML 571 passed, full YAML policy suite 3,429 passed. The 33 pytest files touching the cap or AFIP: 497 passed, 1 skipped (needs a dataset token), 1 xfailed (#1947). That includes test_uc_statutory_pay_properties.py, so the release-test failure you mentioned passes on the combined code. Ruff is clean on the changed files, and .github/check_fix_pr.py passes against the new description.

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@MaxGhenis

Copy link
Copy Markdown
Collaborator

#2089 merged on 2026-10-17 as 0583e23ec, so each scheme now has its own benefit cap exceptions on main. As promised on #2089, here is what that means for this PR:

vahid-ahmadi and others added 2 commits October 18, 2026 20:17
#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>
@vahid-ahmadi

Copy link
Copy Markdown
Collaborator Author

@MaxGhenis thanks for the #2089 note. I've merged main into this branch (44d696c, then ce3e0bc for the later commits; merges, no rebase or force-push), so it is mergeable again.

The three conflicts

  • is_benefit_cap_exempt_health_disability.py: dropped. Give Universal Credit and Housing Benefit their own benefit cap and LHA shared-accommodation rules #2089's is_uc_benefit_cap_exempt_specified_benefit already counts AFIP for a claimant only (UC Regs 2013 regs 2 and 83(1)(c)), and is_housing_benefit_benefit_cap_exempt_specified_benefit counts it for the claimant, partner or a young person (HB Regs 2006 reg 75F(1)(ea)). That is the fix this PR made, so there was nothing left to carry over.
  • test_benefit_cap_specified_benefits_properties.py: main's version, which already has the per-scheme AFIP reference.
  • test_uc_single_claim_properties.py: main's version, plus this PR's partner-only AFIP example, now asserted against the UC and HB specified-benefit variables.

What follows from that

  • The earnings threshold parameter is in universal_credit/benefit_cap/is_uc_benefit_cap_exempt_earnings.py on Fix benefit-cap statutory-pay earnings exemption #2399's measure, as you expected, so the £10,152 literal goes when this lands.
  • test_benefit_cap_lha_scheme_split_properties.py re-adds the old shared earnings formula with the 10_152 literal 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.
  • The earnings YAML cases moved to is_uc_benefit_cap_exempt_earnings.yaml under the new names. The paired final-award AFIP cases (UC £22,054.80, legacy HB £30,000) are in benefit_cap_specified_benefits.yaml beside main's flag-only case.
  • The changelog no longer claims the AFIP fix, and the PR body's reproducer is now only the 2026-27 threshold case. It fails against main's earnings formula.

Benefit cap YAML 105 passed, gov/dwp YAML 606 passed, full YAML policy suite 3,587 passed, and the 28 benefit cap and AFIP pytest files 351 passed with 1 xfailed (#1947). check_fix_pr.py passes on the new body and head.

🤖 Generated with Claude Code

@juaristi22 juaristi22 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

This branch has not been deployed

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

Labels

ci-recheck Re-triggers Fix evidence without changing the PR (UK hub, 2026-10-12)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants