Repository navigation
Benefit cap counts another member's own income-based JSA and income-related ESA #2298
Description
Defect
benefit_cap_reduction counts a benefit-unit member's own income-based JSA and income-related ESA, not only the claimant's and partner's. Its total is
claimants = benunit.members("is_uc_assessed_claimant", period)
total = add_for_members(benunit, period, CAPPED_BENEFITS, claimants)with jsa_income and esa_income in CAPPED_BENEFITS. Both are benefit-unit variables, built from every member's report (jsa_income sums jsa_income_reported over the unit). add_for_members adds a benefit-unit variable as it is, whatever members says; its docstring says so and says to pass claimant_or_partner_jsa_income and claimant_or_partner_esa_income instead. So a member who is not the claimant, the partner or a dependant, and who claims JSA or ESA in their own right, raises the family's cap reduction. The variable's own comment says the cap applies to "the welfare benefits to which a single person or couple is entitled".
The law
Welfare Reform Act 2012 s.96(1): "Regulations may provide for a benefit cap to be applied to the welfare benefits to which a single person or couple is entitled." s.96(2): "where a single person's or couple's total entitlement to welfare benefits in respect of the reference period exceeds the relevant amount, their entitlement to welfare benefits [...] is reduced by an amount up to or equalling the excess."
UC Regs 2013 reg. 80(1): "the amount of a welfare benefit to be used when determining total entitlement to welfare benefits is the amount to which the single person or couple is entitled during the reference period [...]".
Another adult's own JSA or ESA is not an amount to which the single person or couple is entitled.
Reproduction
On main (9475c95), 2025-26, England, no savings or earnings: a couple aged 35, each reporting £3,000 of income-based JSA, two children aged 15, and a lodger aged 30 in the same benefit unit. A member aged 20 who is not the claimant, the partner or a dependant reports £0, then £3,000, of income-based JSA in their own right.
| Variable | Added member reports £0 | Reports £3,000 |
|---|---|---|
claimant_or_partner_jsa_income |
£6,000.00 | £6,000.00 |
jsa_income (whole unit) |
£6,000.00 | £9,000.00 |
benefit_cap |
£25,323.00 | £25,323.00 |
benefit_cap_reduction |
£0.00 | £1,047.52 |
universal_credit |
£15,118.92 | £14,071.40 |
council_tax_reduction_applicable_income |
£23,370.52 | £22,323.00 |
The couple's own JSA does not change; the cap reduction rises by the part of the member's award that takes the unit over the cap, and Universal Credit falls by the same amount.
Script
"""The benefit cap counts another member's own income-based JSA (review r1 of #2294)."""
import subprocess
from policyengine_uk import Simulation
Y = 2025
print("# checkout", subprocess.run(["git", "rev-parse", "--short", "HEAD"], capture_output=True, text=True).stdout.strip())
def situation(outsider_jsa):
people = {
"claimant": {"age": 35, "is_claimant_or_partner": True, "jsa_income_reported": 3_000,
"is_parent": True, "receives_carer_benefit": False, "care_hours": 0},
"partner": {"age": 35, "is_claimant_or_partner": True, "jsa_income_reported": 3_000,
"is_parent": True, "receives_carer_benefit": False, "care_hours": 0},
"child1": {"age": 15, "is_claimant_or_partner": False},
"child2": {"age": 15, "is_claimant_or_partner": False},
"lodger": {"age": 30, "is_claimant_or_partner": False},
"outsider": {"age": 20, "is_claimant_or_partner": False, "jsa_income_reported": outsider_jsa},
}
for p in people.values():
p.setdefault("current_education", "NOT_IN_EDUCATION")
for k in ("esa_income_reported", "income_support_reported", "jsa_income_reported", "employment_income"):
p.setdefault(k, 0)
names = list(people)
return {
"people": {n: {k: {Y: v} for k, v in p.items()} for n, p in people.items()},
"benunits": {"b": {"members": names, "claims_all_entitled_benefits": {Y: False}}},
"households": {"h": {"members": names, "country": {Y: "ENGLAND"}, "savings": {Y: 0}}},
}
rows = {}
for jsa in (0, 3_000):
sim = Simulation(situation=situation(jsa))
rows[jsa] = {v: float(sim.calculate(v, Y)[0]) for v in (
"claimant_or_partner_jsa_income", "jsa_income", "benefit_cap", "benefit_cap_reduction",
"universal_credit", "council_tax_reduction_applicable_income")}
for v in rows[0]:
print(f"{v}: {rows[0][v]:,.2f} -> {rows[3_000][v]:,.2f}")The review of #2294 found this; test_legacy_award_readers_properties.py::test_other_members_awards_never_change_the_readers holds benefit_cap_reduction at zero until it is fixed (#2294 adds the family above as an @example).
Fix
Pass claimant_or_partner_jsa_income and claimant_or_partner_esa_income in CAPPED_BENEFITS in place of jsa_income and esa_income. Then drop the hold in the property test, so the @example checks the fix. Check income_support too: the CTR income comment says it already needs the claimant's or partner's own award.
Invariant to test: the cap reduction never depends on what a member who is not the claimant or partner reports.
axiom: WRA 2012 s.96 and UC Regs 2013 reg. 80: to check in rulespec-uk when the fix is built.
Activity
Hold the benefit cap and bound the Income Support fall in the readers…
MaxGhenis commentedon Oct 11, 2026
UK hub: already addressed in an open PR. #2076 ("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") changes the cap's total to count
claimant_or_partner_esa_incomeandclaimant_or_partner_jsa_income, not the unit-wideesa_incomeandjsa_income. Its tests include a non-dependant withesa_income_reported: 5_000andclaimant_or_partner_esa_income: 0, and the same for JSA.When #2076 merges, this issue can close, and #2294's
CAP_HELDhold can be dropped so its@examplechecks the fix.#2089 also rewrites
CAPPED_BENEFITS, so whichever of #2076 and #2089 lands second must keep the claimant-or-partner variables.Hold each scheme's benefit cap reduction in the legacy readers property
Pin LCWRA entered as false, and calculate the cap in the readers inva…
Restore main's claimants() and update docs after #2119 (review of d76…
Merge origin/main a98f8f0 (#2076 and others) into uc-hb-split-shared-…