From efaec34232cd7353bf9310cd2f036e71b56b2795 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 02:25:14 -0400 Subject: [PATCH 1/3] End income-related ESA when the family claims Universal Credit A claim for Universal Credit brings the abolition of income-related ESA (Welfare Reform Act 2012 s.33(1)(b)) into force for the claimant's old style ESA award, whether or not UC is awarded (SI 2013/983 art. 4(1), (2)(a), (3)(a) and (9); NI: SR 2017/190 art. 6). The model paid it alongside UC. esa_income_report_closed (Person) ends the award on a report when the owner's legacy benefits close, or when they are the claimant or partner of a benefit unit that claims UC. esa_income, esa_income_eligible and award_of_members read it, so every reader of the claimant's or partner's award agrees. A non-dependant's own award is not part of the claim. Existing cases for legacy ESA claimants now say the family does not claim UC (would_claim_uc: false), which keeps what each one tests. Fixes #1914 (the UC overlap; the 1 July 2026 abolition follows #1910). Co-Authored-By: Claude Opus 5.5 --- changelog.d/1914.fixed.md | 1 + .../family/esa_income/esa_income_capital.yaml | 10 + .../esa_income_remunerative_work.yaml | 45 ++++ .../esa_income_universal_credit_claim.yaml | 166 ++++++++++++++ .../family/esa_income/legacy_capital.yaml | 1 + ...income_support_claimant_partner_gates.yaml | 3 + .../claimant_or_partner_awards.yaml | 7 + .../council_tax_reduction_passports.yaml | 4 + ...ome_support_take_up_and_person_awards.yaml | 2 + .../means_test_income.yaml | 1 + ...ixed_age_couple_pension_credit_saving.yaml | 1 + .../legacy_award_readers/passports.yaml | 7 + .../bursary_fund_16_to_19.yaml | 1 + .../gov/dwp/lifetime_isa_capital.yaml | 1 + .../person_legacy_benefits_closed.yaml | 3 +- .../relevant_benefit_receipt.yaml | 1 + ...ncome_universal_credit_claim_properties.py | 203 ++++++++++++++++++ policyengine_uk/utils/benefit_unit.py | 11 +- .../variables/gov/dwp/esa_income.py | 7 +- .../variables/gov/dwp/esa_income_eligible.py | 9 +- .../gov/dwp/esa_income_report_closed.py | 48 +++++ .../claims_universal_credit.py | 14 +- 22 files changed, 532 insertions(+), 14 deletions(-) create mode 100644 changelog.d/1914.fixed.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_universal_credit_claim.yaml create mode 100644 policyengine_uk/tests/test_esa_income_universal_credit_claim_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/esa_income_report_closed.py diff --git a/changelog.d/1914.fixed.md b/changelog.d/1914.fixed.md new file mode 100644 index 0000000000..3f184fc988 --- /dev/null +++ b/changelog.d/1914.fixed.md @@ -0,0 +1 @@ +Income-related ESA ends for the claimant and partner of a family that claims Universal Credit, as the claim brings its abolition into force (Welfare Reform Act 2012 s.33(1)(b); SI 2013/983 art. 4), so it is no longer paid alongside Universal Credit (#1914). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml index aac4fec6c6..87009a36b1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_capital.yaml @@ -9,6 +9,7 @@ benunits: benunit: members: claimant + would_claim_uc: false households: household: members: claimant @@ -32,6 +33,7 @@ benunits: benunit: members: claimant + would_claim_uc: false households: household: members: claimant @@ -51,6 +53,7 @@ benunits: benunit: members: claimant + would_claim_uc: false households: household: members: claimant @@ -71,6 +74,7 @@ benunits: benunit: members: claimant + would_claim_uc: false households: household: members: claimant @@ -91,6 +95,7 @@ benunits: benunit: members: claimant + would_claim_uc: false households: household: members: claimant @@ -135,6 +140,7 @@ benunits: claimant_benunit: members: claimant + would_claim_uc: false unrelated_benunit: members: unrelated_adult households: @@ -158,8 +164,10 @@ benunits: benunit_1: members: claimant_1 + would_claim_uc: false benunit_2: members: claimant_2 + would_claim_uc: false households: household: members: [claimant_1, claimant_2] @@ -182,6 +190,7 @@ benunit: members: [claimant, child] would_claim_IS: true + would_claim_uc: false households: household: members: [claimant, child] @@ -202,6 +211,7 @@ benunits: benunit: members: claimant + would_claim_uc: false households: household: members: claimant diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index c57c401bbf..eacc7e084d 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -40,6 +40,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -63,6 +64,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -88,6 +90,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -112,6 +115,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -135,6 +139,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -159,6 +164,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -181,6 +187,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -205,6 +212,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -226,6 +234,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -248,6 +257,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -275,6 +285,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -299,6 +310,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -320,6 +332,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -348,6 +361,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -383,6 +397,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -410,6 +425,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -433,6 +449,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -458,6 +475,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -483,6 +501,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -514,6 +533,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -540,6 +560,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -571,6 +592,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -596,6 +618,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -620,6 +643,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -646,6 +670,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -678,6 +703,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -703,6 +729,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -734,6 +761,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -760,6 +788,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -791,6 +820,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -818,6 +848,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -843,6 +874,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -868,6 +900,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -899,6 +932,7 @@ benunits: couple: members: [claimant, partner] + would_claim_uc: false households: household: members: [claimant, partner] @@ -924,6 +958,7 @@ benunits: couple: members: [claimant, partner] + would_claim_uc: false households: household: members: [claimant, partner] @@ -949,6 +984,7 @@ benunits: couple: members: [claimant, partner] + would_claim_uc: false households: household: members: [claimant, partner] @@ -975,6 +1011,7 @@ benunits: couple: members: [claimant, partner] + would_claim_uc: false households: household: members: [claimant, partner] @@ -1000,6 +1037,7 @@ benunits: couple: members: [claimant, partner] + would_claim_uc: false households: household: members: [claimant, partner] @@ -1027,6 +1065,7 @@ benunits: couple: members: [first, second] + would_claim_uc: false households: household: members: [first, second] @@ -1053,6 +1092,7 @@ benunits: couple: members: [first, second] + would_claim_uc: false households: household: members: [first, second] @@ -1107,6 +1147,7 @@ benunits: benunit: members: [claimant, lodger] + would_claim_uc: false households: household: members: [claimant, lodger] @@ -1134,6 +1175,7 @@ benunits: benunit: members: [claimant, lodger] + would_claim_uc: false households: household: members: [claimant, lodger] @@ -1167,6 +1209,7 @@ benunits: benunit: members: [claimant, lodger] + would_claim_uc: false households: household: members: [claimant, lodger] @@ -1237,6 +1280,7 @@ couple: members: [carer, partner] income_support_assessable_capital: 0 + would_claim_uc: false households: household: members: [carer, partner] @@ -1268,6 +1312,7 @@ unit: members: [claimant, partner, outside] esa_income_assessable_capital: 0 + would_claim_uc: false households: household: members: [claimant, partner, outside] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_universal_credit_claim.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_universal_credit_claim.yaml new file mode 100644 index 0000000000..efddac0f27 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_universal_credit_claim.yaml @@ -0,0 +1,166 @@ +# Income-related ESA ends when the family claims Universal Credit. +# A claim for Universal Credit brings the abolition of income-related ESA +# (Welfare Reform Act 2012 s.33(1)(b)) into force for the claimant's old style +# ESA award from the first day of the period claimed for, whether or not +# Universal Credit is awarded (SI 2013/983 art. 4(1), (2)(a), (3)(a) and (9); +# Northern Ireland: Welfare Reform (NI) Order 2015 art. 39(1)(b) and SR +# 2017/190 art. 6). The two-week run-on (SI 2019/1152 reg. 5) is not modelled +# in an annual model. A member who is neither the claimant nor the partner +# claims in their own right and is not part of the family's claim. +# No case has capital, so no tariff income: the award is the reported amount. + +- name: Case 1, a single claimant who claims Universal Credit loses income-related ESA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: true + households: + household: + members: [person1] + output: + claims_universal_credit: true + esa_income_report_closed: true + esa_income_eligible: false + # The UC claim ends the award: 0, not the 5_000 reported. + esa_income: 0 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: false + +- name: Case 2, the same claimant without a Universal Credit claim keeps income-related ESA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + claims_universal_credit: false + esa_income_report_closed: false + esa_income_eligible: true + # No claim, no tariff income: the reported 5_000. + esa_income: 5_000 + claimant_or_partner_esa_income: 5_000 + is_on_income_related_esa: true + +- name: Case 3, a Universal Credit claim ends income-related ESA even when no Universal Credit is awarded. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + private_pension_income: 40_000 + benunits: + benunit: + members: [person1] + would_claim_uc: true + households: + household: + members: [person1] + output: + claims_universal_credit: true + # £40,000 of pension income is UC unearned income above the £4,801.68 + # 2025-26 standard allowance (12 x £400.14), so nothing is awarded. + universal_credit: 0 + esa_income_report_closed: true + # The claim still ends the old style award (art. 4(9) of SI 2013/983). + esa_income: 0 + claimant_or_partner_esa_income: 0 + +- name: Case 4, the couple's Universal Credit claim leaves a non-dependent adult's own income-related ESA. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + person2: + age: 38 + is_claimant_or_partner: true + person3: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 2_000 + benunits: + benunit: + members: [person1, person2, person3] + would_claim_uc: true + households: + household: + members: [person1, person2, person3] + output: + claims_universal_credit: true + esa_income_report_closed: [true, true, false] + # Only person3's own claim is left, and person3 does not work. + esa_income_eligible: true + # 2_000, person3's own award, not 5_000 + 2_000. + esa_income: 2_000 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: [false, false, true] + +- name: Case 5, the gate applies in 2027, after income-related ESA's 1 July 2026 abolition date. + period: 2027 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: true + households: + household: + members: [person1] + output: + claims_universal_credit: true + esa_income_report_closed: true + esa_income: 0 + claimant_or_partner_esa_income: 0 + +- name: Case 6, a Universal Credit claim ends income-related ESA in Northern Ireland. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: true + households: + household: + members: [person1] + region: NORTHERN_IRELAND + output: + claims_universal_credit: true + esa_income_report_closed: true + esa_income: 0 + claimant_or_partner_esa_income: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml index beed6513c8..1c37c4c5c4 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/legacy_capital.yaml @@ -64,6 +64,7 @@ benunits: claimant_benunit: members: [claimant] + would_claim_uc: false unrelated_benunit: members: [unrelated_claimant] households: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_claimant_partner_gates.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_claimant_partner_gates.yaml index 1dc8fa0c6b..5e3e9191f1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_claimant_partner_gates.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_claimant_partner_gates.yaml @@ -519,6 +519,7 @@ members: [carer, partner] income_support_assessable_capital: 0 esa_income_assessable_capital: 0 + would_claim_uc: false output: esa_income: 3_000 income_support_eligible: false @@ -542,6 +543,7 @@ income_support_assessable_capital: 0 # Above the £16,000 ESA capital limit (ESA Regs 2008 reg 110). esa_income_assessable_capital: 20_000 + would_claim_uc: false output: esa_income: 0 income_support_eligible: true @@ -582,6 +584,7 @@ members: [carer] income_support_assessable_capital: 0 esa_income: 0 + would_claim_uc: false output: # The model pays no income-related ESA, so none bars Income Support. income_support_eligible: true diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml index 06af3598f5..82cb89df67 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml @@ -19,6 +19,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -41,6 +42,7 @@ benunits: benunit: members: [claimant, partner] + would_claim_uc: false households: household: members: [claimant, partner] @@ -89,6 +91,7 @@ benunits: benunit: members: [claimant, other] + would_claim_uc: false households: household: members: [claimant, other] @@ -111,6 +114,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -155,6 +159,7 @@ benunit: members: [claimant] esa_income: 0 + would_claim_uc: false households: household: members: [claimant] @@ -362,6 +367,7 @@ benunit: members: [claimant] esa_income: 200 + would_claim_uc: false households: household: members: [claimant] @@ -385,6 +391,7 @@ benunit: members: [claimant] esa_income: 0 + would_claim_uc: false households: household: members: [claimant] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml index 88a4f16cfa..e36da6ed71 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml @@ -36,6 +36,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -187,6 +188,7 @@ nondep_unit: members: [nondep, other] universal_credit: 0 + would_claim_uc: false households: household: members: [applicant, nondep, other] @@ -217,6 +219,7 @@ nondep_unit: members: [payee, partner] universal_credit: 0 + would_claim_uc: false households: household: members: [applicant, payee, partner] @@ -254,6 +257,7 @@ nondep_unit: members: [payee, partner] universal_credit: 0 + would_claim_uc: false households: household: members: [applicant, payee, partner] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml index 561dd35b63..abf01a92aa 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml @@ -105,6 +105,7 @@ benunits: benunit: members: [claimant, partner, other] + would_claim_uc: false households: household: members: [claimant, partner, other] @@ -146,6 +147,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml index 8e4bb2be1a..3b5ed5f49c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml @@ -118,6 +118,7 @@ benunit: members: [claimant, other] universal_credit: 0 + would_claim_uc: false households: household: members: [claimant, other] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/mixed_age_couple_pension_credit_saving.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/mixed_age_couple_pension_credit_saving.yaml index 93921ebd58..5e3e43c563 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/mixed_age_couple_pension_credit_saving.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/mixed_age_couple_pension_credit_saving.yaml @@ -78,6 +78,7 @@ benunit: members: [older, younger] is_mixed_age_couple: true + would_claim_uc: false households: household: members: [older, younger] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml index cb78e220ce..63c07bf301 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml @@ -45,6 +45,7 @@ benunits: benunit: members: [claimant] + would_claim_uc: false households: household: members: [claimant] @@ -153,6 +154,7 @@ child_tax_credit: 0 working_tax_credit: 0 pension_credit: 0 + would_claim_uc: false households: household: members: [parent, child, other] @@ -272,6 +274,7 @@ meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false working_tax_credit_run_on: false extended_childcare_entitlement_eligible: false + would_claim_uc: false households: household: members: [parent, child, other] @@ -382,6 +385,7 @@ benunit: members: [claimant, other] working_tax_credit: 0 + would_claim_uc: false households: household: members: [claimant, other] @@ -413,6 +417,7 @@ benunits: benunit: members: [parent, student] + would_claim_uc: false households: household: members: [parent, student] @@ -459,6 +464,7 @@ benunits: benunit: members: [student, partner] + would_claim_uc: false households: household: members: [student, partner] @@ -483,6 +489,7 @@ benunits: benunit: members: [student, partner] + would_claim_uc: false households: household: members: [student, partner] diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml index 33600f00ac..0cf3e91122 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/bursary_fund_16_to_19/bursary_fund_16_to_19.yaml @@ -239,6 +239,7 @@ benunits: benunit: members: ["student"] + would_claim_uc: false households: household: members: ["student"] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml index fed93729ca..5f5f0b671d 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/lifetime_isa_capital.yaml @@ -308,6 +308,7 @@ benunits: parent: members: parent + would_claim_uc: false adult_child: members: adult_child households: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/person_legacy_benefits_closed.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/person_legacy_benefits_closed.yaml index 2f4f43bbfb..352e12a1be 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/person_legacy_benefits_closed.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/universal_credit/person_legacy_benefits_closed.yaml @@ -25,7 +25,8 @@ "claimant", "partner", "outside" - ] + ], + "would_claim_uc": false } }, "households": { diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_payment/relevant_benefit_receipt.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_payment/relevant_benefit_receipt.yaml index 220b4bbcd5..bfd3411b3e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_payment/relevant_benefit_receipt.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/winter_fuel_payment/relevant_benefit_receipt.yaml @@ -1184,6 +1184,7 @@ jsa_income: 0 income_support: 0 universal_credit: 0 + would_claim_uc: false households: household: members: [pension_credit_payee, partner] diff --git a/policyengine_uk/tests/test_esa_income_universal_credit_claim_properties.py b/policyengine_uk/tests/test_esa_income_universal_credit_claim_properties.py new file mode 100644 index 0000000000..6fc6e412da --- /dev/null +++ b/policyengine_uk/tests/test_esa_income_universal_credit_claim_properties.py @@ -0,0 +1,203 @@ +"""Property-based tests for the end of income-related ESA on a Universal +Credit claim (#1914). + +A claim for Universal Credit brings the abolition of income-related ESA +(Welfare Reform Act 2012 s.33(1)(b)) into force for the claimant's old style +ESA award, whether or not Universal Credit is awarded (SI 2013/983 art. 4(1), +(2)(a), (3)(a) and (9); NI: SR 2017/190 art. 6). The claim is the claimant's +and partner's. A member who is neither, such as a non-dependent adult, claims +income-related ESA in their own right and is not part of it. + +Invariants, for any generated population of working-age families in 2025 +and 2027 (either side of income-related ESA's 1 July 2026 abolition date, +which the model does not yet apply) with no other legacy benefit, so none is +moved off legacy benefits: + +1. Exclusive: a family that claims Universal Credit pays its claimant and + partner no income-related ESA (claimant_or_partner_esa_income), and + neither of them is on it (is_on_income_related_esa). So Universal Credit + and the claimant's or partner's income-related ESA are never both paid. + Without a claim the gate is inert: esa_income_report_closed is + person_legacy_benefits_closed. +2. Differential: claiming Universal Credit gives the same income-related ESA + outcomes (esa_income, claimant_or_partner_esa_income, esa_income_eligible + and each member's is_on_income_related_esa) as not claiming with the + claimant's and partner's reports removed. Another member's own award is + what it would be if the couple had never reported one. + +Intended: claiming Universal Credit can raise esa_income. While the claimant +or partner reports an award, the benefit unit's screen tests only them, and +another member's report is paid or not with their claim +(esa_income_eligible). Once the claim ends the couple's award, that member is +screened on their own claim alone, as invariant 2 says. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEARS = (2025, 2027) +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +WORKING_AGE = st.integers(25, 60) +# "other" is an adult who is neither the claimant, the partner nor a young +# person they are responsible for, so they claim in their own right. +SHAPES = { + "single": ["claimant"], + "couple": ["claimant", "partner"], + "lone_parent": ["claimant", "child"], + "single_with_other": ["claimant", "other"], + "couple_with_other": ["claimant", "partner", "other"], +} +REPORT = st.sampled_from([0.0, 2_000.0, 6_000.0]) +BENUNIT_OUTPUTS = [ + "esa_income", + "claimant_or_partner_esa_income", + "esa_income_eligible", + "claims_universal_credit", + "universal_credit", +] +PERSON_OUTPUTS = [ + "is_on_income_related_esa", + "is_claimant_or_partner", + "esa_income_report_closed", + "person_legacy_benefits_closed", +] + + +@st.composite +def families(draw): + roles = SHAPES[draw(st.sampled_from(sorted(SHAPES)))] + members = [] + for role in roles: + members.append( + dict( + role=role, + age=draw(st.integers(0, 15) if role == "child" else WORKING_AGE), + esa_income_reported=0.0 if role == "child" else draw(REPORT), + employment_income=( + 0.0 + if role == "child" + else draw(st.sampled_from([0.0, 0.0, 4_000.0, 20_000.0])) + ), + weekly_hours=0.0 + if role == "child" + else draw(st.sampled_from([0, 10, 30])), + ) + ) + return dict( + members=members, + # Around the £6,000 tariff threshold and £16,000 limit. + savings=draw(st.sampled_from([0.0, 7_000.0, 20_000.0])), + would_claim_uc=draw(st.booleans()), + ) + + +def every_year(value): + return {year: value for year in YEARS} + + +def situation(units, would_claim_uc=None, drop_couple_reports=False): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for j, member in enumerate(unit["members"]): + name = f"p{i}_{j}" + couple = member["role"] in ("claimant", "partner") + report = member["esa_income_reported"] + if drop_couple_reports and couple: + report = 0.0 + # Set every input for every member, or it stops being an input + # for the rest of the simulation. + people[name] = { + "age": every_year(member["age"]), + "is_claimant_or_partner": every_year(couple), + "current_education": every_year("NOT_IN_EDUCATION"), + "esa_income_reported": every_year(report), + "employment_income": every_year(member["employment_income"]), + "weekly_hours": every_year(member["weekly_hours"]), + } + names.append(name) + claims = unit["would_claim_uc"] if would_claim_uc is None else would_claim_uc + benunits[f"b{i}"] = { + "members": names, + "would_claim_uc": every_year(claims), + "claims_all_entitled_benefits": every_year(False), + } + households[f"h{i}"] = { + "members": names, + "savings": every_year(unit["savings"]), + } + return {"people": people, "benunits": benunits, "households": households} + + +def calculate(units, **kwargs): + sim = Simulation(situation=situation(units, **kwargs)) + return { + year: { + v: np.asarray(sim.calculate(v, year)) + for v in BENUNIT_OUTPUTS + PERSON_OUTPUTS + } + for year in YEARS + } + + +def members_of(units): + """Each person's benefit unit index, in simulation order.""" + return np.array([i for i, unit in enumerate(units) for _ in unit["members"]]) + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=12)) +def test_a_universal_credit_claim_ends_the_couples_income_related_esa(units): + values = calculate(units) + benunit = members_of(units) + for year in YEARS: + v = values[year] + claims = v["claims_universal_credit"].astype(bool) + # Nothing here moves a family off legacy benefits, so the claim is + # would_claim_uc. + assert list(claims) == [unit["would_claim_uc"] for unit in units] + assert np.all(v["claimant_or_partner_esa_income"][claims] == 0), year + assert not np.any( + v["is_on_income_related_esa"].astype(bool) + & v["is_claimant_or_partner"].astype(bool) + & claims[benunit] + ), year + paid_uc = v["universal_credit"] > 0 + assert not np.any(paid_uc & (v["claimant_or_partner_esa_income"] > 0)), year + not_claiming = ~claims[benunit] + assert np.array_equal( + v["esa_income_report_closed"][not_claiming], + v["person_legacy_benefits_closed"][not_claiming], + ), year + + +@PROPERTY_SETTINGS +@given(st.lists(families(), min_size=1, max_size=12)) +def test_a_claim_is_the_couple_reporting_no_income_related_esa(units): + claiming = calculate(units, would_claim_uc=True) + without_couple_reports = calculate( + units, would_claim_uc=False, drop_couple_reports=True + ) + for year in YEARS: + for name in [ + "esa_income", + "claimant_or_partner_esa_income", + "esa_income_eligible", + "is_on_income_related_esa", + ]: + assert np.allclose( + claiming[year][name].astype(float), + without_couple_reports[year][name].astype(float), + atol=0.01, + ), (year, name) diff --git a/policyengine_uk/utils/benefit_unit.py b/policyengine_uk/utils/benefit_unit.py index 2df7693ab5..330a9ade55 100644 --- a/policyengine_uk/utils/benefit_unit.py +++ b/policyengine_uk/utils/benefit_unit.py @@ -6,6 +6,15 @@ # each other are the same, and an award within half a penny of zero is none. HALF_PENNY = 0.005 +# For each award award_of_members reads, the person-level variable saying the +# award on a member's report has ended. Its formula leaves those reports out: +# income-related ESA also ends for the claimant and partner of a family that +# claims Universal Credit (esa_income_report_closed). +REPORT_CLOSED = { + "esa_income": "esa_income_report_closed", + "jsa_income": "person_legacy_benefits_closed", +} + def add_for_members(benunit, period, variables, members): """Sum variables over the benefit-unit members a means test counts. @@ -82,7 +91,7 @@ def award_of_members(benunit, period, award, report, award_on_reports, members): members_reported = benunit.sum(reported * members) # Formula awards omit reports whose own award has closed. Keep the raw # totals above for disable_simulated_benefits and direct-value provenance. - open_reported = reported * ~person("person_legacy_benefits_closed", period) + open_reported = reported * ~person(REPORT_CLOSED[award], period) award_on_all_reports = award_on_reports(benunit, period, benunit.sum(open_reported)) award_on_members_reports = award_on_reports( benunit, period, benunit.sum(open_reported * members) diff --git a/policyengine_uk/variables/gov/dwp/esa_income.py b/policyengine_uk/variables/gov/dwp/esa_income.py index 7b723bf4c6..e382b53e47 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income.py +++ b/policyengine_uk/variables/gov/dwp/esa_income.py @@ -16,7 +16,10 @@ class esa_income(Variable): label = "ESA (income-based)" documentation = ( "Reported income-related ESA screened through bounded capital and " - "remunerative work tests (esa_income_eligible). " + "remunerative work tests (esa_income_eligible). Reports whose award " + "has ended are left out (esa_income_report_closed): the owner's " + "legacy benefits have closed, or the owner is the claimant or " + "partner of a family that claims Universal Credit. " "This is not a full entitlement model." ) definition_period = YEAR @@ -26,6 +29,6 @@ def formula(benunit, period, parameters): person = benunit.members reported_award = benunit.sum( person("esa_income_reported", period) - * ~person("person_legacy_benefits_closed", period) + * ~person("esa_income_report_closed", period) ) return income_related_esa_award(benunit, period, reported_award) diff --git a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py index c071f77373..690416d60e 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py @@ -26,9 +26,10 @@ class esa_income_eligible(Variable): "in it, so when the claimant or partner reports, another member's " "report is paid or not with their claim. Whether that member is on " "the award themselves (is_on_income_related_esa) is tested on their " - "own claim. Reports whose owner has closed legacy benefits are left " - "out before choosing candidates (person_legacy_benefits_closed). " - "This is not a full " + "own claim. Reports whose award has ended are left out before " + "choosing candidates (esa_income_report_closed): the owner's legacy " + "benefits have closed, or the owner is the claimant or partner of a " + "family that claims Universal Credit. This is not a full " "entitlement model." ) definition_period = YEAR @@ -45,7 +46,7 @@ def formula(benunit, period, parameters): person = benunit.members capital = benunit("esa_income_assessable_capital", period) reports_award = (person("esa_income_reported", period) > 0) & ~person( - "person_legacy_benefits_closed", period + "esa_income_report_closed", period ) claimant_or_partner = person("is_claimant_or_partner", period) # The candidate claimants: the claimant or partner who reports the diff --git a/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py b/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py new file mode 100644 index 0000000000..dd087317c8 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_income_report_closed.py @@ -0,0 +1,48 @@ +from policyengine_uk.model_api import * + + +class esa_income_report_closed(Variable): + value_type = bool + entity = Person + label = "Income-related ESA award on this person's report has ended" + documentation = ( + "Whether the income-related ESA award on this person's report has " + "ended, so the model leaves the report out of esa_income, " + "esa_income_eligible and the claimant's or partner's award " + "(claimant_or_partner_esa_income). It ends when the person's legacy " + "benefits close (person_legacy_benefits_closed), or when the person " + "is the claimant or partner of a family that claims Universal Credit " + "(claims_universal_credit). A claim for Universal Credit brings the " + "abolition of income-related ESA (Welfare Reform Act 2012 s.33(1)(b)) " + "into force for the claimant's old style ESA award from the first day " + "of the period claimed for, whether or not Universal Credit is " + "awarded (SI 2013/983 art. 4(1), (2)(a), (3)(a) and (9), and the later " + "commencement orders that extended it to further postcode districts; " + "in Northern Ireland, Welfare Reform (Northern Ireland) Order 2015 " + "art. 39(1)(b) and SR 2017/190 art. 6(1) to (3) and (6)). For claims " + "from 22 July 2020 the award runs on for two weeks after that day (SI " + "2019/1152 reg. 5), which this annual model does not represent. A " + "contributory allowance in the award continues as new style ESA " + "(esa_contrib). A member who is neither the claimant nor the partner, " + "such as a non-dependent adult, claims in their own right and is not " + "part of the family's Universal Credit claim, so the claim does not " + "end their award. The model applies this in every year and area: the " + "postcode districts named in the commencement orders are not " + "modelled." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2012/5/section/33", + "https://www.legislation.gov.uk/uksi/2013/983/article/4", + "https://www.legislation.gov.uk/uksi/2019/1152/regulation/5", + "https://www.legislation.gov.uk/uksi/2025/1148/article/3A", + "https://www.legislation.gov.uk/nisi/2015/2006/article/39", + "https://www.legislation.gov.uk/nisr/2017/190/article/6", + "https://www.legislation.gov.uk/nisr/2025/176/article/3A", + ) + + def formula(person, period, parameters): + claims_uc = person.benunit("claims_universal_credit", period) + return person("person_legacy_benefits_closed", period) | ( + person("is_claimant_or_partner", period) & claims_uc + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py b/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py index 21a1bdc84f..952acc87e0 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/claims_universal_credit.py @@ -10,17 +10,19 @@ class claims_universal_credit(Variable): "anyway (would_claim_uc), or it claims when DWP moves it off its " "legacy benefits (claims_uc_at_legacy_closure). Universal Credit is " "paid only to claiming families. A claim ends the family's Housing " - "Benefit continuing award and Child Tax Credit. Once DWP moves a " - "family off legacy benefits (legacy_benefits_closed) all its legacy " - "awards end; for other claimants the overlap of Universal Credit " - "with Working Tax Credit, Income Support, income-related ESA and " - "income-based JSA is not modelled, nor is Housing Benefit for " - "specified or temporary accommodation." + "Benefit continuing award and Child Tax Credit, and the claimant's " + "and partner's income-related ESA (esa_income_report_closed). Once " + "DWP moves a family off legacy benefits (legacy_benefits_closed) all " + "its legacy awards end; for other claimants the overlap of Universal " + "Credit with Working Tax Credit, Income Support and income-based JSA " + "is not modelled, nor is Housing Benefit for specified or temporary " + "accommodation." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/uksi/2014/1230/regulation/8", "https://www.legislation.gov.uk/uksi/2014/1230/regulation/46", + "https://www.legislation.gov.uk/uksi/2013/983/article/4", ) def formula(benunit, period, parameters): From e9aec0d48afaff35cf546f246a619e2a23eae168 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 03:21:06 -0400 Subject: [PATCH 2/3] Make the legacy-award property tests' families non-UC claimants These tests model families on legacy income-related awards. would_claim_uc defaults to true, and a UC claim now ends the couple's income-related ESA (esa_income_report_closed), so their builders say the family does not claim UC. Where a test is about UC too, the claim follows the scheme: the benefit cap's UC families claim and its HB families don't, and the winter heating families claim exactly when their award is UC. The benefit cap's reference counts the couple's income-related ESA only without a claim. Co-Authored-By: Claude Opus 5.5 --- ...nefit_cap_specified_benefits_properties.py | 19 +++++++++++++++---- ...mant_or_partner_awards_entered_directly.py | 12 ++++++++++-- ..._disable_simulated_benefits_benefit_cap.py | 6 +++++- ...t_income_support_eligibility_properties.py | 4 +++- ...t_gate_reads_claimant_or_partner_awards.py | 3 +++ .../test_legacy_award_readers_properties.py | 15 +++++++++++---- .../test_legacy_award_work_properties.py | 3 +++ .../tests/test_legacy_closure_scope.py | 4 ++++ .../test_winter_heating_payment_properties.py | 4 ++++ 9 files changed, 58 insertions(+), 12 deletions(-) diff --git a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py index 9648177f44..1369c4b039 100644 --- a/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py +++ b/policyengine_uk/tests/test_benefit_cap_specified_benefits_properties.py @@ -4,7 +4,9 @@ - the claimant or partner receives ESA that includes the support component (75F(1)(a); 83(1)(a)), industrial injuries benefit, attendance allowance - or an armed forces compensation payment (75F(1)(b)-(d); 83(1)(b)-(e)); + or an armed forces compensation payment (75F(1)(b)-(d); 83(1)(b)-(e)). A + Universal Credit claim ends the couple's income-related ESA (SI 2013/983 + art. 4), so a claiming family can count only contributory ESA; - the claimant, partner or a child or young person they are responsible for receives disability living allowance (75F(1)(e); 83(1)(f)); - the claimant, partner or a young person they are responsible for receives @@ -279,8 +281,14 @@ def reference_specified_benefit(members, benunit_inputs, scheme): young_roles = ("young_person", "sixteen") if scheme == "uc" else ("young_person",) young_persons = [m for m in members if m["_role"] in young_roles] children = [m for m in members if m["_role"] == "child"] - # No capital, so the couple's income-related award is their reports. - couple_award = any(m["esa_income_reported"] > 0 for m in claimants) + # No capital, so the couple's income-related award is their reports, + # unless the family claims Universal Credit: the claim ends it (SI + # 2013/983 art. 4; esa_income_report_closed). would_claim_uc defaults to + # true. + claims_uc = benunit_inputs.get("would_claim_uc", True) + couple_award = not claims_uc and any( + m["esa_income_reported"] > 0 for m in claimants + ) def support_component(m): # Supplied, or assumed for anyone with their own allowance. @@ -398,9 +406,12 @@ def reference_cap(members): def capped_family(scheme): """30,000 of the scheme's award before the cap and none of the other's; the other family-level capped benefits are nil, so the capped total is - the award plus whatever the claimant's and partner's own benefits add.""" + the award plus whatever the claimant's and partner's own benefits add. + The family claims Universal Credit exactly when it is on it, so the + couple's income-related ESA continues only on Housing Benefit.""" return { **CAPPED_FAMILY, + "would_claim_uc": scheme == "uc", "universal_credit_pre_benefit_cap": 30_000 if scheme == "uc" else 0, "housing_benefit_pre_benefit_cap": 30_000 if scheme == "hb" else 0, } diff --git a/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py b/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py index 8a1104b6c1..39c805137f 100644 --- a/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py +++ b/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py @@ -46,7 +46,11 @@ def family(award): reported: {YEAR: 3_000}, }, }, - "benunits": {"family": {"members": members}}, + # Not claiming Universal Credit, which would end the claimant's + # income-related ESA (esa_income_report_closed). + "benunits": { + "family": {"members": members, "would_claim_uc": {YEAR: False}} + }, "households": {"home": {"members": members}}, } ) @@ -154,7 +158,11 @@ def test_the_raw_reported_total_is_read_through_the_reports(award): reported: {YEAR: 3_000}, }, }, - "benunits": {"family": {"members": members}}, + # Not claiming Universal Credit, which would end the partner's + # income-related ESA (esa_income_report_closed). + "benunits": { + "family": {"members": members, "would_claim_uc": {YEAR: False}} + }, "households": {"home": {"members": members, "savings": {YEAR: 10_000}}}, } ) diff --git a/policyengine_uk/tests/test_disable_simulated_benefits_benefit_cap.py b/policyengine_uk/tests/test_disable_simulated_benefits_benefit_cap.py index cf8dbee648..3647f78d50 100644 --- a/policyengine_uk/tests/test_disable_simulated_benefits_benefit_cap.py +++ b/policyengine_uk/tests/test_disable_simulated_benefits_benefit_cap.py @@ -234,7 +234,11 @@ def test_a_couple_that_is_not_the_claimant_and_partner_reads_the_stored_award(): "claimant": {"age": 40, "esa_income_reported": 3_000}, "partner": {"age": 38, "esa_income_reported": 2_000}, }, - "benunits": {"couple": {"members": ["claimant", "partner"]}}, + # Not claiming Universal Credit, which would end the couple's + # income-related ESA (esa_income_report_closed). + "benunits": { + "couple": {"members": ["claimant", "partner"], "would_claim_uc": False} + }, "households": {"home": {"members": ["claimant", "partner"]}}, } ) diff --git a/policyengine_uk/tests/test_income_support_eligibility_properties.py b/policyengine_uk/tests/test_income_support_eligibility_properties.py index 232ec21431..cb3319414a 100644 --- a/policyengine_uk/tests/test_income_support_eligibility_properties.py +++ b/policyengine_uk/tests/test_income_support_eligibility_properties.py @@ -308,7 +308,9 @@ def situation(units, capital_as_savings, esa_income, jsa_income): people[name] = {k: {YEAR: v} for k, v in inputs.items()} people[name]["is_claimant_or_partner"] = {YEAR: claimant_or_partner} names.append(name) - benunits[f"b{i}"] = {"members": names} + # Legacy claimants: a Universal Credit claim would end the couple's + # income-related ESA (esa_income_report_closed). + benunits[f"b{i}"] = {"members": names, "would_claim_uc": {YEAR: False}} households[f"h{i}"] = {"members": names} if capital_as_savings: households[f"h{i}"]["savings"] = {YEAR: capital} diff --git a/policyengine_uk/tests/test_income_support_gate_reads_claimant_or_partner_awards.py b/policyengine_uk/tests/test_income_support_gate_reads_claimant_or_partner_awards.py index b14da3de7b..24894a7fb4 100644 --- a/policyengine_uk/tests/test_income_support_gate_reads_claimant_or_partner_awards.py +++ b/policyengine_uk/tests/test_income_support_gate_reads_claimant_or_partner_awards.py @@ -270,11 +270,14 @@ def simulation(families, entered=None): name = f"p{i}_{j}" people[name] = {k: {YEAR: v} for k, v in inputs.items()} names.append(name) + # Legacy claimants: a Universal Credit claim would end the couple's + # income-related ESA (esa_income_report_closed). benunits[f"b{i}"] = { "members": names, "income_support_assessable_capital": {YEAR: capital}, "esa_income_assessable_capital": {YEAR: capital}, "jsa_income_assessable_capital": {YEAR: capital}, + "would_claim_uc": {YEAR: False}, } for award, values in (entered or {}).items(): benunits[f"b{i}"][award] = {YEAR: values[i]} diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py index 7850e3a79a..af9146ab85 100644 --- a/policyengine_uk/tests/test_legacy_award_readers_properties.py +++ b/policyengine_uk/tests/test_legacy_award_readers_properties.py @@ -295,10 +295,13 @@ def situation(units, year=YEAR, benunit_inputs=None): names.append(name) # claims_all_entitled_benefits sums reports across the whole # simulation, so fix the take-up mode per benefit unit: the variants - # must not share it. + # must not share it. The families are on legacy awards and do not + # claim Universal Credit, which would end the couple's income-related + # ESA (esa_income_report_closed). benunits[f"b{i}"] = { "members": names, "claims_all_entitled_benefits": {year: False}, + "would_claim_uc": {year: False}, **{k: {year: v} for k, v in (benunit_inputs or {}).items()}, } households[f"h{i}"] = { @@ -957,6 +960,7 @@ def test_other_members_esa_never_changes_non_dependant_exemption(drawn): benunits[f"nondep_unit_{tag}"] = { "members": unit, "universal_credit": {year: 0}, + "would_claim_uc": {year: False}, } households[f"h_{tag}"] = { "members": names + unit, @@ -1020,7 +1024,7 @@ def test_claimant_and_partner_awards_count_in_full_as_their_ctr_income(): award adds exactly its amount. """ ctr = "council_tax_reduction_applicable_income" - no_uc = {"universal_credit": 0} + no_uc = {"universal_credit": 0, "would_claim_uc": False} claimant_esa = {"claimant": {**CLAIMANT, "esa_income_reported": 5_000}} assert np.isclose( _difference(ctr, {"claimant": CLAIMANT}, claimant_esa, no_uc), 5_000 @@ -1099,7 +1103,7 @@ def _claimant_and_other(other_report): } return { "people": people, - "benunits": {"b": {"members": list(people)}}, + "benunits": {"b": {"members": list(people), "would_claim_uc": {YEAR: False}}}, "households": {"h": {"members": list(people)}}, } @@ -1155,12 +1159,14 @@ def test_late_zero_entry_overrides_the_claimants_reports(): def _situation(people, year=YEAR): + # Not claiming Universal Credit, which would end the claimant's + # income-related ESA (esa_income_report_closed). names = list(people) return { "people": { n: {k: {year: v} for k, v in inputs.items()} for n, inputs in people.items() }, - "benunits": {"b": {"members": names}}, + "benunits": {"b": {"members": names, "would_claim_uc": {year: False}}}, "households": {"h": {"members": names}}, } @@ -1286,6 +1292,7 @@ def member(age, role, report, works): "non_dependant_unit": { "members": ["claimant", "other"], "universal_credit": {year: 0}, + "would_claim_uc": {year: False}, }, }, "households": { diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 1779a07305..3cb5d833d2 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -151,10 +151,13 @@ def situation(units, year): people[name]["is_claimant_or_partner"] = {year: claimant_or_partner} people[name]["is_parent"] = {year: claimant_or_partner and bool(children)} names.append(name) + # Legacy claimants: a Universal Credit claim would end the couple's + # income-related ESA (esa_income_report_closed). benunits[f"b{i}"] = { "members": names, "esa_income_assessable_capital": {year: capital}, "jsa_income_assessable_capital": {year: capital}, + "would_claim_uc": {year: False}, } households[f"h{i}"] = {"members": names} return {"people": people, "benunits": benunits, "households": households} diff --git a/policyengine_uk/tests/test_legacy_closure_scope.py b/policyengine_uk/tests/test_legacy_closure_scope.py index 4126a21ddc..e3f4428e40 100644 --- a/policyengine_uk/tests/test_legacy_closure_scope.py +++ b/policyengine_uk/tests/test_legacy_closure_scope.py @@ -36,11 +36,15 @@ def household(year, claimant=None, partner=None, outside=None, child=False): name: {key: {str(year): value} for key, value in inputs.items()} for name, inputs in members.items() }, + # The family does not claim Universal Credit of its own accord: that + # claim would end the couple's income-related ESA + # (esa_income_report_closed). "benunits": { "unit": { "members": list(members), "esa_income_assessable_capital": {str(year): 0}, "jsa_income_assessable_capital": {str(year): 0}, + "would_claim_uc": {str(year): False}, } }, "households": {"household": {"members": list(members)}}, diff --git a/policyengine_uk/tests/test_winter_heating_payment_properties.py b/policyengine_uk/tests/test_winter_heating_payment_properties.py index 2639d8881b..66b165de44 100644 --- a/policyengine_uk/tests/test_winter_heating_payment_properties.py +++ b/policyengine_uk/tests/test_winter_heating_payment_properties.py @@ -208,6 +208,10 @@ def situation(drawn, year): variable: {year: AWARD if unit["award"] == key else 0} for key, variable in AWARD_INPUTS.items() }, + # The family claims Universal Credit only when it is on it: a + # claim would end the couple's income-related ESA + # (esa_income_report_closed). + "would_claim_uc": {year: unit["award"] == "UC"}, } benunits[f"h{h}_b{u}"] = benunit hh[f"h{h}"] = {"members": names, "country": {year: household["country"]}} From 1abee5ea0d78a6117da251485378cdb418bbff95 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 22 Oct 2026 04:24:50 -0400 Subject: [PATCH 3/3] Keep the Income Support ESA entry tests' family off Universal Credit The carer reports income-related ESA in some of these tests, and a UC claim (would_claim_uc defaults to true) now ends it, so the family says it does not claim UC. CI shards 1 and 5 failed on these six tests at e9aec0d48; all 20 pass locally. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_income_support_esa_entered_directly.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/policyengine_uk/tests/test_income_support_esa_entered_directly.py b/policyengine_uk/tests/test_income_support_esa_entered_directly.py index ce9b42862f..d3433a7052 100644 --- a/policyengine_uk/tests/test_income_support_esa_entered_directly.py +++ b/policyengine_uk/tests/test_income_support_esa_entered_directly.py @@ -44,11 +44,14 @@ def simulation(people, benunit=None): return Simulation( situation={ "people": people, + # Legacy claimants: a Universal Credit claim would end the + # carer's income-related ESA (esa_income_report_closed). "benunits": { "b": { "members": names, "income_support_assessable_capital": {YEAR: 0}, "esa_income_assessable_capital": {YEAR: 0}, + "would_claim_uc": {YEAR: False}, **(benunit or {}), } },