diff --git a/changelog.d/2565.fixed.md b/changelog.d/2565.fixed.md new file mode 100644 index 0000000000..aee0e6c5ca --- /dev/null +++ b/changelog.d/2565.fixed.md @@ -0,0 +1 @@ +The model no longer pays income-related ESA to a claimant who has reached pensionable age, or to a couple that would get State Pension Credit without that award, whether the model's formulas, an input or a reform set that Pension Credit, and it pays no Pension Credit to a couple while it pays the couple income-related ESA (Welfare Reform Act 2007 s.1(3)(c) and Sch 1 para 6(1)(c) and (d)). diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index 9f9cb76332..f9f85076b6 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -25,6 +25,26 @@ against pay (reg 98(11)). - ESA partner (para 6(1)(f), reg 42(1)): 24 hours or more, unless a carer (reg 43(2)(c)). +- ESA age (WRA 2007 s.1(3)(c)): no claimant may have reached pensionable age, + their own State Pension age (s.1(6); Pensions Act 1995 Sch 4 para 1). A + caller that reads the model's pensionable age passes it as is_SP_age; + otherwise ages avoid 65 to 69. In the years tested (2025 and 2026) anyone + aged 64 or under was born after 5 October 1960, reaches pensionable age at + 66 or later, and so falls below it; anyone aged 70 or over was born before + 6 April 1960 and reached it at 66 or earlier. +- ESA and State Pension Credit (para 6(1)(c) and (d)): neither the claimant + nor the other member of the couple may be entitled to it. A caller that + knows passes whether the couple is entitled (on_pension_credit): it would + claim, and it would get Pension Credit with no income-related ESA in the + family. Otherwise no member reports Pension Credit or Housing Benefit, so + no mixed-age couple keeps the SI 2019/37 art 4 saving and the + exclusion of mixed-age couples is in force (SPCA 2002 s.4(1A)). Only a + couple every member of which has reached the qualifying age can then be + entitled, and from 2019-20 that is pensionable age for everyone (SPCA 2002 + s.1(6)), so each of them has reached pensionable age and has no claim + under s.1(3)(c), whether or not DWP pays the couple Pension Credit. A + member outside the couple keeps their own claim even when the couple has + an entitlement to Pension Credit. - JSA claimant (s.1(2)(e), reg 51(1)(a)): 16 hours or more, with no exception for carers doing unrelated paid work. The other member of the couple: 24 hours or more (s.3(1)(e), reg 51(1)(b)). In a joint-claim @@ -153,17 +173,40 @@ def esa_claimant_in_remunerative_work(adult, parameters): return not (within_lower_limit or permitted_work) +def reached_pensionable_age(adult): + """WRA 2007 s.1(3)(c): whether the adult has reached pensionable age.""" + if "is_SP_age" in adult: + return bool(adult["is_SP_age"]) + assert not 65 <= adult["age"] < 70, adult + return adult["age"] >= 70 + + +def couple_on_pension_credit(couple): + """Para 6(1)(c) and (d), for a couple with no Pension Credit or Housing + Benefit reports in 2025 or 2026: a couple every member of which has + reached the qualifying age, each of whom has also reached pensionable + age and so has no claim under s.1(3)(c), whether or not DWP pays the + couple Pension Credit.""" + assert not any( + adult.get("pension_credit_reported", 0) + or adult.get("housing_benefit_reported", 0) + for adult in couple + ), couple + return len(couple) > 0 and all(reached_pensionable_age(a) for a in couple) + + def legacy_benefits_closed(couple, others, parameters): """Annual closure proxy for the claimant's and partner's own award. Reports by other members neither close nor shield it (SI 2014/1230 regs 44(1), (4) and 46(1)). This is the existing annual proxy, not a - notice/deadline model. No tax-credit reports are generated here. Ages - avoid the changing Pension Credit qualifying-age boundary. + notice/deadline model. The generators draw tax-credit reports only in + years with tax credits, in which they close nothing. Ages avoid the + changing Pension Credit qualifying-age boundary. """ dwp = parameters.gov.dwp members = couple + others - assert not any( + assert dwp.tax_credits.active or not any( member.get("child_tax_credit_reported", 0) or member.get("working_tax_credit_reported", 0) for member in members @@ -187,16 +230,21 @@ def esa_abolition_claim(couple, parameters): reports (esa_income_report_closed). The abolition reaches every remaining award on 1 July 2026 in England - (SI 2025/1148 art. 3A), the households' default country, and no member - here has the appointee saving, so it reaches any award reported in a - year with less than all of it payable. would_claim_uc_at_legacy_closure - keeps its default, true. The conditions are those of the other - working-age closures: no Pension Credit report, and not all pension age. + (SI 2025/1148 art. 3A), the households' default country, so it reaches + any award reported in a year with less than all of it payable. Where a + member who reports the award has an appointee saving + (esa_income_appointee_saving), DWP keeps the whole claim payable (art. + 3A(2)); like esa_income_payable_share, this function ignores a saving + held only by a member who reports nothing. + would_claim_uc_at_legacy_closure keeps its default, true. The conditions + are those of the other working-age closures: no Pension Credit report, + and not all pension age. """ payable = parameters.gov.dwp.ESA.income.awards_payable.great_britain - reports_award = any(m.get("esa_income_reported", 0) > 0 for m in couple) + reporters = [m for m in couple if m.get("esa_income_reported", 0) > 0] + saved = any(m.get("esa_income_appointee_saving", False) for m in reporters) on_pension_credit = any(m.get("pension_credit_reported", 0) > 0 for m in couple) - if not reports_award or payable >= 1 or on_pension_credit: + if not reporters or saved or payable >= 1 or on_pension_credit: return False assert all(not 65 <= adult["age"] < 70 for adult in couple), couple return not all(adult["age"] >= 70 for adult in couple) @@ -233,12 +281,25 @@ def candidates(couple, others, reported, parameters): ] -def esa_screen(couple, others, capital, parameters): +def esa_screen(couple, others, capital, parameters, on_pension_credit=None): """esa_income_eligible for a claimant and partner (couple) and other - members of the benefit unit outside the family (others).""" + members of the benefit unit outside the family (others). on_pension_credit + is whether the couple is entitled to State Pension Credit, if the caller + knows it.""" ESA = parameters.gov.dwp.ESA.income members = couple + others + def barred(i): + # WRA 2007 s.1(3)(c), for every claimant. + if reached_pensionable_age(members[i]): + return True + # Sch 1 para 6(1)(c) and (d), for the couple's claim only. + if i >= len(couple): + return False + if on_pension_credit is not None: + return bool(on_pension_credit) + return couple_on_pension_credit(couple) + def other_member_works(i): if i >= len(couple): return False @@ -250,7 +311,8 @@ def other_member_works(i): ) passes = any( - not esa_claimant_in_remunerative_work(members[i], parameters) + not barred(i) + and not esa_claimant_in_remunerative_work(members[i], parameters) and not other_member_works(i) for i in candidates(couple, others, "esa_income_reported", parameters) ) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_pension_age.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_pension_age.yaml new file mode 100644 index 0000000000..6987ebea0d --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_pension_age.yaml @@ -0,0 +1,1027 @@ +# Income-related ESA and pensionable age or State Pension Credit. +# Welfare Reform Act 2007 s.1(3)(c): a claimant who has reached pensionable +# age is not entitled to ESA. Pensionable age is the person's own State +# Pension age (s.1(6); Pensions Act 1995 Sch 4 para 1). +# Sch 1 para 6(1)(c) and (d): a claimant gets the income-related allowance +# only if the claimant "is not entitled to state pension credit" and "is not a +# member of a couple the other member of which is entitled to ... state +# pension credit". The model reads the Pension Credit the couple would get +# with no income-related ESA in the family, B(0), and refuses the couple's +# claim where B(0) exceeds nil. B(e) is the Pension Credit the family would +# get with the award e. Where B(0) is nil, the model pays the couple's award +# and no Pension Credit, whatever B(e) (cases 18, 25 and 26). A mixed-age +# couple has no entitlement to State Pension Credit from 15 May 2019 (State +# Pension Credit Act 2002 s.4(1A)) unless it keeps the SI 2019/37 art 4 +# saving. +# Nobody here claims Universal Credit (would_claim_uc false), since a couple +# that claims it loses its income-related ESA anyway. No case has capital, so +# no tariff income: the model pays an award that passes at the amount +# reported. With no income, a family gets the guarantee credit's standard +# minimum guarantee for 52 weeks: in 2026-27, £238.00 for a single claimant and +# £363.25 for a couple (SPC Regs 2002 reg 6(1), as amended by the Social +# Security Benefits Up-rating Order 2026, SI 2026/148, art 29(2)), so +# 52 x 238.00 = 12_376 and 52 x 363.25 = 18_889. Nobody here in 2026 reached +# pensionable age before 6 April 2016, so no savings credit is due (SPCA 2002 +# s.3(1)). + +- name: Case 1, a single claimant aged 70 who reports income-related ESA gets Pension Credit and no ESA. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + # Born in 1956: pensionable age 66 (Pensions Act 1995 Sch 4 para 1 rule 6). + is_SP_age: true + # The claimant fails s.1(3)(c): 0, not the 5_000 reported. + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: false + # 52 x 238.00. + pension_credit: 12_376 + +- name: Case 2, a couple aged 70 and 68 where the 68-year-old reports income-related ESA get Pension Credit and no ESA. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + is_claimant_or_partner: true + person2: + age: 68 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # Both born after 5 October 1954 and before 6 April 1960: pensionable + # age 66 (rule 6). + is_SP_age: [true, true] + meets_pension_credit_age_conditions: true + # The 68-year-old fails s.1(3)(c), and para 6(1)(c) and (d) too. + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: [false, false] + # 52 x 363.25. + pension_credit: 18_889 + +- name: Case 3, a saved mixed-age couple aged 73 and 60 get Pension Credit and no ESA on the younger member's report. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 73 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + # The couple has had Pension Credit since before 15 May 2019. + pension_credit_reported: 9_000 + person2: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # SI 2019/37 art 4(1): born 1 March 1953, the older member reached the + # qualifying age on 6 January 2016 (Pensions Act 1995 Sch 4 para 1 rule 3, + # table 1, via SPCA 2002 s.1(6)), so the couple could be entitled to + # Pension Credit on 14 May 2019, and it reports Pension Credit now, so + # it keeps Pension Credit despite s.4(1A). + has_mixed_age_couple_pension_credit_saving: true + # The 60-year-old has not reached pensionable age, so meets s.1(3)(c). + is_SP_age: [true, false] + meets_pension_credit_age_conditions: true + # Para 6(1)(d): B(0) = 52 x 363.25 = 18_889 is payable, so the other + # member of the couple is entitled to State Pension Credit and the + # 60-year-old gets no income-related ESA. + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: [false, false] + # 52 x 363.25. He reached pensionable age at 65 on 1 March 2018 (rule + # 1), after 6 April 2016, so no savings credit is due (SPCA 2002 s.3(1)). + pension_credit: 18_889 + +- name: Case 4, the same mixed-age couple without the saving keep income-related ESA and get no Pension Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 73 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + person2: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # DWP keeps this award in payment after the 1 July 2026 abolition + # under the claimant's appointee saving (SI 2025/1148 art. 3A(2)), + # so the model pays it for the whole of 2026-27 + # (esa_income_payable_share). + esa_income_appointee_saving: true + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # Neither member reports Pension Credit or pension-age Housing Benefit, + # so the couple keeps no saving, and the 73-year-old has no entitlement + # to Pension Credit while the partner is under the qualifying age (SPCA + # 2002 s.4(1A)). + has_mixed_age_couple_pension_credit_saving: false + meets_pension_credit_age_conditions: false + pension_credit: 0 + # The 60-year-old meets both para 6(1)(d) and s.1(3)(c): the 5_000. + esa_income_eligible: true + esa_income: 5_000 + claimant_or_partner_esa_income: 5_000 + is_on_income_related_esa: [false, true] + +- name: Case 5, a saved mixed-age couple that does not claim Pension Credit keep income-related ESA. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 73 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + pension_credit_reported: 9_000 + person2: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # DWP keeps this award in payment after the 1 July 2026 abolition + # under the claimant's appointee saving (SI 2025/1148 art. 3A(2)), + # so the model pays it for the whole of 2026-27 + # (esa_income_payable_share). + esa_income_appointee_saving: true + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [person1, person2] + output: + has_mixed_age_couple_pension_credit_saving: true + meets_pension_credit_age_conditions: true + # Entitlement to Pension Credit needs a claim (Social Security + # Administration Act 1992 s.1), so the 60-year-old meets para 6(1)(d). + pension_credit: 0 + esa_income_eligible: true + esa_income: 5_000 + claimant_or_partner_esa_income: 5_000 + is_on_income_related_esa: [false, true] + +- name: Case 6, a single claimant aged 60 keeps income-related ESA. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # DWP keeps this award in payment after the 1 July 2026 abolition + # under the claimant's appointee saving (SI 2025/1148 art. 3A(2)), + # so the model pays it for the whole of 2026-27 + # (esa_income_payable_share). + esa_income_appointee_saving: true + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + is_SP_age: false + meets_pension_credit_age_conditions: false + pension_credit: 0 + esa_income_eligible: true + esa_income: 5_000 + claimant_or_partner_esa_income: 5_000 + is_on_income_related_esa: true + +- name: Case 7, a man aged 63 in 2016 over the Pension Credit qualifying age but under pensionable age gets no income-related ESA while he would claim Pension Credit. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + # Born 1 March 1953, before 6 December 1953: pensionable age 65, on 1 + # March 2018 (Pensions Act 1995 Sch 4 para 1 rule 1), so he meets + # s.1(3)(c) in 2016-17. + is_SP_age: false + # The qualifying age for State Pension Credit is a woman's pensionable + # age for the same date of birth (SPCA 2002 s.1(6)): a woman born from 6 + # February to 5 March 1953 reached it on 6 January 2016 (rule 3, table 1). + has_attained_state_pension_credit_qualifying_age: true + meets_pension_credit_age_conditions: true + # Para 6(1)(c): with no income, B(0) is the minimum guarantee, 52 x + # 155.60 = 8_091.20 in 2016-17 (SPC Regs 2002 reg 6(1)(b)), so he is + # entitled to State Pension Credit. + esa_income_eligible: false + esa_income: 0 + is_on_income_related_esa: false + pension_credit: 8_091.20 + +- name: Case 8, the same man in 2016 keeps income-related ESA if he would not claim Pension Credit. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [person1] + output: + is_SP_age: false + pension_credit: 0 + esa_income_eligible: true + esa_income: 5_000 + is_on_income_related_esa: true + +- name: Case 9, a woman born on the same day reached pensionable age, so gets no income-related ESA in 2016 even without a Pension Credit claim. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: FEMALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [person1] + output: + # Rule 3, table 1: pensionable age on 6 January 2016. + is_SP_age: true + # s.1(3)(c). + esa_income_eligible: false + esa_income: 0 + is_on_income_related_esa: false + +- name: Case 10, before the mixed-age couple exclusion, a couple aged 70 and 60 that would claim Pension Credit get no ESA on the younger member's report. + period: 2018 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + is_claimant_or_partner: true + person2: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # SPCA 2002 s.4(1A) came into force on 15 May 2019 (SI 2019/37 art 3), so + # in 2018-19 the 70-year-old can claim Pension Credit for the couple. + is_mixed_age_couple: true + meets_pension_credit_age_conditions: true + # Para 6(1)(d). + esa_income_eligible: false + esa_income: 0 + is_on_income_related_esa: [false, false] + +- name: Case 11, a member outside the couple aged 70 who reports income-related ESA gets none. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 45 + is_claimant_or_partner: true + person2: + age: 70 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # They claim in their own right, and fail s.1(3)(c) too. + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: [false, false] + +- name: Case 12, a member outside the couple who claims income-related ESA in their own right keeps the award beside the couple's Pension Credit. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + is_claimant_or_partner: true + person2: + age: 40 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + # DWP keeps this award in payment after the 1 July 2026 abolition + # under the claimant's appointee saving (SI 2025/1148 art. 3A(2)), + # so the model pays it for the whole of 2026-27 + # (esa_income_payable_share). + esa_income_appointee_saving: true + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # The 70-year-old is a single claimant of Pension Credit: 52 x 238.00. + meets_pension_credit_age_conditions: true + pension_credit: 12_376 + # The 40-year-old is not a member of the 70-year-old's couple, claims in + # their own right and keeps their award of 5_000 (para 6(1)(d)). + esa_income_eligible: true + esa_income: 5_000 + claimant_or_partner_esa_income: 0 + is_on_income_related_esa: [false, true] + +- name: Case 13, where both members of a mixed-age couple report income-related ESA, only the one under pensionable age can be its payee. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # The model applies this member's appointee saving to the couple's + # whole claim (esa_income_payable_share), as it does the claimant's + # below. + esa_income_appointee_saving: true + person2: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # DWP keeps this award in payment after the 1 July 2026 abolition + # under the claimant's appointee saving (SI 2025/1148 art. 3A(2)), + # so the model pays it for the whole of 2026-27 + # (esa_income_payable_share). + esa_income_appointee_saving: true + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # Born in 1956, the 70-year-old reached the qualifying age after 14 May + # 2019, so the couple keeps no saving (SI 2019/37 art 4(1)). + has_mixed_age_couple_pension_credit_saving: false + meets_pension_credit_age_conditions: false + # The 60-year-old can claim; the model makes the couple's two reports one + # award of 5_000 + 5_000 = 10_000. + esa_income_eligible: true + esa_income: 10_000 + claimant_or_partner_esa_income: 10_000 + # s.1(3)(c): the 70-year-old cannot be the claimant, so the award is not + # payable to them. + is_on_income_related_esa: [false, true] + +- name: Case 14, where only the older member of an unsaved mixed-age couple has the support component, the couple gets neither benefit cap exception. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # The model applies this member's appointee saving to the couple's + # whole claim (esa_income_payable_share), as it does the claimant's + # below. + esa_income_appointee_saving: true + esa_includes_support_component: true + person2: + age: 60 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # DWP keeps this award in payment after the 1 July 2026 abolition + # under the claimant's appointee saving (SI 2025/1148 art. 3A(2)), + # so the model pays it for the whole of 2026-27 + # (esa_income_payable_share). + esa_income_appointee_saving: true + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + esa_income: 10_000 + # s.1(3)(c): only the 60-year-old can be the claimant, so the award is + # paid to them, without the support component. + is_on_income_related_esa: [false, true] + # HB Regs 2006 reg 75F(1) and UC Regs 2013 + # reg 83(1): the exception needs the award's support component, which + # belongs to the 70-year-old, who is not its claimant. + is_uc_benefit_cap_exempt_specified_benefit: false + is_housing_benefit_benefit_cap_exempt_specified_benefit: false + +# Cases 15 to 19 take one case for each pairing of B(0) and B(e). Each is a +# man aged 63 in 2016, born 1 March 1953: over the qualifying age for State +# Pension Credit (from 6 January 2016) and under his pensionable age (65, +# on 1 March 2018), with support-group income-related ESA. In 2016-17 the +# minimum guarantee is £155.60 a week, 52 x 155.60 = 8_091.20 a year (SPC +# Regs 2002 reg 6(1)(b)), and DWP disregards £5 a week of a single claimant's +# earnings (Sch VI para 5), or £20 a week while the claimant is on employment +# and support allowance (para 4(1)(a)(vii)). Below 65 no savings +# credit is due (SPCA 2002 s.3(1)). His income stays under the personal +# allowance (£11,000) and his earnings under the National Insurance primary +# threshold (£155 a week), so he owes no income tax or primary Class 1 +# contributions. + +- name: Case 15, where DWP would pay Pension Credit with or without the award, the man gets the Pension Credit due on his income without the award and no income-related ESA. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 6_011.20 + esa_includes_support_component: true + # £50 a week for 10 hours. + employment_income: 2_600 + hours_worked: 520 + # £40 a week. + private_pension_income: 2_080 + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + # B(0): income 50 + 40 - 5 = 85 a week, so 155.60 - 85 = 70.60 a week. + # B(e): 50 + 40 - 20 = 70, so 85.60 a week. B(0) exceeds nil, so he fails + # para 6(1)(c) and gets B(0): 52 x 70.60. + esa_income_eligible: false + esa_income: 0 + pension_credit: 3_671.20 + pension_credit_earnings_disregard: 260 + +- name: Case 16, where DWP would pay no Pension Credit with or without the award, the man keeps income-related ESA. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 4_919.20 + esa_includes_support_component: true + # £115.50 a week for 15 hours. + employment_income: 6_006 + hours_worked: 780 + # £61 a week. + private_pension_income: 3_172 + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + # B(0): 115.50 + 61 - 5 = 171.50 a week, over 155.60. B(e): 115.50 + 61 + # - 20 = 156.50, still over it. He gets no Pension Credit either way, so + # meets para 6(1)(c) and keeps the award: 52 x 94.60 = 4_919.20. + pension_credit: 0 + esa_income_eligible: true + esa_income: 4_919.20 + is_on_income_related_esa: true + +- name: Case 17, the same man keeps the same income-related ESA if he would not claim Pension Credit. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 4_919.20 + esa_includes_support_component: true + employment_income: 6_006 + hours_worked: 780 + private_pension_income: 3_172 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_pc: false + households: + household: + members: [person1] + output: + pension_credit: 0 + esa_income: 4_919.20 + +- name: Case 18, where DWP would pay Pension Credit only with the award, the man keeps income-related ESA and gets no Pension Credit. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_491.20 + esa_includes_support_component: true + employment_income: 6_006 + hours_worked: 780 + # £50 a week. + private_pension_income: 2_600 + benunits: + benunit: + members: [person1] + would_claim_uc: false + households: + household: + members: [person1] + output: + # In 2016-17 the minimum guarantee for a single claimant is £155.60 a week. + # B(0): 115.50 + 50 - 5 = 160.50 a week, over 155.60, so nil. B(e): + # 115.50 + 50 - 20 = 145.50, so 155.60 - 145.50 = 10.10 a week, or + # 52 x 10.10 = 525.20. B(0) is nil, so he meets para 6(1)(c) and the model + # pays the award, 52 x 105.60 = 5_491.20. + # The model pays no Pension Credit beside it. Parliament allows the award + # only while he "is not entitled to state pension credit" (WRA 2007 Sch 1 + # para 6(1)(c)), so with Pension Credit in payment his ESA would stop, and + # with it the £20 that DWP disregards for a claimant "in receipt of ... + # employment and support allowance" (SPC Regs 2002 Sch VI para + # 4(1)(a)(vii)). DWP keeps disregarding the £20 after the award ends only + # where (para 4(2)) "£20 was disregarded in respect of earnings taken into + # account in that award". He works 15 hours a week, under the 16-hour + # limit, and his £115.50 does not exceed the 2016-17 permitted work limit + # of £115.50: 16 hours at the £7.20 national living wage, £115.20, which + # esa_exempt_work_higher_earnings_limit rounds up to the next 50p, citing + # ESA Regs 2008 reg 45(9A) (from + # gov.dwp.ESA.exempt_work.higher_earnings_limit_minimum_wage_hours and + # gov.hmrc.minimum_wage.non_apprentice). So DWP disregards "any earnings + # derived from that work which do not exceed the limits" (ESA Regs 2008 + # Sch 7 para 5): all of them, not £20. The expected values agree with the + # reading under which "£20 was disregarded" means the £20 disregard of + # Sch 7 paras 6 and 7(1), as under para 5 DWP disregards "any earnings ... + # which do not exceed the limits", not a fixed £20. On that reading he + # fails para 4(2)(a), his Pension Credit would fall back to B(0), nil, and + # his ESA would come back, so ESA with no Pension Credit holds and Pension + # Credit with no ESA does not. On another reading, para 4(2)(a) holds + # wherever DWP disregarded at least £20 of earnings in the award, and he + # would then keep £525.20 of Pension Credit with no ESA while he stays in + # employment (para 4(2)(b)). + esa_income_eligible: true + esa_income: 5_491.20 + claimant_or_partner_esa_income: 5_491.20 + esa_exempt_work_higher_earnings_limit: 115.50 + pension_credit_earnings_disregard: 1_040 + pension_credit_entitlement: 525.20 + pension_credit: 0 + +- name: Case 19, a couple aged 66 and 58 to whom DWP would pay Pension Credit only without the award, since HMRC then applies working tax credit's income test, get Pension Credit and no income-related ESA. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + is_claimant_or_partner: true + # 20 hours a week. + employment_income: 10_000 + hours_worked: 1_040 + working_tax_credit_reported: 1 + person2: + age: 58 + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # Before 15 May 2019 the 66-year-old can claim Pension Credit for the + # couple. In 2016-17 the couple minimum guarantee is £237.55 a week, + # 52 x 237.55 = 12_352.60, and the couple's standard earnings disregard + # is £10 a week (Sch VI para 5), £20 with the award. The 66-year-old has + # reached pensionable age, so pays no primary Class 1 contributions + # (SSCBA 1992 s.6(3)), and 10_000 is under the £11,000 personal + # allowance, so his earnings count in full. + # Working tax credit: the basic and couple elements, 1_960 + 2_010 = + # 3_970 (WTC (Entitlement and Maximum Rate) Regs 2002 Sch 2), which counts + # as Pension Credit income (SPCA 2002 s.15(1)(b)). + # B(e): with the award HMRC pays working tax credit without its income + # test (TCA 2002 s.7(2)), so working tax credit comes to 3_970, and + # 10_000 + 3_970 - 1_040 = 12_930, over 12_352.60: nil. DWP counts no + # working tax credit in the savings credit's qualifying income (SPC Regs + # 2002 reg 9(a); savings_credit_income): 10_000 - 1_040 = 8_960, 172.31 a + # week, under the £212.97 couple threshold: nil. + # B(0): the couple would get no Pension Credit on 3_970 either, so it has + # no passport, and HMRC applies the income test: 41% of income over £6,420 + # comes off, 3_970 - 0.41 x (10_000 - 6_420) = 2_502.20. Then 10_000 + + # 2_502.20 - 520 = 11_982.20, so 12_352.60 - 11_982.20 = 370.40. B(0) + # exceeds nil, so the younger member fails para 6(1)(d), and the couple + # gets B(0). + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + working_tax_credit: 2_502.20 + pension_credit: 370.40 + +- name: Case 20, in 2018 a couple aged 66 and 58 to whom DWP would pay no Pension Credit on their income keep support-group ESA and both benefit cap exceptions. + period: 2018 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 66 + date_of_birth: 19520701 + gender: MALE + is_claimant_or_partner: true + # £200 a week. + private_pension_income: 10_400 + person2: + age: 58 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 2_537.60 + esa_includes_support_component: true + # £124 a week for 15 hours. + employment_income: 6_448 + hours_worked: 780 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # In 2018-19 the couple minimum guarantee is £248.80 a week. B(0): 200 + + # 124 - 10 = 314 a week; B(e): 200 + 124 - 20 = 304. Both exceed it, so + # nil either way. (Each member's income stays under the £11,850 personal + # allowance, and £124 a week under the £162 National Insurance primary + # threshold, so neither member owes income tax or primary Class 1 + # contributions.) The younger member meets para 6(1)(d). + pension_credit: 0 + esa_income: 2_537.60 + claimant_or_partner_esa_income: 2_537.60 + is_on_income_related_esa: [false, true] + # HB Regs 2006 reg 75F(1) and UC Regs 2013 + # reg 83(1): the award with its support component. + is_uc_benefit_cap_exempt_specified_benefit: true + is_housing_benefit_benefit_cap_exempt_specified_benefit: true + # So the model applies no Universal Credit benefit cap. + is_uc_benefit_cap_exempt: true + +# Cases 21 to 24 supply Pension Credit, or a part of it, as an input. The model +# then reads the award it pays without the couple's income-related ESA, +# whatever the Pension Credit age conditions and the claim +# (would_claim_pc), since the caller, not the formulas, sets the award. The +# man of cases 7 and 8 meets the age conditions; a man aged 40, born 1 March +# 1976, does not. + +- name: Case 21, a man who reports income-related ESA and has £2,000 of Pension Credit as an input gets no ESA, though he would not claim Pension Credit. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_pc: false + pension_credit: 2_000 + households: + household: + members: [person1] + output: + # Para 6(1)(c): he has an entitlement to the £2,000 of Pension Credit. + pension_credit: 2_000 + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + +- name: Case 22, a man aged 40 whose Pension Credit entitlement is an input of £2,000 gets that Pension Credit and no income-related ESA. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + date_of_birth: 19760301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + pension_credit_entitlement: 2_000 + households: + household: + members: [person1] + output: + # He would claim, so pension_credit pays the entitlement, though he is + # under the qualifying age for State Pension Credit. + meets_pension_credit_age_conditions: false + pension_credit: 2_000 + esa_income_eligible: false + esa_income: 0 + +- name: Case 23, a man aged 40 whom an input makes eligible for Pension Credit, with no income, gets the minimum guarantee and no income-related ESA. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + date_of_birth: 19760301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + is_pension_credit_eligible: true + households: + household: + members: [person1] + output: + meets_pension_credit_age_conditions: false + # Without the award his income is nil, so his guarantee credit comes to + # the minimum guarantee, 52 x 155.60 = 8_091.20 (SPC Regs 2002 reg + # 6(1)(b)). Under 65 he gets no savings credit. + guarantee_credit: 8_091.20 + pension_credit: 8_091.20 + esa_income_eligible: false + esa_income: 0 + +- name: Case 24, the man of case 21 with £2,000 of guarantee credit as an input keeps income-related ESA if he would not claim Pension Credit. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [person1] + would_claim_uc: false + would_claim_pc: false + guarantee_credit: 2_000 + households: + household: + members: [person1] + output: + # pension_credit pays the entitlement only on a claim, so he gets no + # Pension Credit, and the model pays the award. + pension_credit: 0 + esa_income_eligible: true + esa_income: 5_000 + +- name: Case 25, a couple in whose award DWP may disregard £20 of the partner's part-time earnings keeps income-related ESA and gets no Pension Credit where the couple gets Pension Credit only with the award. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_491.20 + esa_includes_support_component: true + # £135 a week. + private_pension_income: 7_020 + person2: + age: 60 + date_of_birth: 19560301 + gender: FEMALE + is_claimant_or_partner: true + # £115.50 a week for 15 hours, under the 24 hours of WRA 2007 Sch 1 + # para 6(1)(f) (ESA Regs 2008 reg 42(1)). + employment_income: 6_006 + hours_worked: 780 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # The man reports the award and the woman has the earnings, so case 18's + # pension of £50 a week becomes £135 to land the couple in the same + # pairing. In 2016-17 the couple minimum guarantee is £237.55 a week, + # 52 x 237.55 = 12_352.60, and the couple's standard earnings disregard is + # £10 a week (Sch VI para 5), £20 with the award (para 4(1)(a)(vii)). + # Income: 115.50 + 135 = 250.50 a week, 52 x 250.50 = 13_026. Neither + # member owes income tax or primary Class 1 contributions on £115.50 a + # week in 2016-17. + # B(0): 13_026 - 520 = 12_506, over 12_352.60, so nil. B(e): 13_026 - + # 1_040 = 11_986, so 12_352.60 - 11_986 = 366.60, or 52 x 7.05. Neither + # member has reached pensionable age, so no savings credit is due (SPCA + # 2002 s.3(1)). + # Here the law may differ from the model. DWP disregards £20 of earnings + # "in a case where the claimant's partner is in part-time employment and + # paragraph 6 does not apply" (ESA Regs 2008 Sch 7 para 7(4)). If DWP + # disregarded £20 of the woman's earnings in the man's award, DWP keeps + # disregarding the £20 (SPC Regs 2002 Sch VI para 4(2)) where "£20 was + # disregarded in respect of earnings taken into account in that award" + # and "the person whose earnings qualified for the disegard continues in + # employment after the termination of that award". Once the couple + # claimed, it could then keep £366.60 of Pension Credit with no ESA. The + # model holds no record of how an award started, and + # pension_credit_earnings_disregard does not apply para 4(2), so the model + # pays this couple the award and no Pension Credit, as in case 18. + # The same holds where DWP disregarded £20 of the partner's earnings + # because the claimant's permitted-work earnings fell short of the limit + # (Sch 7 para 6, case 26), or £20 of the claimant's own earnings (Sch 7 + # para 7(1) and (2)). + meets_pension_credit_age_conditions: true + esa_income_eligible: true + esa_income: 5_491.20 + claimant_or_partner_esa_income: 5_491.20 + pension_credit_earnings_disregard: 1_040 + pension_credit_entitlement: 366.60 + pension_credit: 0 + +- name: Case 26, a couple in whose award DWP may disregard £20 of the partner's earnings under the permitted-work shortfall keeps income-related ESA and gets no Pension Credit where the couple gets Pension Credit only with the award. + period: 2016 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 63 + date_of_birth: 19530301 + gender: MALE + is_claimant_or_partner: true + esa_income_reported: 5_491.20 + esa_includes_support_component: true + # £90 a week for 15 hours, under the 16 hours and the £115.50 limit of + # permitted work (ESA Regs 2008 reg 45(4)). + employment_income: 4_680 + hours_worked: 780 + # £130 a week. + private_pension_income: 6_760 + person2: + age: 60 + date_of_birth: 19560301 + gender: FEMALE + is_claimant_or_partner: true + # £30 a week for 5 hours, under the 24 hours of WRA 2007 Sch 1 para + # 6(1)(f) (ESA Regs 2008 reg 42(1)). + employment_income: 1_560 + hours_worked: 260 + benunits: + benunit: + members: [person1, person2] + would_claim_uc: false + households: + household: + members: [person1, person2] + output: + # In 2016-17 the couple minimum guarantee is £237.55 a week, + # 52 x 237.55 = 12_352.60, and the couple's standard earnings disregard is + # £10 a week (Sch VI para 5), £20 with the award (para 4(1)(a)(vii)). + # Income: 90 + 130 + 30 = 250 a week, 52 x 250 = 13_000. Neither member + # owes primary Class 1 contributions on £90 or £30 a week in 2016-17. His + # £11,440 of earnings and pension exceeds the £11,000 personal allowance + # by £440. The model takes the couple to be married (is_married) and to + # claim the marriage allowance (would_claim_marriage_allowance), so it + # transfers £1,100 of her unused allowance to him (marriage_allowance, 10% + # of £11,000) and neither member owes income tax or contributions on the + # earnings. Without the transfer he would owe £88 and B(e) would come to + # 480.60. + # B(0): 13_000 - 520 = 12_480, over 12_352.60, so nil. B(e): 13_000 - + # 1_040 = 11_960, so 12_352.60 - 11_960 = 392.60, or 52 x 7.55. Neither + # member has reached pensionable age, so no savings credit is due (SPCA + # 2002 s.3(1)). + # Here the law may differ from the model. His £90 falls more than £20 + # short of 16 x £7.20 = £115.20, so DWP disregards the woman's earnings + # "to the extent that the claimant's earnings are less than ... 16 x + # National Minimum Wage, but only up to a maximum of £20" (ESA Regs 2008 + # Sch 7 para 6): £20 of her £30. DWP keeps disregarding the £20 (SPC Regs + # 2002 Sch VI para 4(2)) where "£20 was disregarded in respect of earnings + # taken into account in that award" and "the person whose earnings + # qualified for the disegard continues in employment after the termination + # of that award". Once the couple claimed, it could then keep £392.60 of + # Pension Credit with no ESA while she stays in work. The model holds no + # record of how an award started, and pension_credit_earnings_disregard + # does not apply para 4(2), so the model pays this couple the award and + # no Pension Credit, as in cases 18 and 25. + meets_pension_credit_age_conditions: true + esa_income_eligible: true + esa_income: 5_491.20 + claimant_or_partner_esa_income: 5_491.20 + esa_exempt_work_higher_earnings_limit: [115.50, 115.50] + pension_credit_employment_income_tax: 0 + pension_credit_earnings_disregard: 1_040 + pension_credit_income: 11_960 + pension_credit_entitlement: 392.60 + pension_credit: 0 diff --git a/policyengine_uk/tests/test_esa_income_pension_age_properties.py b/policyengine_uk/tests/test_esa_income_pension_age_properties.py new file mode 100644 index 0000000000..9331151624 --- /dev/null +++ b/policyengine_uk/tests/test_esa_income_pension_age_properties.py @@ -0,0 +1,1279 @@ +"""The model refuses a couple income-related ESA only where the couple would +get State Pension Credit without it. + +Parliament bars ESA for a claimant who has reached pensionable age, their own +State Pension age (Welfare Reform Act 2007 s.1(3)(c) and s.1(6)), and bars the +income-related allowance where the claimant, or the other member of the +couple, has an entitlement to State Pension Credit (Sch 1 para 6(1)(c) and +(d)). The model's Pension Credit reads the couple's award, so the model reads +the Pension Credit the couple would get with no income-related ESA in the +family, B(0), and refuses the couple's claim where B(0) exceeds nil. + +Each example builds a second, separate simulation of the same people with no +income-related ESA reported. Its pension_credit gives B(0) without a branch. +Against it, for every family drawn here: + +- where B(0) is nil, whether the couple would claim Pension Credit changes + neither the couple's income-related ESA (claimant_or_partner_esa_income) + nor the family's (esa_income); +- where B(0) exceeds nil, the couple would claim it and has an open report, + the couple's income-related ESA is nil and the family's Pension Credit is + B(0); +- claiming Pension Credit never raises income-related ESA; +- esa_income_eligible equals the family-by-family reading of the law in + legacy_award_work_reference (esa_screen), given the model's pensionable age + (is_SP_age) and B(0); +- nobody who has reached pensionable age is on income-related ESA + (is_on_income_related_esa), and a family whose every report comes from such + a member gets none; +- no family has both Pension Credit and the couple's income-related ESA. + +DWP disregards £20 a week of earnings while the claimant or partner +receives ESA (SPC Regs 2002 Sch VI para 4(1)(a)(vii)), so some families get +Pension Credit only with the award. The model pays such a family the award +and no Pension Credit (pension_credit), and the test counts them. + +The years are 2016, when a man born before 6 December 1953 could be over the +qualifying age for State Pension Credit but under his pensionable age of 65 +(Pensions Act 1995 Sch 4 para 1 rule 1), 2018, before the exclusion of +mixed-age couples (SPCA 2002 s.4(1A), from 15 May 2019), and 2026, when only +a mixed-age couple that keeps the SI 2019/37 art 4 saving +(has_mixed_age_couple_pension_credit_saving, drawn) can get Pension Credit. +The Secretary of State appointed 1 July 2026 for the abolition of +income-related ESA to reach every remaining award (SI 2025/1148 art. 3A(1)), +and under the claimant's appointee saving DWP keeps the award in payment +(art. 3A(2)), so every adult drawn in 2026 has the saving +(esa_income_appointee_saving) and each award stays payable for the whole year; +test_esa_income_abolition_properties.py tests unsaved awards. Adults have +earnings, pensions and household savings, so some families pass Pension +Credit's means test and others fail it, and in 2016 and 2018 some report +working tax credit, which HMRC pays without its income test to a family on +the award (TCA 2002 s.7(2)). A member outside the couple claims in their own +right and keeps that claim while below pensionable age, whatever the couple's +Pension Credit. + +Each of would_claim_uc, claims_all_entitled_benefits and the mixed-age saving +would otherwise read the ESA report, so the generator enters them, and no one +reports Housing Benefit, whose saving route also reads it. The test asserts +that the two simulations agree on the Pension Credit age conditions. Roles are +given explicitly (is_claimant_or_partner). Each example builds many families +in one simulation, in separate benefit units, with the take-up copies in the +same simulation. Simulations take most of the time here, so each example +builds two and the file builds few. + +A differential test runs the drawn families through a reform that calculates +the model's Pension Credit by a formula of its own, so the screen reads +Pension Credit for every couple whose claim passes the other tests, and +requires the same results as the model, which reads it only for a couple +that meets the age conditions and would claim. + +Separate tests count the branches the model opens and the variables the +screen reads: a population with no couple that could be on Pension Credit +opens none, a branch never outlives the calculation, the tax credit passport +branch inside it reads the couple's award as nil, and the screen reads no +Pension Credit condition without a couple's claim that passes the other +tests. Others enter Pension Credit, or a part of it, as an input, or reform +it: an entered award, a reform that pays every family, a frozen award, a +reform that changes the default or coverage of a Pension Credit variable and +a reform that gives take-up a formula each make the model read the award for +every couple, and a parameter reform keeps the age and take-up conditions. +One test changes each attribute of each Pension Credit variable in turn, and +requires the model to drop the age and take-up conditions for every change to +what Core calculates the variable from. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, event, example, given, settings +from hypothesis import strategies as st +from policyengine_core.periods import period as period_ +from policyengine_core.reforms import Reform +from policyengine_core.simulations import Simulation as CoreSimulation + +from policyengine_uk import Simulation +from policyengine_uk.model_api import GBP, YEAR, BenUnit, Variable +from policyengine_uk.tests.legacy_award_work_reference import esa_screen +from policyengine_uk.variables.gov.dwp.esa_income_eligible import ( + NO_INCOME_RELATED_ESA_BRANCH, + PENSION_CREDIT_VARIABLES, + pension_credit_follows_model_formulas, +) +from policyengine_uk.variables.gov.dwp.pension_credit.guarantee_credit.guarantee_credit import ( + guarantee_credit as model_guarantee_credit, +) +from policyengine_uk.variables.gov.dwp.pension_credit.is_pension_credit_eligible import ( + is_pension_credit_eligible as model_eligibility, +) +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit import ( + pension_credit as model_pension_credit, +) + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEARS = [2016, 2018, 2026] +# Either side of the State Pension Credit qualifying age and of pensionable +# age in each year drawn, including men aged 62 to 64 in 2016. +AGES = [30, 45, 58, 60, 62, 63, 64, 65, 66, 67, 68, 70, 75, 82] +# 0, 10, 15 and 24 hours a week. +HOURS = [0, 0, 520, 780, 1_248] +# £50, £100 and about £192 a week. +EARNINGS = [0, 0, 2_600, 5_200, 10_000] +# £40, £61 and £200 a week. +PENSIONS = [0, 0, 2_080, 3_172, 10_400] +SAVINGS = [0, 0, 8_000, 14_000, 40_000] + + +@st.composite +def adults(draw, outside=False, tax_credits=False, saved=False): + adult = { + "age": draw(st.sampled_from(AGES)), + "months_since_last_birthday": draw(st.sampled_from([0, 3, 8, 11])), + "gender": draw(st.sampled_from(["MALE", "FEMALE"])), + "esa_income_reported": draw(st.sampled_from([0, 2_500, 5_000])), + "esa_includes_support_component": draw(st.booleans()), + "hours_worked": draw(st.sampled_from(HOURS)), + "employment_income": draw(st.sampled_from(EARNINGS)), + "private_pension_income": draw(st.sampled_from(PENSIONS)), + } + if saved: + adult["esa_income_appointee_saving"] = True + if outside: + adult["current_education"] = "NOT_IN_EDUCATION" + elif tax_credits: + adult["working_tax_credit_reported"] = draw(st.sampled_from([0, 0, 0, 1])) + return adult + + +@st.composite +def families(draw, tax_credits, saved): + """A claimant, an optional partner, an optional member outside the couple, + the couple's saving and the household's savings. Members of the couple + report working tax credit only in a year with tax credits. With saved, + every adult has the appointee saving for income-related ESA.""" + couple = [draw(adults(tax_credits=tax_credits, saved=saved))] + if draw(st.booleans()): + couple.append(draw(adults(tax_credits=tax_credits, saved=saved))) + others = ( + [draw(adults(outside=True, saved=saved))] + if draw(st.integers(0, 3)) == 0 + else [] + ) + saving = draw(st.booleans()) + return couple, others, saving, draw(st.sampled_from(SAVINGS)) + + +@st.composite +def examples(draw): + year = draw(st.sampled_from(YEARS)) + # Tax credits ended on 5 April 2025. + tax_credits = year < 2025 + # The abolition of income-related ESA reaches unsaved awards from 1 July + # 2026 (SI 2025/1148 art. 3A(1)), so every adult drawn in 2026 has the + # appointee saving, under which DWP keeps the award payable all year + # (art. 3A(2)). + saved = year >= 2026 + units = draw(st.lists(families(tax_credits, saved), min_size=4, max_size=12)) + # In 2016, add case 18's man and case 26's couple, who get Pension Credit + # only with the award. + if year == 2016: + units = units + [ROWS[2], COUPLE_ROW] + return units, year + + +def situation(units, year, would_claim_pc, reports=True): + """Each family once for each take-up in would_claim_pc. Without reports, + nobody reports income-related ESA.""" + people, benunits, households = {}, {}, {} + copies = [(unit, claims) for claims in would_claim_pc for unit in units] + for i, ((couple, others, saving, savings), claims) in enumerate(copies): + names = [] + members = [(m, True) for m in couple] + [(m, False) for m in others] + for j, (inputs, claimant_or_partner) in enumerate(members): + name = f"p{i}_{j}" + values = dict(inputs, is_claimant_or_partner=claimant_or_partner) + if not reports: + values.pop("esa_income_reported") + people[name] = {k: {year: v} for k, v in values.items()} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "would_claim_uc": {year: False}, + "claims_all_entitled_benefits": {year: False}, + "has_mixed_age_couple_pension_credit_saving": {year: saving}, + "would_claim_pc": {year: claims}, + } + households[f"h{i}"] = {"members": names, "savings": {year: savings}} + return {"people": people, "benunits": benunits, "households": households} + + +def couple_reports(sim, year): + """Whether the couple has an open report of income-related ESA.""" + open_report = (sim.calculate("esa_income_reported", year) > 0) & ~sim.calculate( + "esa_income_report_closed", year + ) + couple = sim.calculate("is_claimant_or_partner", year) + return sim.populations["benunit"].sum(open_report & couple) > 0 + + +# Each pairing of B(0) and B(e) in 2016, for a man aged 63 and six months, whom +# the model takes to be born on 6 April 1953, with the earnings, pensions and +# awards of esa_income_pension_age.yaml cases 15, 16 and 18; the couple of case +# 19, whose working tax credit decides the pairing; and a member outside the +# couple reporting beside a pensioner on Pension Credit. +MAN = { + "age": 63, + "months_since_last_birthday": 6, + "gender": "MALE", + "esa_includes_support_component": True, + "working_tax_credit_reported": 0, +} +ROWS = [ + ( + [ + dict( + MAN, + esa_income_reported=reported, + employment_income=earnings, + hours_worked=hours, + private_pension_income=pension, + ) + ], + [], + False, + 0, + ) + for reported, earnings, hours, pension in [ + (6_011.20, 2_600, 520, 2_080), + (4_919.20, 6_006, 780, 3_172), + (5_491.20, 6_006, 780, 2_600), + ] +] + [ + ( + [ + { + "age": 66, + "months_since_last_birthday": 0, + "gender": "MALE", + "esa_income_reported": 0, + "esa_includes_support_component": False, + "hours_worked": 1_040, + "employment_income": 10_000, + "private_pension_income": 0, + "working_tax_credit_reported": 1, + }, + { + "age": 58, + "months_since_last_birthday": 0, + "gender": "FEMALE", + "esa_income_reported": 3_000, + "esa_includes_support_component": False, + "hours_worked": 0, + "employment_income": 0, + "private_pension_income": 0, + "working_tax_credit_reported": 0, + }, + ], + [], + False, + 0, + ), + ( + [ + { + "age": 70, + "months_since_last_birthday": 0, + "gender": "FEMALE", + "esa_income_reported": 0, + "esa_includes_support_component": False, + "hours_worked": 0, + "employment_income": 0, + "private_pension_income": 0, + "working_tax_credit_reported": 0, + } + ], + [ + { + "age": 40, + "months_since_last_birthday": 0, + "gender": "MALE", + "esa_income_reported": 3_000, + "esa_includes_support_component": False, + "hours_worked": 0, + "employment_income": 0, + "private_pension_income": 0, + "current_education": "NOT_IN_EDUCATION", + } + ], + False, + 0, + ), +] + +# Case 26's couple: the man of case 18 earning £90 a week in 15 hours with a +# £130 weekly pension, and a woman aged 60 earning £30 a week in 5 hours. +COUPLE_ROW = ( + [ + dict( + MAN, + esa_income_reported=5_491.20, + employment_income=4_680, + hours_worked=780, + private_pension_income=6_760, + ), + { + "age": 60, + "months_since_last_birthday": 0, + "gender": "FEMALE", + "esa_income_reported": 0, + "esa_includes_support_component": False, + "hours_worked": 260, + "employment_income": 1_560, + "private_pension_income": 0, + "working_tax_credit_reported": 0, + }, + ], + [], + False, + 0, +) + +# Two couples in 2018, a year with tax credits and before the exclusion of +# mixed-age couples. Case 19's couple (ROWS[3]) gets Pension Credit with or +# without the award that year. In case 20's couple, a man aged 66 has a £200 +# weekly pension and a man aged 58 reports support-group income-related ESA +# and earns £124 a week in 15 hours, and the couple gets no Pension Credit +# with or without the award. +ROWS_2018 = [ + ROWS[3], + ( + [ + { + "age": 66, + "months_since_last_birthday": 0, + "gender": "MALE", + "esa_income_reported": 0, + "esa_includes_support_component": False, + "hours_worked": 0, + "employment_income": 0, + "private_pension_income": 10_400, + "working_tax_credit_reported": 0, + }, + { + "age": 58, + "months_since_last_birthday": 0, + "gender": "MALE", + "esa_income_reported": 2_537.60, + "esa_includes_support_component": True, + "hours_worked": 780, + "employment_income": 6_448, + "private_pension_income": 0, + "working_tax_credit_reported": 0, + }, + ], + [], + False, + 0, + ), +] + + +@settings( + max_examples=4, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(examples()) +@example((ROWS, 2016)) +@example((ROWS_2018, 2018)) +def test_couple_esa_follows_pension_credit_worked_out_without_it(drawn): + units, year = drawn + n = len(units) + event(f"year {year}") + sim = Simulation(situation=situation(units, year, [True, False])) + oracle = Simulation(situation=situation(units, year, [True], reports=False)) + route = sim.calculate("meets_pension_credit_age_conditions", year) + # The oracle's Pension Credit reads the same age conditions. + assert ( + route[:n] == oracle.calculate("meets_pension_credit_age_conditions", year) + ).all() + assert (route[:n] == route[n:]).all() + without_award = oracle.calculate("pension_credit", year) + pension_credit = sim.calculate("pension_credit", year) + couple_esa = sim.calculate("claimant_or_partner_esa_income", year) + esa = sim.calculate("esa_income", year) + reports = couple_reports(sim, year) + claims, declines = slice(0, n), slice(n, 2 * n) + + pays = without_award > 0 + barred = pays & reports[claims] + refused = ~pays + for label, mask in [ + ("Pension Credit pays without the award", barred), + ("Pension Credit refuses a couple with a report", refused & reports[claims]), + ( + "Pension Credit pays only with the award", + refused + & (couple_esa[claims] > 0) + & (sim.calculate("pension_credit_entitlement", year)[claims] > 0), + ), + ]: + if mask.any(): + event(label) + + # Sch 1 para 6(1)(c) and (d): where the family would get Pension Credit + # without the award, the couple's award is nil and the family gets that + # Pension Credit. + assert (couple_esa[claims][barred] == 0).all(), units + assert np.allclose( + pension_credit[claims][barred], without_award[barred], atol=0.01 + ), units + # Where the family would get no Pension Credit without the award, take-up + # changes no award. + assert np.allclose( + couple_esa[claims][refused], couple_esa[declines][refused], atol=0.01 + ), units + assert np.allclose(esa[claims][refused], esa[declines][refused], atol=0.01), units + # Claiming Pension Credit never raises income-related ESA. + assert (esa[claims] <= esa[declines] + 0.01).all(), units + # Sch 1 para 6(1)(c) and (d): no family has both the couple's award and + # Pension Credit, whether or not it would claim. + assert not ((couple_esa > 0) & (pension_credit > 0)).any(), units + if units == ROWS: + # The third row is case 18: B(0) nil and B(e) 525.20, so the man + # keeps his award and gets no Pension Credit. + assert couple_esa[2] == pytest.approx(5_491.20, abs=0.01) + assert sim.calculate("pension_credit_entitlement", year)[2] == pytest.approx( + 525.20, abs=0.01 + ) + assert pension_credit[2] == 0 + # The fifth row is case 12's pairing: the member outside the couple + # keeps their 3,000 beside the couple's minimum guarantee, 52 x + # 155.60 = 8,091.20, whether or not the couple would claim. + assert esa[[4, n + 4]] == pytest.approx([3_000, 3_000], abs=0.01) + assert couple_esa[4] == 0 + assert pension_credit[4] == pytest.approx(8_091.20, abs=0.01) + + # The screen, family by family. + eligible = sim.calculate("esa_income_eligible", year) + pensionable_age = sim.calculate("is_SP_age", year) + on_esa = sim.calculate("is_on_income_related_esa", year) + capital = sim.calculate("esa_income_assessable_capital", year) + parameters = sim.tax_benefit_system.parameters(year) + # s.1(3)(c): the award is not payable to anyone over pensionable age. + assert not (pensionable_age & on_esa).any(), units + start = 0 + for i, (unit, claimed) in enumerate( + [(unit, True) for unit in units] + [(unit, False) for unit in units] + ): + couple, others, _, _ = unit + members = [ + dict(member, is_SP_age=pensionable_age[start + j]) + for j, member in enumerate(couple + others) + ] + on_pension_credit = claimed and route[i % n] and pays[i % n] + expected = esa_screen( + members[: len(couple)], + members[len(couple) :], + capital[i], + parameters, + on_pension_credit=on_pension_credit, + ) + assert eligible[i] == expected, (year, claimed, unit) + reporters = [m["esa_income_reported"] > 0 for m in members] + if any(reporters) and all( + m["is_SP_age"] for m, r in zip(members, reporters) if r + ): + assert esa[i] == 0, (year, unit) + start += len(members) + + +# The appointee saving for income-related ESA, under which DWP keeps the award +# in payment after the abolition of 1 July 2026 (SI 2025/1148 art. 3A(2)). +SAVED = {"esa_income_appointee_saving": True} + + +class BranchLog: + """Records each branch Core creates, with the simulation it branches.""" + + def __init__(self, monkeypatch): + self.created = [] + get_branch = CoreSimulation.get_branch + + def logged(simulation, name="branch", clone_system=False): + new = name not in simulation.branches and name != simulation.branch_name + branch = get_branch(simulation, name, clone_system) + if new: + self.created.append((simulation, branch)) + return branch + + monkeypatch.setattr(CoreSimulation, "get_branch", logged) + + def bar_branches(self): + return [ + (parent, branch) + for parent, branch in self.created + if branch.branch_name.startswith(NO_INCOME_RELATED_ESA_BRANCH) + ] + + +def household(members, year, **benunit): + names = [f"p{j}" for j in range(len(members))] + return { + "people": { + name: {k: {year: v} for k, v in inputs.items()} + for name, inputs in zip(names, members) + }, + "benunits": { + "b": { + "members": names, + "would_claim_uc": {year: False}, + **{k: {year: v} for k, v in benunit.items()}, + } + }, + "households": {"h": {"members": names}}, + } + + +def combine(*situations): + combined = {"people": {}, "benunits": {}, "households": {}} + for i, part in enumerate(situations): + for entity, units in part.items(): + for name, unit in units.items(): + unit = dict(unit) + if "members" in unit: + unit["members"] = [f"{m}_{i}" for m in unit["members"]] + combined[entity][f"{name}_{i}"] = unit + return combined + + +def test_no_branch_opens_without_a_couple_that_could_be_on_pension_credit( + monkeypatch, +): + """In 2026 no couple here has a claim that passes the other tests and + meets the Pension Credit age conditions: a working-age claimant reporting + the award, a couple aged 70 and 60 without the saving (case 4's kind), a + pensioner without a report, a member outside the couple reporting beside + a pensioner, and a pensioner reporting the award, who has reached + pensionable age and so has no claim whatever the couple's Pension + Credit. Each reporter has the appointee saving (SI 2025/1148 art. 3A(2)), + so the model keeps each report open for the whole of 2026-27 + (esa_income_payable_share is 1).""" + log = BranchLog(monkeypatch) + year = 2026 + adult = {"is_claimant_or_partner": True} + reports = {"esa_income_reported": 5_000, **SAVED} + sim = Simulation( + situation=combine( + household([dict(adult, age=45, **reports)], year), + household( + [dict(adult, age=70), dict(adult, age=60, **reports)], + year, + ), + household([dict(adult, age=75)], year), + household( + [ + dict(adult, age=70), + { + "age": 40, + "is_claimant_or_partner": False, + "current_education": "NOT_IN_EDUCATION", + **reports, + }, + ], + year, + ), + household([dict(adult, age=75, **reports)], year), + ) + ) + assert sim.calculate("esa_income", year).tolist() == [5_000, 5_000, 0, 5_000, 0] + assert sim.calculate("meets_pension_credit_age_conditions", year)[-1] + sim.calculate("household_net_income", year) + assert log.bar_branches() == [] + + +def test_the_branch_closes_and_its_passport_branch_reads_no_award(monkeypatch): + """In 2016 the couple of esa_income_pension_age.yaml case 19 meets the + Pension Credit age conditions with an open report, so the model opens the + branch, and its working tax credit opens the passport branch inside it + (pension_credit_with_passported_tax_credits). That branch reads the + award as nil, set on its parent, and no branch outlives the calculation.""" + log = BranchLog(monkeypatch) + year = 2016 + sim = Simulation( + situation=household( + [ + { + "age": 66, + "is_claimant_or_partner": True, + "employment_income": 10_000, + "hours_worked": 1_040, + "working_tax_credit_reported": 1, + }, + { + "age": 58, + "is_claimant_or_partner": True, + "esa_income_reported": 3_000, + }, + ], + year, + ) + ) + assert sim.calculate("esa_income", year).tolist() == [0] + assert sim.calculate("pension_credit", year) == pytest.approx([370.40], abs=0.01) + bar_branches = log.bar_branches() + assert bar_branches + passports = [ + branch + for parent, branch in log.created + if any(parent is bar for _, bar in bar_branches) + and branch.branch_name.startswith("tax_credits_pension_credit_passport") + ] + assert passports + for passport in passports: + for variable in ["esa_income", "claimant_or_partner_esa_income"]: + assert passport.calculate(variable, year).tolist() == [0], variable + for parent, branch in bar_branches: + assert all(other is not branch for other in parent.branches.values()) + assert not [ + name for name in sim.branches if name.startswith(NO_INCOME_RELATED_ESA_BRANCH) + ] + + +# The man of esa_income_pension_age.yaml cases 7 and 8, born 1 March 1953: in +# 2016 he meets the Pension Credit age conditions, under his pensionable age. +# A man aged 40 does not meet them. +MAN_63 = { + "age": 63, + "date_of_birth": 19530301, + "gender": "MALE", + "is_claimant_or_partner": True, + "esa_income_reported": 5_000, +} +MAN_40 = dict(MAN_63, age=40, date_of_birth=19760301) +ADULT = {"is_claimant_or_partner": True} + + +def no_branch_opens_beside(log): + return log.bar_branches() == [] + + +def test_an_entered_pension_credit_award_bars_the_award_whatever_the_age_and_take_up(): + """Three men in 2016 each have £2,000 of Pension Credit as an input: the + man aged 63 who would claim Pension Credit, the same man who would not, + and a man aged 40. Each has an entitlement to that Pension Credit (Sch 1 + para 6(1)(c)), so none gets income-related ESA (esa_income_pension_age.yaml + case 21).""" + year = 2016 + sim = Simulation( + situation=combine( + household([MAN_63], year, would_claim_pc=True, pension_credit=2_000), + household([MAN_63], year, would_claim_pc=False, pension_credit=2_000), + household([MAN_40], year, pension_credit=2_000), + ) + ) + assert sim.calculate("meets_pension_credit_age_conditions", year).tolist() == [ + True, + True, + False, + ] + assert sim.calculate("pension_credit", year).tolist() == [2_000] * 3 + assert sim.calculate("esa_income", year).tolist() == [0] * 3 + assert sim.calculate("claimant_or_partner_esa_income", year).tolist() == [0] * 3 + + +class flat_pension_credit(Reform): + """Pension Credit of £2,000 for every benefit unit.""" + + def apply(self): + class pension_credit(Variable): + label = "Pension Credit" + value_type = float + entity = BenUnit + definition_period = YEAR + unit = GBP + + def formula(benunit, period, parameters): + return benunit.filled_array(2_000) + + self.update_variable(pension_credit) + + +def test_a_reform_that_pays_pension_credit_to_all_bars_the_award(monkeypatch): + """Under a reform that pays every benefit unit £2,000 of Pension Credit, + the man aged 63 who would claim it, the same man who would not and a man + aged 40 each have an entitlement to it, so none gets income-related ESA, + and the model reads that Pension Credit for all three.""" + log = BranchLog(monkeypatch) + year = 2016 + sim = Simulation( + situation=combine( + household([MAN_63], year), + household([MAN_63], year, would_claim_pc=False), + household([MAN_40], year), + ), + reform=flat_pension_credit, + ) + assert sim.calculate("pension_credit", year).tolist() == [2_000] * 3 + assert sim.calculate("esa_income", year).tolist() == [0] * 3 + assert log.bar_branches() + assert not pension_credit_follows_model_formulas( + sim.populations["benunit"], period_(year), sim.tax_benefit_system.parameters + ) + + +class pension_credit_by_another_formula(Reform): + """The model's Pension Credit, calculated by a formula of the reform's.""" + + def apply(self): + class pension_credit(Variable): + label = "Pension Credit" + value_type = float + entity = BenUnit + definition_period = YEAR + unit = GBP + + def formula(benunit, period, parameters): + return model_pension_credit.formula(benunit, period, parameters) + + self.update_variable(pension_credit) + + +OUTPUTS = [ + "esa_income", + "claimant_or_partner_esa_income", + "esa_income_eligible", + "pension_credit", + "working_tax_credit", + "household_net_income", +] + + +@settings( + max_examples=2, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(examples()) +@example((ROWS, 2016)) +@example((ROWS_2018, 2018)) +def test_reading_pension_credit_for_every_couple_matches_the_model(drawn): + """Differential: a reform that calculates the model's Pension Credit by a + formula of its own makes the screen read Pension Credit without the award + for every couple whose claim passes the other tests. It must give every + family the same ESA, Pension Credit and income as the model, which reads it + only for a couple that meets the age conditions and would claim.""" + units, year = drawn + event(f"year {year}") + people = situation(units, year, [True, False]) + model = Simulation(situation=people) + reformed = Simulation(situation=people, reform=pension_credit_by_another_formula) + parameters = model.tax_benefit_system.parameters + assert pension_credit_follows_model_formulas( + model.populations["benunit"], period_(year), parameters + ) + assert not pension_credit_follows_model_formulas( + reformed.populations["benunit"], period_(year), parameters + ) + for variable in OUTPUTS: + assert np.allclose( + model.calculate(variable, year), + reformed.calculate(variable, year), + atol=0.01, + ), (variable, units) + + +def test_a_parameter_reform_keeps_the_age_and_take_up_conditions(monkeypatch): + """A reform that lifts the exclusion of mixed-age couples in 2026 keeps + the model's formulas, and meets_pension_credit_age_conditions reads its + parameters. A couple aged 70 and 60 without the saving then meets the + age conditions, so the model reads its Pension Credit and refuses the + younger member's award. With only a working-age claimant, no couple meets + them, and the model opens no branch, under the reform or without it. Each + reporter has the appointee saving (SI 2025/1148 art. 3A(2)), so the model + keeps each report open for the whole of 2026-27 (esa_income_payable_share + is 1).""" + log = BranchLog(monkeypatch) + year = 2026 + reform = { + "gov.dwp.pension_credit.mixed_age_couples.excluded": { + f"{year}-01-01.{year}-12-31": False + } + } + working_age = household( + [dict(ADULT, age=45, esa_income_reported=5_000, **SAVED)], year + ) + mixed = household( + [dict(ADULT, age=70), dict(ADULT, age=60, esa_income_reported=5_000, **SAVED)], + year, + has_mixed_age_couple_pension_credit_saving=False, + ) + + alone = Simulation(situation=combine(working_age), reform=reform) + assert pension_credit_follows_model_formulas( + alone.populations["benunit"], period_(year), alone.tax_benefit_system.parameters + ) + assert alone.calculate("esa_income", year).tolist() == [5_000] + assert no_branch_opens_beside(log) + + baseline = Simulation(situation=combine(working_age, mixed)) + assert baseline.calculate("esa_income", year).tolist() == [5_000, 5_000] + assert baseline.calculate("pension_credit", year).tolist() == [0, 0] + assert no_branch_opens_beside(log) + + lifted = Simulation(situation=combine(working_age, mixed), reform=reform) + assert follows_model_formulas(lifted, year) + assert lifted.calculate("meets_pension_credit_age_conditions", year).tolist() == [ + False, + True, + ] + # The couple's minimum guarantee for 52 weeks in 2026-27: 52 x 363.25. + assert lifted.calculate("pension_credit", year) == pytest.approx( + [0, 18_889], abs=0.01 + ) + assert lifted.calculate("esa_income", year).tolist() == [5_000, 0] + assert log.bar_branches() + + +def test_a_frozen_pension_credit_bars_the_award_whatever_the_reform_age_conditions(): + """In 2018 a couple aged 70 and 60 meets the Pension Credit age + conditions, so gets Pension Credit and no income-related ESA. A reform + that brings the exclusion of mixed-age couples forward to 2018 and freezes + Pension Credit at the baseline's award (gov.contrib.freeze_pension_credit) + still pays that award, though the couple no longer meets the reform's age + conditions, so the couple still gets no income-related ESA.""" + year = 2018 + reform = { + "gov.dwp.pension_credit.mixed_age_couples.excluded": { + f"{year}-01-01.{year}-12-31": True + }, + "gov.contrib.freeze_pension_credit": {f"{year}-01-01.{year}-12-31": True}, + } + sim = Simulation( + situation=household( + [dict(ADULT, age=70), dict(ADULT, age=60, esa_income_reported=5_000)], + year, + has_mixed_age_couple_pension_credit_saving=False, + ), + reform=reform, + ) + assert not sim.calculate("meets_pension_credit_age_conditions", year)[0] + assert sim.baseline.calculate("esa_income", year).tolist() == [0] + # The couple's minimum guarantee for 52 weeks in 2018-19: 52 x 248.80. + frozen = sim.baseline.calculate("pension_credit", year) + assert frozen == pytest.approx([12_937.60], abs=0.01) + assert sim.calculate("pension_credit", year) == pytest.approx(frozen, abs=0.01) + assert not pension_credit_follows_model_formulas( + sim.populations["benunit"], period_(year), sim.tax_benefit_system.parameters + ) + assert sim.calculate("esa_income", year).tolist() == [0] + + +def test_each_entered_part_of_pension_credit_drops_the_age_and_take_up_conditions(): + """An input for pension_credit, its entitlement, its eligibility or either + credit can pay Pension Credit outside the age and take-up conditions, so + each makes the model read Pension Credit for every couple.""" + year = 2016 + sim = Simulation(situation=household([MAN_40], year)) + parameters = sim.tax_benefit_system.parameters + assert pension_credit_follows_model_formulas( + sim.populations["benunit"], period_(year), parameters + ) + for variable, value in [ + ("pension_credit", 2_000), + ("pension_credit_entitlement", 2_000), + ("is_pension_credit_eligible", True), + ("guarantee_credit", 2_000), + ("savings_credit", 2_000), + ]: + branch = sim.get_branch(f"entered_{variable}") + try: + branch.set_input(variable, year, np.array([value])) + assert not pension_credit_follows_model_formulas( + branch.populations["benunit"], period_(year), parameters + ), variable + # An input for another year leaves this year to the formulas. + assert pension_credit_follows_model_formulas( + branch.populations["benunit"], period_(year + 1), parameters + ), variable + finally: + sim.branches.pop(f"entered_{variable}", None) + assert pension_credit_follows_model_formulas( + sim.populations["benunit"], period_(year), parameters + ) + + +def follows_model_formulas(sim, year): + return pension_credit_follows_model_formulas( + sim.populations["benunit"], period_(year), sim.tax_benefit_system.parameters + ) + + +class entitlement_with_a_default(Reform): + """The model's Pension Credit entitlement, which gives £2,000 to a family + outside its defined_for.""" + + def apply(self): + class pension_credit_entitlement(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR + default_value = 2_000 + defined_for = "is_pension_credit_eligible" + adds = ["guarantee_credit", "savings_credit"] + + self.update_variable(pension_credit_entitlement) + + +class payment_with_a_default(Reform): + """The model's Pension Credit formula for a family eligible for Pension + Credit, and £2,000 for any other.""" + + def apply(self): + class pension_credit(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR + default_value = 2_000 + defined_for = "is_pension_credit_eligible" + formula = model_pension_credit.formula + + self.update_variable(pension_credit) + + +class eligibility_with_a_default(Reform): + """The model's eligibility formula for a family that meets the age + conditions, and eligibility for any other.""" + + def apply(self): + class is_pension_credit_eligible(Variable): + value_type = bool + entity = BenUnit + definition_period = YEAR + default_value = True + defined_for = "meets_pension_credit_age_conditions" + formula = model_eligibility.formula + + self.update_variable(is_pension_credit_eligible) + + +class guarantee_credit_for_every_family_it_tests(Reform): + """The model's guarantee credit and entitlement for every family with + income within the minimum guarantee (is_guarantee_credit_eligible), not + only one eligible for Pension Credit. Only their coverage changes.""" + + def apply(self): + class guarantee_credit(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR + defined_for = "is_guarantee_credit_eligible" + formula = model_guarantee_credit.formula + + class pension_credit_entitlement(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR + defined_for = "is_guarantee_credit_eligible" + adds = ["guarantee_credit", "savings_credit"] + + self.update_variable(guarantee_credit) + self.update_variable(pension_credit_entitlement) + + +class model_pension_credit_registered_again(Reform): + """The model's own five Pension Credit variables, registered again.""" + + def apply(self): + for variable in PENSION_CREDIT_VARIABLES: + self.update_variable(variable) + + +@pytest.mark.parametrize( + "reform, follows, pension_credit_at_40, esa", + [ + (entitlement_with_a_default, False, 2_000, [5_000, 0, 0]), + ( + (entitlement_with_a_default, pension_credit_by_another_formula), + False, + 2_000, + [5_000, 0, 0], + ), + (payment_with_a_default, False, 2_000, [5_000, 0, 0]), + (eligibility_with_a_default, False, 8_091.20, [5_000, 0, 0]), + (guarantee_credit_for_every_family_it_tests, False, 8_091.20, [5_000, 0, 0]), + (model_pension_credit_registered_again, True, 0, [5_000, 5_000, 0]), + ], + ids=[ + "entitlement_default", + "entitlement_default_another_formula", + "payment_default", + "eligibility_default", + "guarantee_credit_coverage", + "model_definitions_again", + ], +) +def test_a_reform_that_pays_outside_the_age_conditions_bars_the_award( + reform, follows, pension_credit_at_40, esa +): + """In 2016 the man aged 63 who would not claim Pension Credit gets none + and keeps his £5,000 award, and the man aged 63 who would claim gets the + £8,091.20 minimum guarantee (52 x £155.60) and no award. The man aged 40 + does not meet the age conditions. Under each reform but the last, the + model pays him £2,000 of Pension Credit or the minimum guarantee outside + the definition's defined_for, so he has an entitlement to it (Sch 1 para + 6(1)(c)) and gets no award. The model's own definitions, registered + again, keep the shortcut, and he keeps his £5,000.""" + year = 2016 + sim = Simulation( + situation=combine( + household([MAN_63], year, would_claim_pc=False), + household([MAN_40], year), + household([MAN_63], year), + ), + reform=reform, + ) + assert sim.calculate("esa_income", year).tolist() == esa + assert sim.calculate("pension_credit", year) == pytest.approx( + [0, pension_credit_at_40, 8_091.20], abs=0.01 + ) + assert sim.calculate("meets_pension_credit_age_conditions", year).tolist() == [ + True, + False, + True, + ] + assert follows_model_formulas(sim, year) is follows + + +class take_up_without_income_related_esa(Reform): + """A family claims Pension Credit while it gets no income-related ESA.""" + + def apply(self): + class would_claim_pc(Variable): + value_type = bool + entity = BenUnit + definition_period = YEAR + + def formula(benunit, period, parameters): + return benunit("esa_income", period) == 0 + + self.update_variable(would_claim_pc) + + +@pytest.mark.parametrize( + "reform", + [ + take_up_without_income_related_esa, + (take_up_without_income_related_esa, pension_credit_by_another_formula), + ], + ids=["take_up_formula", "take_up_formula_another_formula"], +) +def test_a_take_up_formula_makes_the_model_read_pension_credit_for_every_couple( + reform, +): + """Take-up that reads the family's ESA would close a cycle if the screen + read take-up in the simulation itself, so the model reads Pension Credit + without the award for every couple. Without the award both men aged 63 + in 2016 would claim and get the £8,091.20 minimum guarantee, so neither + gets the award, and both then claim. The man aged 40 gets no Pension + Credit, so he keeps his £5,000 and does not claim.""" + year = 2016 + sim = Simulation( + situation=combine( + household([MAN_63], year), + household([MAN_40], year), + household([MAN_63], year), + ), + reform=reform, + ) + assert sim.calculate("esa_income", year).tolist() == [0, 5_000, 0] + assert sim.calculate("pension_credit", year) == pytest.approx( + [8_091.20, 0, 8_091.20], abs=0.01 + ) + assert sim.calculate("would_claim_pc", year).tolist() == [True, False, True] + assert not follows_model_formulas(sim, year) + + +def wrapped(formula): + def wrapper(entity, period, parameters): + return formula(entity, period, parameters) + + return wrapper + + +def changes(variable): + """For each attribute a reform can change, the attribute, a new value and + whether the change can alter what Core calculates.""" + formula = wrapped(variable.get_formula() or model_pension_credit.formula) + default = True if variable.value_type is bool else 2_000.0 + return [ + ("default_value", default, True), + ("defined_for", "is_guarantee_credit_eligible", True), + ("is_neutralized", True, True), + ("formulas", {"0001-01-01": formula}, True), + ("adds", ["guarantee_credit"], True), + ("definition_period", "month", True), + ("end", period_(2010).start.date, True), + ("label", "Changed", False), + ("documentation", "Changed.", False), + ("reference", ("https://www.legislation.gov.uk/ukpga/2002/16",), False), + ("unit", "currency-USD", False), + ("metadata", {"changed": True}, False), + ] + + +# The five Pension Credit variables, named apart from the model's own list. +PENSION_CREDIT_NAMES = [ + "pension_credit", + "pension_credit_entitlement", + "is_pension_credit_eligible", + "guarantee_credit", + "savings_credit", +] + + +def test_every_change_to_a_pension_credit_definition_drops_the_shortcut(): + """Exhaustive over the five Pension Credit variables and the attributes a + reform can change: any change to what Core calculates a variable from + drops the age and take-up conditions, and a change to its description + alone keeps them. A take-up formula drops them too, and a take-up input or + default keeps them, since the screen reads take-up itself.""" + year = 2016 + sim = Simulation(situation=household([MAN_40], year)) + system = sim.tax_benefit_system + assert follows_model_formulas(sim, year) + for name in PENSION_CREDIT_NAMES: + variable = system.variables[name] + for attribute, value, calculates in changes(variable): + changed = variable.clone() + if attribute == "formulas": + value = type(changed.formulas)(value) + setattr(changed, attribute, value) + system.variables[name] = changed + try: + assert follows_model_formulas(sim, year) is not calculates, ( + name, + attribute, + ) + finally: + system.variables[name] = variable + take_up = system.variables["would_claim_pc"] + for attribute, value, follows in [ + ("formulas", {"0001-01-01": lambda benunit, period, parameters: True}, False), + ("default_value", False, True), + ]: + changed = take_up.clone() + if attribute == "formulas": + value = type(changed.formulas)(value) + setattr(changed, attribute, value) + system.variables["would_claim_pc"] = changed + try: + assert follows_model_formulas(sim, year) is follows, attribute + finally: + system.variables["would_claim_pc"] = take_up + branch = sim.get_branch("entered_take_up") + try: + branch.set_input("would_claim_pc", year, np.array([False])) + assert pension_credit_follows_model_formulas( + branch.populations["benunit"], period_(year), system.parameters + ) + finally: + sim.branches.pop("entered_take_up", None) + assert follows_model_formulas(sim, year) + + +class ScreenReads: + """Records each variable the income-related ESA screen reads, with the + branch and year it reads in. A branch can change the family: the one that + works out Pension Credit without employment income gives a claimant in + paid work no earnings, so their claim can pass the work test there. So + can a later year: the model also runs the screen for 2027 to 2032 here, + and the situation enters earnings and hours for 2026 only.""" + + def __init__(self, monkeypatch): + self.reads = [] + calculate = CoreSimulation.calculate + + def logged(simulation, variable_name, *args, **kwargs): + stack = simulation.tracer.stack + if stack and stack[-1]["name"] == "esa_income_eligible": + frame = stack[-1] + self.reads.append( + (simulation.branch_name, str(frame["period"]), variable_name) + ) + return calculate(simulation, variable_name, *args, **kwargs) + + monkeypatch.setattr(CoreSimulation, "calculate", logged) + + def variables(self, year): + """What the screen read in the simulation itself, for year.""" + return { + variable + for branch, period, variable in self.reads + if branch == "default" and period == str(year) + } + + +PENSION_CREDIT_CONDITIONS = {"meets_pension_credit_age_conditions", "would_claim_pc"} + + +def test_the_screen_reads_no_pension_credit_condition_without_a_couple_claim( + monkeypatch, +): + """In 2026 no couple here has a claim that passes the age, work and + capital tests: a pensioner reporting the award, a claimant aged 45 in + remunerative work, and a member outside the couple aged 40 reporting + beside a pensioner, whose own claim passes. The screen then reads neither + the Pension Credit age conditions nor the claim. A claimant aged 45 out of + work passes, and the screen reads both. Each reporter has the appointee + saving (SI 2025/1148 art. 3A(2)), so the model keeps each report open for + the whole of 2026-27 (esa_income_payable_share is 1).""" + reads = ScreenReads(monkeypatch) + year = 2026 + no_couple_claim = [ + household([dict(ADULT, age=75, esa_income_reported=5_000, **SAVED)], year), + household( + [ + dict( + ADULT, + age=45, + esa_income_reported=5_000, + employment_income=15_000, + hours_worked=1_560, + **SAVED, + ) + ], + year, + ), + household( + [ + dict(ADULT, age=70), + { + "age": 40, + "is_claimant_or_partner": False, + "current_education": "NOT_IN_EDUCATION", + "esa_income_reported": 5_000, + **SAVED, + }, + ], + year, + ), + ] + sim = Simulation(situation=combine(*no_couple_claim)) + assert sim.calculate("esa_income_eligible", year).tolist() == [False, False, True] + assert "esa_income_assessable_capital" in reads.variables(year) + assert not PENSION_CREDIT_CONDITIONS & reads.variables(year), reads.reads + + reads.reads.clear() + out_of_work = household( + [dict(ADULT, age=45, esa_income_reported=5_000, **SAVED)], year + ) + sim = Simulation(situation=combine(*no_couple_claim, out_of_work)) + assert sim.calculate("esa_income_eligible", year).tolist() == [ + False, + False, + True, + True, + ] + assert PENSION_CREDIT_CONDITIONS <= reads.variables(year) diff --git a/policyengine_uk/tests/test_income_support_eligibility_properties.py b/policyengine_uk/tests/test_income_support_eligibility_properties.py index cb3319414a..9556084c54 100644 --- a/policyengine_uk/tests/test_income_support_eligibility_properties.py +++ b/policyengine_uk/tests/test_income_support_eligibility_properties.py @@ -58,7 +58,12 @@ model's reported or proxy inputs; it reads the qualifying age (has_attained_state_pension_credit_qualifying_age) and the model's proxy for being on Pension Credit (meets_pension_credit_age_conditions and -would_claim_pc) from the model; and the means test is out of scope. No +would_claim_pc) from the model, and, for the income-related ESA screen, +pensionable age (is_SP_age; WRA 2007 s.1(3)(c)) and whether the model pays +the couple Pension Credit (Sch 1 para 6(1)(c) and (d)): no one drawn here has +earnings, and in 2025 no one has tax credits, so the model's pension_credit is the same +with or without the couple's income-related ESA; and the means test is out of +scope. No claimant or partner drawn here reports Pension Credit or Housing Benefit (only some added adults do), so in 2025 no family with a claimant or partner under the qualifying age meets the Pension Credit age conditions. The @@ -396,8 +401,14 @@ def reference_eligibility( attained_qualifying_age, on_pension_credit, parameters, + entitled_to_pension_credit=False, ): - """The model's Income Support gate, read family by family.""" + """The model's Income Support gate, read family by family. + + on_pension_credit is the model's proxy for s.124(1)(g); + entitled_to_pension_credit records whether the model pays the couple + Pension Credit, which the income-related ESA screen reads (WRA 2007 Sch 1 + para 6(1)(c) and (d)).""" IS = parameters.gov.dwp.income_support WORK = IS.eligibility.remunerative_work JSA = parameters.gov.dwp.JSA.income @@ -448,7 +459,13 @@ def is_claimant(i): sum(a["esa_income_reported"] for a in adults), capital, parameters.gov.dwp.ESA.income.capital, - ) and esa_screen(adults, [], capital, parameters) + ) and esa_screen( + adults, + [], + capital, + parameters, + on_pension_credit=entitled_to_pension_credit, + ) if jsa_income is not None: # Within half a penny of zero is no award. income_based_jsa = jsa_income > 0.005 @@ -511,9 +528,23 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): on_pension_credit = sim.calculate( "meets_pension_credit_age_conditions", YEAR ) & sim.calculate("would_claim_pc", YEAR) + # Entitlement for the ESA screen: the couple would get Pension Credit. + # Without earnings or tax credits, DWP would pay the same Pension Credit + # with the couple's award as without. + entitled = on_pension_credit & (sim.calculate("pension_credit", YEAR) > 0) + pensionable_age = sim.calculate("is_SP_age", YEAR) parameters = sim.tax_benefit_system.parameters(YEAR) start = 0 for i, (adults, dependants, capital, extra) in enumerate(units): + # The ESA screen reads each member's pensionable age from the model. + adults = [ + dict(adult, is_SP_age=pensionable_age[start + j]) + for j, adult in enumerate(adults) + ] + if extra is not None: + extra = dict( + extra, is_SP_age=pensionable_age[start + len(adults) + len(dependants)] + ) # An esa_income or jsa_income the reported amounts explain is read # through them, like a calculated one. entered_esa = entered_jsa = None @@ -529,7 +560,13 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): reported_total, capital, parameters, - screen=esa_screen(adults, others, capital, parameters), + screen=esa_screen( + adults, + others, + capital, + parameters, + on_pension_credit=entitled[i], + ), ): entered_esa = esa_income[i] if jsa_income is not None: @@ -554,6 +591,7 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): attained_qualifying_age[start : start + len(adults)], on_pension_credit[i], parameters, + entitled_to_pension_credit=entitled[i], ) assert eligible[i] == expected, units[i] # The docstring's claim: no family drawn here with a claimant or 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 ff2edaeab4..05f1fd1794 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 @@ -62,6 +62,9 @@ def reference_report_awards(benunit, period, award): "pension_credit_reported", "esa_income_reported", "jsa_income_reported", + # The ESA reference reads the model's pensionable age (WRA 2007 + # s.1(3)(c)). + "is_SP_age", ] arrays = {name: person(name, period) for name in names} roles = person("is_claimant_or_partner", period) @@ -76,6 +79,29 @@ def reference_report_awards(benunit, period, award): is_esa = award == "esa_income" rules = parameters.gov.dwp.ESA.income if is_esa else parameters.gov.dwp.JSA.income screen = reference.esa_screen if is_esa else reference.jsa_screen + if is_esa: + # Sch 1 para 6(1)(c) and (d): the couple would claim State Pension + # Credit and DWP would pay it. No one here has earnings, and in 2025 no + # one has tax credits, so DWP would pay the same Pension Credit with + # the couple's award as without. An outside member keeps their own + # claim whatever the couple's Pension Credit. + on_pension_credit = ( + benunit("meets_pension_credit_age_conditions", period) + & benunit("would_claim_pc", period) + & (benunit("pension_credit", period) > 0) + ) + + def passes(members, unit, couple): + if not is_esa: + return screen(members, [], capitals[unit], parameters) + return screen( + members, + [], + capitals[unit], + parameters, + on_pension_credit=couple and on_pension_credit[unit], + ) + full, scoped = [], [] for unit in range(benunit.count): ids = np.flatnonzero(benunit.members_entity_id == unit) @@ -92,13 +118,13 @@ def reference_report_awards(benunit, period, award): active = is_esa or parameters.gov.dwp.JSA.income.active couple_amount = ( max(0, sum(member[f"{award}_reported"] for member in couple) - tariff) - if active and screen(couple, [], capital, parameters) + if active and passes(couple, unit, couple=True) else 0 ) outside_amount = sum( max(0, member[f"{award}_reported"] - tariff) for member in others - if active and screen([member], [], capital, parameters) + if active and passes([member], unit, couple=False) ) full.append(couple_amount + outside_amount) scoped.append(couple_amount) diff --git a/policyengine_uk/utils/supplied_inputs.py b/policyengine_uk/utils/supplied_inputs.py index 29699fe2ac..743457b074 100644 --- a/policyengine_uk/utils/supplied_inputs.py +++ b/policyengine_uk/utils/supplied_inputs.py @@ -45,6 +45,7 @@ ``set_model_value`` stored, which ``is_model_value`` tells apart. """ +from functools import cache from typing import List, Optional, Tuple import numpy as np @@ -77,6 +78,7 @@ "extended_childcare_entitlement_term_share", "extended_childcare_entitlement_total_per_child", "gas_consumption_kwh", + "guarantee_credit", "housing_benefit_applicable_income", "housing_benefit_applicable_income_childcare_element", "housing_benefit_applicable_income_disregard", @@ -85,7 +87,10 @@ "housing_benefit_specified_or_temporary_accommodation_disregard", "housing_benefit_wtc_disregard", "in_HE", + "is_pension_credit_eligible", "other_local_bus_trips", + "pension_credit", + "pension_credit_entitlement", "pip_dl_category", "pip_dl_points", "pip_m_category", @@ -97,6 +102,7 @@ "ni_class_4_losses_brought_forward", "ni_class_4_trading_loss", "ni_epg_full_rate_applies", + "savings_credit", "tenure_type", "targeted_childcare_entitlement", "trading_allowance_deduction", @@ -219,6 +225,65 @@ def stored_as_input(population, variable_name: str, period: Period) -> bool: ) +# Variable attributes that describe a variable without changing what Core +# calculates for it. +DESCRIPTIVE_ATTRIBUTES = frozenset( + { + "_explicit_attribute_names", + "baseline_variable", + "category", + "cerfa_field", + "documentation", + "hidden_input", + "index_in_module", + "label", + "metadata", + "module_name", + "reference", + "unit", + } +) + + +def calculation_definition(variable, period: Period) -> dict: + """Every attribute Core calculates ``variable`` from for ``period``: all + but the descriptive ones, with the formula for the period, by its code, + in place of the formulas, and the entity by its key.""" + definition = { + name: value + for name, value in vars(variable).items() + if name not in DESCRIPTIVE_ATTRIBUTES | {"entity", "formulas"} + } + formula = variable.get_formula(period) + definition["formula"] = None if formula is None else formula.__code__ + definition["entity"] = variable.entity.key + return definition + + +@cache +def _model_variable(variable_class): + return variable_class() + + +def replaces_definition(population, variable_class, period: Period) -> bool: + """Whether the simulation can give the variable that ``variable_class`` + defines a value for ``period`` other than the one that definition + calculates. A reform can change anything Core calculates it from: + neutralize it, give it another formula for the period or none, or change + its default, its defined_for, what it sums or its period. A caller can + also set an input for the period (``stored_as_input``), as a scenario + does with its own value (``set_model_value``). A cached formula result is + not an input, even in populations taken from another simulation or in + their clones and branches.""" + name = variable_class.__name__ + variable = population.simulation.tax_benefit_system.get_variable(name) + if calculation_definition(variable, period) != calculation_definition( + _model_variable(variable_class), period + ): + return True + return stored_as_input(population, name, period) + + def supplied_input( population, variable_name: str, period: Period ) -> Optional[np.ndarray]: diff --git a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py index 85339ff44e..e49049f393 100644 --- a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py +++ b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py @@ -27,7 +27,8 @@ def is_payee_of_couple_award(person, period, reported, can_claim=None, couple=No can_claim, if given, limits the payee to the members of the couple who can be the claimant (for Pension Credit, those who have attained the - qualifying age). Where neither can, both are considered, so a positive + qualifying age; for income-related ESA, those who have not reached + pensionable age). Where neither can, both are considered, so a positive award entered for the couple is still paid to one of them. couple, if given (a person-level mask), replaces the claimant and @@ -51,15 +52,18 @@ def is_payee_of_couple_award(person, period, reported, can_claim=None, couple=No return where(person.benunit.any(reports), reports, claimant) -def own_report_is_paid(person, period, award, works, capital_limit): +def own_report_is_paid(person, period, award, barred, capital_limit): """Whether income-related ESA or income-based JSA (award: "esa_income" or "jsa_income") is paid on this person's own report, for a member who is neither the claimant nor the partner and so claims in their own right. - Their claim is screened on them alone. They must not be engaged in - remunerative work as its claimant (works), and the award on their report - alone must be positive: the report exceeds the tariff income from the - benefit unit's capital, within the capital limit (capital_limit). Their + The model screens their claim on them alone. They must meet each + condition of their own (barred is true where they fail one): they must + not engage in remunerative work as its claimant, and, for income-related + ESA, must not have reached pensionable age (Welfare Reform Act 2007 + s.1(3)(c)). The award on their report alone + must be positive: the report exceeds the tariff income from the benefit + unit's capital, within the capital limit (capital_limit). Their own capital is not observed, so the benefit unit's stands in for it. The benefit unit's screen (esa_income_eligible or jsa_income_eligible) cannot stand in for theirs, because when the claimant or partner reports @@ -90,7 +94,7 @@ def own_report_is_paid(person, period, award, works, capital_limit): """ benunit = person.benunit own_award = ( - ~works + ~barred & (benunit(f"{award}_assessable_capital", period) <= capital_limit) & ( person(f"{award}_reported", period) diff --git a/policyengine_uk/variables/gov/dwp/esa_income.py b/policyengine_uk/variables/gov/dwp/esa_income.py index 4bdb566124..5300a63f2d 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income.py +++ b/policyengine_uk/variables/gov/dwp/esa_income.py @@ -3,8 +3,8 @@ def income_related_esa_award(benunit, period, reported_award): """Income-related ESA paid on a reported award: the award less tariff - income from capital, or nothing if the benefit unit fails the screen in - esa_income_eligible.""" + income from capital, or nothing if the benefit unit fails the age, Pension + Credit, work and capital screen in esa_income_eligible.""" tariff_income = benunit("esa_income_tariff_income", period) eligible = benunit("esa_income_eligible", period) return where(eligible, max_(0, reported_award - tariff_income), 0) @@ -14,7 +14,9 @@ def income_related_esa_award_on_reports(benunit, period, reported, members=None) """Screen open person reports, retaining each original claim's ownership. The claimant and partner share one claim. Other reporters have separate - claims, each using the existing own-work and shared-capital/tariff proxy. + claims, each using the existing own-work and shared-capital/tariff proxy, + and the model pays none once the reporter reaches pensionable age + (Welfare Reform Act 2007 s.1(3)(c)). Selecting members changes only reports, never the original couple or the work/capital screen. This helper must not read payable-person status, which itself depends on the aggregate award. @@ -28,9 +30,10 @@ def income_related_esa_award_on_reports(benunit, period, reported, members=None) benunit, period, benunit.sum(reported * couple) ) works = person("esa_income_claimant_remunerative_work", period) + pensionable_age = person("is_SP_age", period) capital = benunit("esa_income_assessable_capital", period) limit = rules.income.capital.limit - own_claim = ~couple & ~works & benunit.project(capital <= limit) + own_claim = ~couple & ~works & ~pensionable_age & benunit.project(capital <= limit) tariff = benunit.project(benunit("esa_income_tariff_income", period)) own_awards = where(own_claim, max_(0, reported - tariff), 0) # The model pays each claim for the share of the year before its @@ -46,8 +49,9 @@ class esa_income(Variable): entity = BenUnit label = "ESA (income-based)" documentation = ( - "Reported income-related ESA screened through bounded capital and " - "remunerative work tests (esa_income_eligible). Reports whose award " + "Reported income-related ESA screened through bounded pensionable " + "age, Pension Credit, capital and 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, the owner is the claimant or " "partner of a family that claims Universal Credit, or the abolition " diff --git a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py index 22f9c6b4b6..9c00f34dc1 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py @@ -1,38 +1,210 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.supplied_inputs import replaces_definition +from policyengine_uk.variables.gov.dwp.pension_credit.guarantee_credit.guarantee_credit import ( + guarantee_credit, +) +from policyengine_uk.variables.gov.dwp.pension_credit.is_pension_credit_eligible import ( + is_pension_credit_eligible, +) +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit import ( + pension_credit, +) +from policyengine_uk.variables.gov.dwp.pension_credit.pension_credit_entitlement import ( + pension_credit_entitlement, +) +from policyengine_uk.variables.gov.dwp.pension_credit.savings_credit.savings_credit import ( + savings_credit, +) + +# The branch in which the family has no income-related ESA. +NO_INCOME_RELATED_ESA_BRANCH = "esa_income_pension_credit_bar" + +# The Pension Credit variables whose definitions pay Pension Credit only to a +# family that meets the age conditions and would claim. +PENSION_CREDIT_VARIABLES = [ + pension_credit, + pension_credit_entitlement, + is_pension_credit_eligible, + guarantee_credit, + savings_credit, +] + + +def pension_credit_follows_model_formulas(benunit, period, parameters): + """Whether the simulation calculates Pension Credit with the model's own + formulas, which pay it only to a family that meets the Pension Credit age + conditions (meets_pension_credit_age_conditions) and would claim + (would_claim_pc). + + pension_credit equals the entitlement where the family would claim and + the couple has no income-related ESA (claimant_or_partner_esa_income), + and nil otherwise. The entitlement sums the guarantee and savings credits for + a family eligible for Pension Credit (is_pension_credit_eligible), and + gives nil to any other, and only a family that meets the age conditions + is eligible. A parameter reform keeps these definitions, and the age + conditions read its parameters. + + A caller can instead set pension_credit, its entitlement, its eligibility + or either credit as an input for the period. A reform can change anything + Core calculates one of them from (replaces_definition), such as the + default a family outside its defined_for gets, or freeze Pension Credit + at the baseline's award (gov.contrib.freeze_pension_credit) whatever the + reform's age conditions. The model can then pay Pension Credit to a + family that does not meet the age conditions or would not claim. A + reform can also give would_claim_pc a formula, which could read the couple's ESA; the screen + would then close a cycle if it read take-up in the simulation itself. + """ + simulation = benunit.simulation + if ( + parameters(period).gov.contrib.freeze_pension_credit + and simulation.baseline is not None + ): + return False + if any( + replaces_definition(benunit, variable, period) + for variable in PENSION_CREDIT_VARIABLES + ): + return False + take_up = simulation.tax_benefit_system.get_variable("would_claim_pc") + return take_up.get_formula(period) is None + + +def pension_credit_without_income_related_esa(benunit, period): + """The Pension Credit the family would get with no income-related ESA. + + Parliament bars the couple's income-related ESA while the claimant, or + the other member of the couple, has an entitlement to State Pension + Credit (Welfare Reform Act 2007 Sch 1 para 6(1)(c) and (d)), and the + model's Pension Credit reads that award in five ways. pension_credit + itself pays nothing while the couple has the award + (claimant_or_partner_esa_income). pension_credit_earnings_disregard reads + the couple's award for the higher earnings disregard (SPC Regs 2002 Sch + VI para 4(1)(a)(vii)). pension_credit_income counts working tax credit, + and the tax credit income test reads the award (TCA 2002 s.7(2)), as + does Income Support's gate, which that test reads. The route test + (left_pension_route_at_legacy_closure) reads Universal Credit + entitlement, whose work capability test reads esa_income + (has_own_esa_award). A formula that read Pension Credit in the + simulation itself would close a cycle. + + This function calculates Pension Credit in a branch that sets + esa_income, the couple's award and esa_income_eligible to nil, so the + screen never runs there. The branch reads pension_credit, so it counts + the claim (would_claim_pc) and an entered award. Where the branch's + Pension Credit exceeds nil, the screen refuses the couple's award, and + the simulation's own Pension Credit then equals the branch's. Where it + comes to nil, the model pays the award, and pension_credit pays nothing + while the couple has it, even where the family would get Pension Credit + with the award through the higher earnings disregard. With Pension + Credit in payment the award would stop, and with it the disregard of SPC + Regs 2002 Sch VI para 4(1)(a)(vii). DWP keeps disregarding £20 only + where it disregarded £20 of earnings in the award (para 4(2)), and it + disregards all of a claimant's permitted-work earnings up to the limit + (ESA Regs 2008 Sch 7 para 5). For a family whose only earnings are + those, the model's result agrees with the reading of para 4(2)(a) under + which "£20 was disregarded" means the £20 disregard of Sch 7 paras 6 and + 7(1): the family would lose its Pension Credit and get the award back. + On the reading under which para 4(2)(a) holds wherever DWP disregarded + at least £20 of earnings in the award, DWP could keep such a family on + Pension Credit with no award where it disregarded at least £20 of those + earnings and the earner stays in employment (para 4(2)(b)). + pension_credit_earnings_disregard does not apply para 4(2), and the + model holds no record of how an award started, so the model also pays + the award and no Pension Credit to a couple in whose award DWP + disregarded £20 of earnings (ESA Regs 2008 Sch 7 paras 6 and 7), though + DWP could keep that couple on Pension Credit with no award (para 4(2)). + + In a year with tax credits, a calculation in this branch opens the + passport branch (pension_credit_with_passported_tax_credits), and one in + the passport branch opens this one. Core reads a branch's inputs from + its parent branches as well, so a branch of either keeps both sets of + inputs. + """ + simulation = benunit.simulation + # get_branch returns an existing branch of that name, or this simulation + # if it has that name, so take a name in use by neither. + name = NO_INCOME_RELATED_ESA_BRANCH + while name in simulation.branches or name == simulation.branch_name: + name += "_" + branch = simulation.get_branch(name) + try: + for variable in ["esa_income", "claimant_or_partner_esa_income"]: + branch.set_input(variable, period, benunit.empty_array()) + branch.set_input("esa_income_eligible", period, benunit.filled_array(False)) + # Through the population, so a microsimulation returns an + # unweighted array. + return branch.populations[benunit.entity.key]("pension_credit", period) + finally: + simulation.branches.pop(name, None) class esa_income_eligible(Variable): value_type = bool entity = BenUnit label = ( - "Whether a reported income-related ESA award passes the capital and work tests" + "Whether a reported income-related ESA award passes the age, Pension " + "Credit, capital and work tests" ) documentation = ( - "Bounded work/capital screen for reported income-related ESA, not a full " - "entitlement model. When an original claimant or partner reports an open " - "award, only they are candidate claimants; otherwise other open reporters " - "are candidates. The claimant must not engage in remunerative work (Welfare " - "Reform Act 2007 Sch 1 para 6(1)(e); " - "esa_income_claimant_remunerative_work). The other member of the couple " - "must not engage in remunerative work (para 6(1)(f)): paid work of 24 hours " - "a week or more (ESA Regs 2008 reg 42(1)), subject to the existing carer " - "exception (reg 43(2)(c); IS Regs 1987 Sch 1B para 4), which the model " + "Bounded age, Pension Credit, work and capital screen for reported " + "income-related ESA, not a full entitlement model. When an original claimant " + "or partner reports an open award, only they are candidate claimants; " + "otherwise other open reporters are candidates. The claimant must not have " + "reached pensionable age (Welfare Reform Act 2007 s.1(3)(c)): their own State " + "Pension age (s.1(6)), which is_SP_age places at 6 October of the year. " + "Neither the claimant nor the other member of the couple may have an " + "entitlement to State Pension Credit (Sch 1 para 6(1)(c) and (d)). The " + "model's Pension Credit reads the couple's award, so the screen reads the " + "Pension Credit the couple would get with no income-related ESA in the " + "family (pension_credit_without_income_related_esa), which counts the claim " + "(would_claim_pc) and an entered award, and refuses the couple's claim where " + "it exceeds nil. The screen works that out for each couple whose claim " + "passes the other tests, and only where some couple's claim does. Where the " + "model's own formulas calculate Pension Credit " + "(pension_credit_follows_model_formulas), they pay it only to a couple that " + "meets the Pension Credit age conditions (meets_pension_credit_age_conditions) " + "and would claim, so the screen works it out only for such a couple. DWP " + "disregards £20 a week of earnings while the claimant or partner receives " + "ESA (SPC Regs 2002 Sch VI para 4(1)(a)(vii)). Where the family gets Pension " + "Credit only with that disregard, the screen passes the couple's claim, and " + "pension_credit pays nothing while the couple has the award. A member who " + "claims in their own right keeps their claim whatever the couple's Pension " + "Credit. The claimant must not engage in remunerative work " + "(para 6(1)(e); esa_income_claimant_remunerative_work). The other member of " + "the couple must not engage in remunerative work (para 6(1)(f)): paid work of " + "24 hours a week or more (ESA Regs 2008 reg 42(1)), subject to the existing " + "carer exception (reg 43(2)(c); IS Regs 1987 Sch 1B para 4), which the model " "reads as is_carer_for_benefits. Assessable capital must remain within the " - "existing limit (para 6(1)(b); reg 110). The aggregate esa_income uses this " - "screen for its original couple's part; each outside reporter's claim is " - "paid separately on their own existing work/capital screen and tariff. The " - "benefit unit's capital is an explicit proxy for unobserved outside-claim " - "capital. Reports whose own award is closed are excluded before choosing " - "candidates; ESA also excludes the couple's reports on a UC claim. This " - "variable's candidate rules are unchanged by the separate aggregation." + "existing limit (para 6(1)(b); reg 110). The model applies neither para " + "6(1)(g) (receiving education) nor the other member's income-based JSA under " + "para 6(1)(d). The Northern Ireland Assembly set the same conditions in the " + "Welfare Reform Act (Northern Ireland) 2007 s.1(3)(c) and Sch 1 para 6(1)(c) " + "and (d), but took pensionable age in s.1(6) from the Pensions (Northern " + "Ireland) Order 1995 Sch 2 para 1, where Parliament took it from the Pensions " + "Act 1995 Sch 4 para 1. Under either schedule a person born on a given date " + "reaches pensionable age on the same day, so the model applies one rule " + "across the UK. The aggregate esa_income uses this screen for its original " + "couple's part and pays each outside reporter's claim separately, on their " + "own existing work/capital screen, pensionable age and tariff. The screen " + "uses the benefit unit's capital as an explicit proxy for unobserved " + "outside-claim capital. It drops reports whose own award has closed before it " + "chooses candidates, and ESA also drops the couple's reports on a UC claim. " + "The separate aggregation leaves this variable's candidate rules unchanged." ) definition_period = YEAR reference = ( + "https://www.legislation.gov.uk/ukpga/2007/5/section/1", "https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6", + "https://www.legislation.gov.uk/ukpga/1995/26/schedule/4", + "https://www.legislation.gov.uk/ukpga/2002/16/section/4", + "https://www.legislation.gov.uk/uksi/2019/37/article/4", "https://www.legislation.gov.uk/uksi/2008/794/regulation/41", "https://www.legislation.gov.uk/uksi/2008/794/regulation/42", "https://www.legislation.gov.uk/uksi/2008/794/regulation/43", "https://www.legislation.gov.uk/uksi/2008/794/regulation/110", + "https://www.legislation.gov.uk/nia/2007/2/section/1", + "https://www.legislation.gov.uk/nia/2007/2/schedule/1/paragraph/6", + "https://www.legislation.gov.uk/nisi/1995/3213/schedule/2", ) def formula(benunit, period, parameters): @@ -49,6 +221,10 @@ def formula(benunit, period, parameters): candidate = reports_award & ( claimant_or_partner | ~benunit.project(couple_reports) ) + # WRA 2007 s.1(3)(c): the claimant "has not reached pensionable age" + # (s.1(6): the age in Pensions Act 1995 Sch 4 para 1). A man born + # before 6 December 1953 reaches it at 65. + pensionable_age = person("is_SP_age", period) # Para 6(1)(e) and reg 41: the claimant's own remunerative work. claimant_works = person("esa_income_claimant_remunerative_work", period) # Para 6(1)(f) and reg 42(1): the other member of the couple works 24 @@ -62,5 +238,31 @@ def formula(benunit, period, parameters): other_member_works = claimant_or_partner & ( (benunit.project(benunit.sum(works_as_partner)) - works_as_partner) > 0 ) - claimant = candidate & ~claimant_works & ~other_member_works - return benunit.any(claimant) & (capital <= ESA.capital.limit) + passes = candidate & ~pensionable_age & ~claimant_works & ~other_member_works + within_capital = capital <= ESA.capital.limit + couple_passes = benunit.any(passes & claimant_or_partner) & within_capital + if not couple_passes.any(): + return benunit.any(passes) & within_capital + # Sch 1 para 6(1)(c) and (d): Parliament allows the income-related + # allowance only where neither the claimant nor the other member of + # the couple has an entitlement to State Pension Credit. The model's + # Pension Credit reads the couple's award, so this reads the couple's + # Pension Credit without it, for each couple whose claim passes the + # other tests. The model's own formulas pay Pension Credit only to a + # couple that meets the age conditions and would claim, so where they + # calculate it, this reads it only for a couple that does, and only + # where some couple does. A member who claims in their own right keeps + # their claim whatever the couple's Pension Credit. + on_pension_credit = couple_passes + if pension_credit_follows_model_formulas(benunit, period, parameters): + on_pension_credit = ( + couple_passes + & benunit("meets_pension_credit_age_conditions", period) + & benunit("would_claim_pc", period) + ) + if on_pension_credit.any(): + on_pension_credit = on_pension_credit & ( + pension_credit_without_income_related_esa(benunit, period) > 0 + ) + pension_credit_bars = claimant_or_partner & benunit.project(on_pension_credit) + return benunit.any(passes & ~pension_credit_bars) & within_capital diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py index 4f629c776a..7c935d79a8 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/benefit_cap/is_housing_benefit_benefit_cap_exempt_specified_benefit.py @@ -120,7 +120,12 @@ def received_by(benefits, members): receiving_income_related_esa = benunit.project( couple_esa_award > 0 ) & is_payee_of_couple_award( - person, period, "esa_income_reported", couple=claimant_or_partner + person, + period, + "esa_income_reported", + # WRA 2007 s.1(3)(c): not a member who has reached pensionable age. + can_claim=~person("is_SP_age", period), + couple=claimant_or_partner, ) receiving_esa = (person("esa_contrib", period) > 0) | ( receiving_income_related_esa diff --git a/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py b/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py index 318ee8f10c..52b463ab20 100644 --- a/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py +++ b/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py @@ -55,7 +55,7 @@ def formula(person, period, parameters): person, period, "jsa_income", - works=hours >= JSA.remunerative_work.claimant_hours, + barred=hours >= JSA.remunerative_work.claimant_hours, capital_limit=JSA.income.capital.limit, ) return where(person("is_claimant_or_partner", period), couple_award, own_award) diff --git a/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py b/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py index 24ccc86b0b..678c5cd314 100644 --- a/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py +++ b/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py @@ -16,12 +16,16 @@ class is_on_income_related_esa(Variable): "couple's award is paid to the claimant, not the partner: of the " "claimant and partner, only the payee is on it, meaning the one who " "reports the award, or the claimant where neither does, and only while " - "their award (claimant_or_partner_esa_income) is positive. Any other " + "their award (claimant_or_partner_esa_income) is positive. A member " + "who has reached pensionable age cannot be the claimant (Welfare Reform " + "Act 2007 s.1(3)(c)), so where the other member has not, only the " + "other member can be the payee. Any other " "member of the benefit unit, such as a non-dependent adult, claims in " - "their own right and is on it only if their own claim passes: they are " - "not engaged in remunerative work as its claimant " - "(esa_income_claimant_remunerative_work; Welfare Reform Act 2007 Sch 1 " - "para 6(1)(e)), and the award on their own report alone is positive, " + "their own right and is on it only if their own claim passes: they " + "have not reached pensionable age (is_SP_age; s.1(3)(c)), they are not " + "engaged in remunerative work as its claimant " + "(esa_income_claimant_remunerative_work; Sch 1 para 6(1)(e)), and the " + "award on their own report alone is positive, " "the report exceeding the tariff income from the benefit unit's " "capital, within the capital limit. On the formula path the claimant's " "or partner's claim and work never change this. The legacy " @@ -43,19 +47,28 @@ class is_on_income_related_esa(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + "https://www.legislation.gov.uk/ukpga/2007/5/section/1", "https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6", ) def formula(person, period, parameters): + # WRA 2007 s.1(3)(c): a member who has reached pensionable age cannot + # be the claimant, so the model pays them none of the award. + pensionable_age = person("is_SP_age", period) couple_award = ( person.benunit("claimant_or_partner_esa_income", period) > 0 - ) & (is_payee_of_couple_award(person, period, "esa_income_reported")) - # Their own claim, on their own report and their own work alone. + ) & is_payee_of_couple_award( + person, period, "esa_income_reported", can_claim=~pensionable_age + ) + # Their own claim, on their own report, work and age alone (WRA 2007 + # s.1(3)(c): a claimant who has reached pensionable age has no + # entitlement). own_award = own_report_is_paid( person, period, "esa_income", - works=person("esa_income_claimant_remunerative_work", period), + barred=person("esa_income_claimant_remunerative_work", period) + | pensionable_age, capital_limit=parameters(period).gov.dwp.ESA.income.capital.limit, ) return where(person("is_claimant_or_partner", period), couple_award, own_award) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit.py index d949955407..97295a64ff 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit.py @@ -7,12 +7,40 @@ class pension_credit(Variable): definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/ukpga/2002/16/contents" + documentation = ( + "State Pension Credit the family receives: its entitlement where it " + "would claim (would_claim_pc), and nil where the claimant or partner " + "has income-related ESA (claimant_or_partner_esa_income above nil). " + "Parliament allows income-related ESA only where neither the claimant " + "nor the other member of the couple has an entitlement to State " + "Pension Credit (Welfare Reform Act 2007 Sch 1 para 6(1)(c) and (d)), " + "so the two awards never meet in one couple. The ESA screen " + "(esa_income_eligible) refuses the couple's award where the family " + "would get Pension Credit without it. Where the family gets Pension " + "Credit only with the award, through the higher earnings disregard " + "for a claimant or partner in receipt of ESA (SPC Regs 2002 Sch VI " + "para 4(1)(a)(vii)), the model pays the award, and this formula " + "pays no Pension Credit. A member outside the couple who claims " + "income-related ESA in their own right leaves the couple's Pension " + "Credit unchanged: in " + "para 6(1)(d) Parliament named only the other member of the " + "claimant's couple. Under gov.contrib.freeze_pension_credit, the " + "formula returns the baseline's award." + ) + reference = ( + "https://www.legislation.gov.uk/ukpga/2002/16/contents", + "https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6", + ) def formula(benunit, period, parameters): entitlement = benunit("pension_credit_entitlement", period) would_claim = benunit("would_claim_pc", period) - amount = entitlement * would_claim + # The formula pays nothing while the couple has income-related ESA, + # so that no couple gets both: Parliament allows the award only where + # neither member of the couple has an entitlement to State Pension + # Credit (WRA 2007 Sch 1 para 6(1)(c) and (d)). + couple_esa = benunit("claimant_or_partner_esa_income", period) + amount = where(couple_esa > 0, 0, entitlement * would_claim) freeze = parameters(period).gov.contrib.freeze_pension_credit baseline = benunit.simulation.baseline diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py index a0d689c3c7..5b0f20425c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/benefit_cap/is_uc_benefit_cap_exempt_specified_benefit.py @@ -134,7 +134,12 @@ def received_by(benefits, members): receiving_income_related_esa = benunit.project( couple_esa_award > 0 ) & is_payee_of_couple_award( - person, period, "esa_income_reported", couple=couple + person, + period, + "esa_income_reported", + # WRA 2007 s.1(3)(c): not a member who has reached pensionable age. + can_claim=~person("is_SP_age", period), + couple=couple, ) receiving_esa = (person("esa_contrib", period) > 0) | ( receiving_income_related_esa