From bebbfad61238d3d4e1e94e17d268315f888043d4 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 01:32:10 -0400 Subject: [PATCH 01/11] Apply the ESA and JSA remunerative work conditions to reported awards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Income-related ESA (WRA 2007 Sch 1 para 6(1)(e), (f); ESA Regs 2008 regs 41-43, 45) and income-based JSA (Jobseekers Act 1995 ss.1(2)(e), 1(2B)(b), 3(1)(e); JSA Regs 1996 regs 3A, 51) are barred when the claimant, or the other member of the couple, is engaged in remunerative work. The screens applied only the capital test. - ESA claimant: paid work other than exempt work (£20 a week, or under 16 hours within 16 x NMW), on net earnings. - ESA partner: 24 hours, except a carer (reg 43(2)(c)). - JSA: 16 hours for a claimant, including each member of a joint-claim couple; 24 for a partner otherwise; no carer exception. - Candidates are the claimant and partner when either reports the award. The income_related_*_award helpers are unchanged; they read the screens. The Income Support property reference now applies the same tests, holding the JSA joint-claim status fixed in the invariance property and allowing work that ends a barring award in the monotonicity property. Co-Authored-By: Claude Opus 5.5 --- .../esa-jsa-remunerative-work.fixed.md | 3 + .../exempt_work/higher_earnings_limit.yaml | 74 +++ .../gov/dwp/ESA/exempt_work/hours_limit.yaml | 14 + .../ESA/exempt_work/lower_earnings_limit.yaml | 18 + .../remunerative_work/partner_hours.yaml | 16 + .../parameters/gov/dwp/JSA/hours/couple.yaml | 6 +- .../parameters/gov/dwp/JSA/hours/single.yaml | 6 +- .../income/joint_claim/born_after_year.yaml | 23 + .../dwp/JSA/income/joint_claim/in_effect.yaml | 16 + .../JSA/remunerative_work/claimant_hours.yaml | 16 + .../JSA/remunerative_work/partner_hours.yaml | 16 + .../tests/legacy_award_work_reference.py | 160 +++++ .../esa_income_remunerative_work.yaml | 570 ++++++++++++++++++ .../jsa_income_remunerative_work.yaml | 405 +++++++++++++ ...t_income_support_eligibility_properties.py | 76 ++- .../test_legacy_award_work_properties.py | 242 ++++++++ .../gov/dwp/esa_exempt_work_earnings.py | 77 +++ .../esa_income_claimant_remunerative_work.py | 50 ++ .../variables/gov/dwp/esa_income_eligible.py | 54 +- .../dwp/esa_income_remunerative_work_hours.py | 42 ++ .../gov/dwp/is_jsa_joint_claim_couple.py | 51 ++ .../variables/gov/dwp/jsa_income_eligible.py | 60 +- .../gov/dwp/jsa_remunerative_work_hours.py | 42 ++ 23 files changed, 2006 insertions(+), 31 deletions(-) create mode 100644 changelog.d/esa-jsa-remunerative-work.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/exempt_work/hours_limit.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/exempt_work/lower_earnings_limit.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/income/remunerative_work/partner_hours.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml create mode 100644 policyengine_uk/tests/legacy_award_work_reference.py create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml create mode 100644 policyengine_uk/tests/test_legacy_award_work_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py create mode 100644 policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py create mode 100644 policyengine_uk/variables/gov/dwp/esa_income_remunerative_work_hours.py create mode 100644 policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py create mode 100644 policyengine_uk/variables/gov/dwp/jsa_remunerative_work_hours.py diff --git a/changelog.d/esa-jsa-remunerative-work.fixed.md b/changelog.d/esa-jsa-remunerative-work.fixed.md new file mode 100644 index 000000000..0f8218151 --- /dev/null +++ b/changelog.d/esa-jsa-remunerative-work.fixed.md @@ -0,0 +1,3 @@ +- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage, rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)), on earnings net of tax, National Insurance and half the pension contributions (new `esa_exempt_work_earnings` and `esa_income_claimant_remunerative_work`). For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)). Hours come from the new `esa_income_remunerative_work_hours`. +- End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). In a joint-claim couple, a couple without children (s.1(4); reg 3A(1); new `is_jsa_joint_claim_couple`, from 19 March 2001), each member is a claimant, so neither may work 16 hours (s.1(2B)(b)). The JSA regulations have no carer exception. Hours come from the new `jsa_remunerative_work_hours`. +- The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours`, `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}` and `gov.dwp.JSA.income.joint_claim.{in_effect, born_after_year}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. diff --git a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml new file mode 100644 index 000000000..c7960df11 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml @@ -0,0 +1,74 @@ +description: Work done for less than the exempt work hours limit, for which a claimant's earnings in any week do not exceed this amount, is exempt work (permitted work) for Employment and Support Allowance. From 11 April 2011 the limit is 16 times the National Minimum Wage, rounded up to the next 50p, and changes when that wage does. +values: + 2008-10-27: + value: 92 + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(4) (as made) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45/2008-10-27 + 2009-10-01: + value: 93 + reference: + - title: Social Security (Miscellaneous Amendments) (No. 3) Regulations 2009 (SI 2009/2343) reg. 5(2) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45/2009-10-01 + # From 11 April 2011 (SI 2011/674 reg. 16(5)): 16 x the National Minimum + # Wage, rounded up to the next 50p (reg. 45(9A)). The rate is that in + # reg. 11 of the National Minimum Wage Regulations 1999 (ESA Regs reg. 2(1)), + # now the National Living Wage; DMG 41190 and Appendix 5 list it. + 2011-04-11: + value: 95 # 16 x £5.93 = £94.88 + reference: + - title: Social Security (Miscellaneous Amendments) Regulations 2011 (SI 2011/674) reg. 16(5) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45/2011-04-11 + - title: Decision Makers' Guide ch. 41, para. 41190 and Appendix 5 + href: https://assets.publishing.service.gov.uk/media/689db34d1fedc616bb13399c/dmg-ch-41.pdf + 2011-10-01: + value: 97.5 # 16 x £6.08 = £97.28 + 2012-10-01: + value: 99.5 # 16 x £6.19 = £99.04 + 2013-10-01: + value: 101 # 16 x £6.31 = £100.96 + 2014-10-01: + value: 104 # 16 x £6.50 + 2015-10-01: + value: 107.5 # 16 x £6.70 = £107.20 + 2016-04-01: + value: 115.5 # 16 x £7.20 = £115.20 + 2017-04-01: + value: 120 # 16 x £7.50 + 2018-04-01: + value: 125.5 # 16 x £7.83 = £125.28 + 2019-04-01: + value: 131.5 # 16 x £8.21 = £131.36 + 2020-04-01: + value: 140 # 16 x £8.72 = £139.52 + 2021-04-01: + value: 143 # 16 x £8.91 = £142.56 + 2022-04-01: + value: 152 # 16 x £9.50 + 2023-04-01: + value: 167 # 16 x £10.42 = £166.72 + 2024-04-01: + value: 183.5 # 16 x £11.44 = £183.04 + reference: + - title: DWP benefit and pension rates 2025 to 2026 (permitted work higher, 2024/25) + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 + 2025-04-01: + value: 195.5 # 16 x £12.21 = £195.36 + reference: + - title: DWP benefit and pension rates 2025 to 2026 (permitted work higher) + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2025-to-2026/benefit-and-pension-rates-2025-to-2026 + 2026-04-01: + value: 203.5 # 16 x £12.71 = £203.36 + reference: + - title: DWP benefit and pension rates 2026 to 2027 (permitted work higher) + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2026-to-2027/proposed-benefit-and-pension-rates-2026-to-2027 +metadata: + unit: currency-GBP + period: week + label: ESA exempt work higher earnings limit + # The National Living Wage is set with reference to earnings, so later + # years follow average earnings until the rate is announced. + uprating: gov.economic_assumptions.indices.obr.average_earnings + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(4) and (9A) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45 diff --git a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/hours_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/hours_limit.yaml new file mode 100644 index 000000000..bf1cd5425 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/hours_limit.yaml @@ -0,0 +1,14 @@ +description: Work done for less than this many hours a week, with earnings within the higher earnings limit, is exempt work (permitted work) for Employment and Support Allowance. +values: + 2008-10-27: + value: 16 + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(4) (as made) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45/2008-10-27 +metadata: + unit: hour + period: week + label: ESA exempt work hours limit + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(4) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45 diff --git a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/lower_earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/lower_earnings_limit.yaml new file mode 100644 index 000000000..0e4e78747 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/lower_earnings_limit.yaml @@ -0,0 +1,18 @@ +description: Work for which a claimant's earnings in any week do not exceed this amount is exempt work, so it does not end an Employment and Support Allowance award or count as remunerative work for income-related ESA, whatever the hours. +values: + 2008-10-27: + value: 20 + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(2) (as made) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45/2008-10-27 +metadata: + unit: currency-GBP + period: week + label: ESA exempt work lower earnings limit + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(2) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45 + - title: Employment and Support Allowance Regulations 2008 regs. 40(2)(f) and 41(1) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/41 + - title: DWP benefit and pension rates 2026 to 2027 (permitted work lower) + href: https://www.gov.uk/government/publications/benefit-and-pension-rates-2026-to-2027/proposed-benefit-and-pension-rates-2026-to-2027 diff --git a/policyengine_uk/parameters/gov/dwp/ESA/income/remunerative_work/partner_hours.yaml b/policyengine_uk/parameters/gov/dwp/ESA/income/remunerative_work/partner_hours.yaml new file mode 100644 index 000000000..60e6524b1 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/income/remunerative_work/partner_hours.yaml @@ -0,0 +1,16 @@ +description: Income-related Employment and Support Allowance treats a claimant's partner as engaged in remunerative work at this many weekly hours of paid work or more. +values: + 2008-10-27: + value: 24 + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 42(1) (as made) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/42/2008-10-27 +metadata: + unit: hour + period: week + label: Income-related ESA partner remunerative work hours + reference: + - title: Welfare Reform Act 2007 Sch. 1 para. 6(1)(f) + href: https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6 + - title: Employment and Support Allowance Regulations 2008 reg. 42(1) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/42 diff --git a/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml b/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml index eb295e174..95bfccb93 100644 --- a/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml +++ b/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml @@ -1,10 +1,10 @@ -description: Hours requirement for joint claimants of Jobseeker's Allowance. +description: Income-based Jobseeker's Allowance treats a claimant's partner as engaged in remunerative work at this many weekly hours of paid work or more, except in a joint claim, where each member is a claimant at the claimant's hours. Kept for existing users; the model reads gov.dwp.JSA.remunerative_work.partner_hours. values: 2015-04-01: value: 24 reference: - - title: The Jobseeker's Allowance Regulations 1996 - href: https://www.legislation.gov.uk/uksi/1996/207/made + - title: The Jobseeker's Allowance Regulations 1996, regulation 51(1)(b) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51 metadata: unit: hour period: week diff --git a/policyengine_uk/parameters/gov/dwp/JSA/hours/single.yaml b/policyengine_uk/parameters/gov/dwp/JSA/hours/single.yaml index a5b4d47ce..a43990229 100644 --- a/policyengine_uk/parameters/gov/dwp/JSA/hours/single.yaml +++ b/policyengine_uk/parameters/gov/dwp/JSA/hours/single.yaml @@ -1,10 +1,10 @@ -description: Hours requirement for single claimants of Jobseeker's Allowance. +description: Jobseeker's Allowance treats a claimant as engaged in remunerative work at this many weekly hours of paid work or more. Kept for existing users; the model reads gov.dwp.JSA.remunerative_work.claimant_hours. values: 2015-04-01: value: 16 reference: - - title: The Jobseeker's Allowance Regulations 1996, regulation 13 - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/13 + - title: The Jobseeker's Allowance Regulations 1996, regulation 51(1)(a) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51 metadata: unit: hour period: week diff --git a/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml b/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml new file mode 100644 index 000000000..10a69a779 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml @@ -0,0 +1,23 @@ +description: A couple without children is a joint-claim couple for income-based Jobseeker's Allowance if at least one member is 18 or over and was born after a date in this year (19 March 1976, then 28 October 1957, then 28 October 1947). The model compares birth years, so it treats a member born later in this year as born before the date. +values: + 2001-03-19: + value: 1976 + reference: + - title: Jobseeker's Allowance Regulations 1996 reg. 3A(1) (born after 19 March 1976) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2001-03-19 + 2002-10-28: + value: 1957 + reference: + - title: Jobseeker's Allowance (Joint Claims) Amendment Regulations 2002 (SI 2002/1701) reg. 2(a) (born after 28 October 1957) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2002-10-28 + 2008-02-25: + value: 1947 + reference: + - title: Jobseeker's Allowance (Joint Claims) Amendment Regulations 2008 (SI 2008/13) reg. 2(2) (born after 28 October 1947) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2008-02-25 +metadata: + unit: year + label: Income-based JSA joint claim birth year cutoff + reference: + - title: Jobseeker's Allowance Regulations 1996 reg. 3A(1) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A diff --git a/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml b/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml new file mode 100644 index 000000000..8682f300f --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml @@ -0,0 +1,16 @@ +description: Whether couples without children of the prescribed description claim income-based Jobseeker's Allowance jointly, so that each member is a claimant. +values: + 1996-10-07: false + 2001-03-19: + value: true + reference: + - title: Jobseeker's Allowance (Joint Claims) Regulations 2000 (SI 2000/1978) reg. 2(2), inserting JSA Regs 1996 Part 1A + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2001-03-19 +metadata: + unit: bool + label: Income-based JSA joint claims in effect + reference: + - title: Jobseekers Act 1995 ss. 1(2B), 1(4) and 35(1) + href: https://www.legislation.gov.uk/ukpga/1995/18/section/1 + - title: Jobseeker's Allowance Regulations 1996 reg. 3A + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A diff --git a/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml new file mode 100644 index 000000000..a2d87ee3a --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml @@ -0,0 +1,16 @@ +description: Jobseeker's Allowance treats a claimant, including each member of a joint-claim couple, as engaged in remunerative work at this many weekly hours of paid work or more. +values: + 1996-10-07: + value: 16 + reference: + - title: Jobseeker's Allowance Regulations 1996 reg. 51(1)(a) (as made) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51/1996-10-07 +metadata: + unit: hour + period: week + label: JSA claimant remunerative work hours + reference: + - title: Jobseekers Act 1995 s. 1(2)(e) + href: https://www.legislation.gov.uk/ukpga/1995/18/section/1 + - title: Jobseeker's Allowance Regulations 1996 reg. 51(1)(a) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51 diff --git a/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml new file mode 100644 index 000000000..76a8d0d7b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml @@ -0,0 +1,16 @@ +description: Income-based Jobseeker's Allowance treats a claimant's partner, outside a joint claim, as engaged in remunerative work at this many weekly hours of paid work or more. +values: + 1996-10-07: + value: 24 + reference: + - title: Jobseeker's Allowance Regulations 1996 reg. 51(1)(b) (as made) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51/1996-10-07 +metadata: + unit: hour + period: week + label: Income-based JSA partner remunerative work hours + reference: + - title: Jobseekers Act 1995 s. 3(1)(e) + href: https://www.legislation.gov.uk/ukpga/1995/18/section/3 + - title: Jobseeker's Allowance Regulations 1996 reg. 51(1)(b) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51 diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py new file mode 100644 index 000000000..ebcc2b1bd --- /dev/null +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -0,0 +1,160 @@ +"""Reference readings of the income-related ESA and income-based JSA screens. + +Written from the law, family by family and in plain Python, for property +tests to compare with the model (test_legacy_award_work_properties.py and +test_income_support_eligibility_properties.py). Each adult is a dict of +simulation inputs for one year: hours_worked (annual), employment_income, +employee_pension_contributions, receives_carer_benefit, care_hours, age and +the reported awards. + +- Capital (WRA 2007 Sch 1 para 6(1)(b), ESA Regs reg 110; JSA 1995 s.13(1), + JSA Regs reg 107): within £16,000, with £1 a week of tariff income for + each £250 or part over £6,000. +- ESA claimant (para 6(1)(e), ESA Regs reg 41(1)): paid work is remunerative + unless it is exempt work: earnings of no more than £20 a week (reg 45(2)), + or under 16 hours with earnings within the higher limit (reg 45(4)). + Earnings are net of income tax, National Insurance and half the pension + contributions (regs 96(3), 98(4)). +- ESA partner (para 6(1)(f), reg 42(1)): 24 hours or more, unless a carer + (reg 43(2)(c)). +- JSA claimant (s.1(2)(e), reg 51(1)(a)): 16 hours or more, with no carer + exception. Each member of a joint-claim couple is a claimant (s.1(2B)(b)); + otherwise the partner's limit is 24 hours (s.3(1)(e), reg 51(1)(b)). +- The claimant is a member who reports the award; when the claimant or + partner reports one, only they are candidates. +""" + +import math + +WEEKS = 52 + + +def tariff_income(capital, rules): + """Annual tariff income: £1 a week for each £250 (or part) over £6,000.""" + excess = max(0, capital - rules.tariff_income.threshold) + return ( + math.ceil(excess / rules.tariff_income.step) + * rules.tariff_income.amount + * WEEKS + ) + + +def award_survives_capital_test(reported, capital, rules): + """Whether a reported award survives a legacy benefit's capital test.""" + return ( + reported > 0 + and capital <= rules.limit + and reported > tariff_income(capital, rules) + ) + + +def carer(adult): + return ( + adult.get("receives_carer_benefit", False) or adult.get("care_hours", 0) >= 35 + ) + + +def weekly_hours(adult): + return adult.get("hours_worked", 0) / WEEKS + + +def esa_weekly_earnings(adult, parameters): + """Net weekly earnings for the exempt work limits, for an employee with no + other income and earnings within the basic rate band.""" + gross = adult.get("employment_income", 0) + pension = adult.get("employee_pension_contributions", 0) + hmrc = parameters.gov.hmrc + allowance = hmrc.income_tax.allowances.personal_allowance.amount + basic_rate = hmrc.income_tax.rates.uk.rates[0] + # Pension contributions are only used with pay below the personal + # allowance, so how they are relieved for tax does not arise. + assert pension == 0 or gross <= allowance, adult + tax = basic_rate * max(0, gross - allowance) + class_1 = hmrc.national_insurance.class_1 + threshold = class_1.thresholds.primary_threshold * WEEKS + national_insurance = class_1.rates.employee.main * max(0, gross - threshold) + return max(0, gross - tax - national_insurance - pension / 2) / WEEKS + + +def esa_claimant_in_remunerative_work(adult, parameters): + exempt_work = parameters.gov.dwp.ESA.exempt_work + earnings = esa_weekly_earnings(adult, parameters) + within_lower_limit = earnings <= exempt_work.lower_earnings_limit + permitted_work = ( + weekly_hours(adult) < exempt_work.hours_limit + and earnings <= exempt_work.higher_earnings_limit + ) + return not (within_lower_limit or permitted_work) + + +def candidates(couple, others, reported): + """Indexes (into couple + others) of members who could be the claimant.""" + members = couple + others + couple_reports = any(adult.get(reported, 0) > 0 for adult in couple) + return [ + i + for i, member in enumerate(members) + if member.get(reported, 0) > 0 and (i < len(couple) or not couple_reports) + ] + + +def esa_screen(couple, others, capital, parameters): + """esa_income_eligible for a claimant and partner (couple) and other + members of the benefit unit outside the family (others).""" + ESA = parameters.gov.dwp.ESA.income + members = couple + others + + def other_member_works(i): + if i >= len(couple): + return False + return any( + not carer(other) + and weekly_hours(other) >= ESA.remunerative_work.partner_hours + for j, other in enumerate(couple) + if j != i + ) + + passes = any( + not esa_claimant_in_remunerative_work(members[i], parameters) + and not other_member_works(i) + for i in candidates(couple, others, "esa_income_reported") + ) + return passes and capital <= ESA.capital.limit + + +def jsa_joint_claim(couple, family_has_child, year, parameters): + joint_claim = parameters.gov.dwp.JSA.income.joint_claim + return ( + bool(joint_claim.in_effect) + and len(couple) == 2 + and not family_has_child + and any( + adult["age"] >= 18 and year - adult["age"] > joint_claim.born_after_year + for adult in couple + ) + ) + + +def jsa_screen(couple, others, capital, joint_claim, parameters): + """jsa_income_eligible for a claimant and partner (couple), other members + of the benefit unit outside the family (others) and the couple's + joint-claim status.""" + JSA = parameters.gov.dwp.JSA + WORK = JSA.remunerative_work + members = couple + others + other_limit = WORK.claimant_hours if joint_claim else WORK.partner_hours + + def other_member_works(i): + if i >= len(couple): + return False + return any( + weekly_hours(other) >= other_limit + for j, other in enumerate(couple) + if j != i + ) + + passes = any( + weekly_hours(members[i]) < WORK.claimant_hours and not other_member_works(i) + for i in candidates(couple, others, "jsa_income_reported") + ) + return passes and capital <= JSA.income.capital.limit diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml new file mode 100644 index 000000000..c66ca5ae6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -0,0 +1,570 @@ +# Income-related ESA remunerative work conditions. +# Welfare Reform Act 2007 Sch 1 para 6(1): the claimant +# (e) "is not engaged in remunerative work"; +# (f) "is not a member of a couple the other member of which is engaged in +# remunerative work". +# ESA Regs 2008 reg 41(1): for the claimant, remunerative work is "any work +# which a claimant does for which payment is made or which is done in +# expectation of payment, other than work listed in paragraph (2) of +# regulation 40", which includes exempt work (reg 40(2)(f), reg 45): +# reg 45(2): "Work for which the earnings in any week do not exceed £20.00"; +# reg 45(4): "Work which is done for less than 16 hours a week, for which +# earnings in any week do not exceed 16 x National Minimum Wage", +# rounded up to the next 50p (reg 45(9A)): £195.50 in 2025-26 (16 x +# £12.21 = £195.36) and £203.50 in 2026-27 (16 x £12.71 = £203.36). +# Earnings are net: less income tax, National Insurance and half the +# claimant's pension contributions (DMG 41189; regs 96(3), 98(4)). +# Reg 42(1): for the partner, remunerative work is paid work "for not less +# than 24 hours a week"; reg 43(2)(c): a partner who would satisfy IS Regs +# Sch 1B para 4 (a carer) is not treated as engaged in it. +# +# Hours are annual hours_worked; the weekly figure is hours_worked / 52, so +# 780 is 15 hours, 832 is 16, 1,196 is 23 and 1,248 is 24. Earnings are annual +# employment_income; 1,040 is £20 a week and 10,166 is £195.50. No tax or +# National Insurance is due below £12,570 a year. Capital is nil. + +# Para 6(1)(e): the claimant's own work. + +- name: A claimant with no paid work keeps the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 0 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + +- name: Earnings of £20 a week are exempt work whatever the hours (reg 45(2)) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 30 hours a week for £20 a week. + hours_worked: 1_560 + employment_income: 1_040 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_income_remunerative_work_hours: 30 + esa_exempt_work_earnings: 20 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + +- name: Earnings over £20 a week at 16 hours are remunerative work + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 16 hours a week for £21 a week: not under 16 hours, so not + # permitted work, and over the £20 lower limit. + hours_worked: 832 + employment_income: 1_092 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_income_remunerative_work_hours: 16 + esa_exempt_work_earnings: 21 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: Under 16 hours within the higher limit is permitted work (reg 45(4)) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 15 hours a week for exactly £195.50 a week, the 2025-26 limit. + hours_worked: 780 + employment_income: 10_166 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 195.5 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + +- name: Under 16 hours above the higher limit is remunerative work + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 15 hours a week for £196 a week, over £195.50. + hours_worked: 780 + employment_income: 10_192 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 196 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: The 2026-27 higher limit is £203.50 + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 15 hours a week for £196 a week: over the 2025-26 limit, within + # 2026-27's. + hours_worked: 780 + employment_income: 10_192 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 196 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + +- name: Earnings over the 2026-27 higher limit are remunerative work + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 15 hours a week for £204 a week, over £203.50. + hours_worked: 780 + employment_income: 10_608 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 204 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + +- name: Half the claimant's pension contributions come off earnings + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 15 hours a week for £200 a week gross, paying £10 a week into a + # pension: £200 - £5 = £195 a week, within £195.50. + hours_worked: 780 + employment_income: 10_400 + employee_pension_contributions: 520 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 195 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + +- name: Without the pension contributions the same pay is over the limit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 10_400 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 200 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + +- name: Income tax comes off earnings as the lowest slice of non-savings income + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 520 + employment_income: 13_000 + private_pension_income: 5_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # The personal allowance (£12,570) goes against earnings first, so tax on + # earnings is 20% x (£13,000 - £12,570) = £86; tax on the pension is not + # deducted. Class 1 NI: 8% x (£13,000 - 52 x £241.73) = £34.40. + # (£13,000 - £86 - £34.40) / 52 = £247.68. + esa_exempt_work_earnings: 247.68 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + +- name: A carer claimant is not taken out of remunerative work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + receives_carer_benefit: true + # 16 hours a week for £100 a week. + hours_worked: 832 + employment_income: 5_200 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + is_carer_for_benefits: true + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + +- name: A claimant's remunerative work entered directly (supervised work) + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + # 20 hours a week for £100 a week of supervised work within the + # higher limit, which reg 45(3) exempts whatever the hours. + hours_worked: 1_040 + employment_income: 5_200 + esa_income_claimant_remunerative_work: false + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_income_eligible: true + esa_income: 5_000 + +# Para 6(1)(f): the partner's work, 24 hours (reg 42(1)). + +- name: A partner working 23 hours a week does not end the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + partner: + age: 40 + is_claimant_or_partner: true + hours_worked: 1_196 + employment_income: 15_000 + benunits: + couple: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + esa_income_remunerative_work_hours: [0, 23] + esa_income_eligible: true + esa_income: 5_000 + +- name: A partner working 24 hours a week ends the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + partner: + age: 40 + is_claimant_or_partner: true + # The partner's test is hours alone: any earnings, including none. + hours_worked: 1_248 + benunits: + couple: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + esa_income_remunerative_work_hours: [0, 24] + esa_income_eligible: false + esa_income: 0 + +- name: A carer partner working 40 hours is not in remunerative work (reg 43(2)(c)) + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + partner: + age: 40 + is_claimant_or_partner: true + receives_carer_benefit: true + hours_worked: 2_080 + benunits: + couple: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + esa_income_eligible: true + esa_income: 5_000 + +- name: A partner's hours entered directly (a reg 43(1) employment) + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + partner: + age: 40 + is_claimant_or_partner: true + # 30 hours a week, all as a part-time firefighter (reg 43(1)(e)(i)), + # which do not count (reg 42(6)). + hours_worked: 1_560 + esa_income_remunerative_work_hours: 0 + benunits: + couple: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + esa_income_eligible: true + +- name: The partner's own earnings above the limit are not the claimant's test + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + partner: + age: 40 + is_claimant_or_partner: true + # 20 hours a week for £300 a week: remunerative work were the partner + # the claimant, but under the partner's 24 hours. + hours_worked: 1_040 + employment_income: 15_600 + benunits: + couple: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + esa_income_claimant_remunerative_work: [false, true] + esa_income_eligible: true + +- name: When both report the award, one of them passing as claimant is enough + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + second: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # 20 hours a week for £100 a week: remunerative as claimant, under 24 + # hours as partner. The first member is the claimant. + hours_worked: 1_040 + employment_income: 5_200 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + esa_income_eligible: true + +- name: When both report the award and neither can be the claimant, it ends + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + second: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # 30 hours a week for £300 a week: remunerative as claimant, and over + # 24 hours as the first member's partner. + hours_worked: 1_560 + employment_income: 15_600 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + esa_income_eligible: false + esa_income: 0 + +# Members outside the family. + +- name: A non-dependent adult's own award is tested on their own work alone + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 50 + is_claimant_or_partner: true + hours_worked: 2_080 + employment_income: 30_000 + lodger: + age: 30 + is_claimant_or_partner: false + esa_income_reported: 5_000 + benunits: + benunit: + members: [head, lodger] + households: + household: + members: [head, lodger] + output: + # The head is not the lodger's partner, so the head's 40 hours do not + # count against the lodger's award. + esa_income_eligible: true + esa_income: 5_000 + +- name: A non-dependent adult's award does not rescue the claimant's + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # 16 hours a week for £100 a week: remunerative work. + hours_worked: 832 + employment_income: 5_200 + lodger: + age: 30 + is_claimant_or_partner: false + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, lodger] + households: + household: + members: [claimant, lodger] + output: + esa_income_eligible: false + +# The Income Support gate reads the screened award (s.124(1)(h)). + +- name: A partner's ESA lost to remunerative work no longer bars Income Support + period: 2025 + absolute_error_margin: 0 + input: + people: + carer: + age: 40 + is_claimant_or_partner: true + receives_carer_benefit: true + income_support_reported: 1_000 + partner: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + # 20 hours a week for £100 a week: remunerative work for the ESA + # claimant, but under the Income Support partner's 24 hours. + hours_worked: 1_040 + employment_income: 5_200 + benunits: + couple: + members: [carer, partner] + income_support_assessable_capital: 0 + households: + household: + members: [carer, partner] + output: + esa_income_eligible: false + esa_income: 0 + # Without the work test the partner's award would count as income-related + # ESA of the other member of the couple and bar the claim. + income_support_eligible: true diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml new file mode 100644 index 000000000..7e54778ab --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml @@ -0,0 +1,405 @@ +# Income-based JSA remunerative work conditions. +# Jobseekers Act 1995 s.1(2)(e): a claimant is entitled if he "is not engaged +# in remunerative work"; s.3(1)(e): the income-based conditions include that +# the claimant "is not a member of a couple the other member of which is +# engaged in remunerative work". s.1(2B)(b): in a joint claim "each member of +# the couple satisfies the conditions set out in paragraphs (a) to (c) and (e) +# to (i) of subsection (2)", and s.35(1) makes each member a claimant. A +# joint-claim couple is a couple with no child or young person for whom one +# of them gets Child Benefit (s.1(4)), at least one of whom is 18 or over and +# born after 28 October 1947 (JSA Regs 1996 reg 3A(1)). +# JSA Regs 1996 reg 51(1): remunerative work is paid work of "not less than +# 16 hours per week" for "a claimant" and "not less than 24 hours per week" +# for "any partner of the claimant". Reg 53 has no exception for carers. +# +# Hours are annual hours_worked; the weekly figure is hours_worked / 52, so +# 780 is 15 hours, 832 is 16, 1,196 is 23 and 1,248 is 24. 2025 is used +# because the model pays income-based JSA until 31 March 2026. Capital is nil. + +- name: A single claimant working 15 hours a week keeps the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + jsa_income_reported: 3_000 + hours_worked: 780 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + jsa_remunerative_work_hours: 15 + jsa_income_eligible: true + jsa_income: 3_000 + +- name: A single claimant working 16 hours a week is in remunerative work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + jsa_income_reported: 3_000 + hours_worked: 832 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + jsa_remunerative_work_hours: 16 + jsa_income_eligible: false + jsa_income: 0 + +- name: A carer claimant working 16 hours is still in remunerative work + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + jsa_income_reported: 3_000 + receives_carer_benefit: true + hours_worked: 832 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + jsa_income_eligible: false + +# A couple with a child: claimant and partner (s.3(1)(e), reg 51(1)(b)). + +- name: With a child, a partner working 23 hours does not end the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + is_parent: true + jsa_income_reported: 3_000 + partner: + age: 30 + is_claimant_or_partner: true + is_parent: true + hours_worked: 1_196 + child: + age: 5 + is_claimant_or_partner: false + benunits: + family: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + is_jsa_joint_claim_couple: false + jsa_income_eligible: true + jsa_income: 3_000 + +- name: With a child, a partner working 24 hours ends the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + is_parent: true + jsa_income_reported: 3_000 + partner: + age: 30 + is_claimant_or_partner: true + is_parent: true + hours_worked: 1_248 + child: + age: 5 + is_claimant_or_partner: false + benunits: + family: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + jsa_income_eligible: false + jsa_income: 0 + +- name: With a child, the claimant working 16 hours ends the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + is_parent: true + jsa_income_reported: 3_000 + hours_worked: 832 + partner: + age: 30 + is_claimant_or_partner: true + is_parent: true + child: + age: 5 + is_claimant_or_partner: false + benunits: + family: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + jsa_income_eligible: false + +- name: With a child, a carer partner working 30 hours still ends the award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + is_claimant_or_partner: true + is_parent: true + jsa_income_reported: 3_000 + partner: + age: 30 + is_claimant_or_partner: true + is_parent: true + receives_carer_benefit: true + hours_worked: 1_560 + child: + age: 5 + is_claimant_or_partner: false + benunits: + family: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + jsa_income_eligible: false + +# A couple without children: a joint claim (s.1(2B)(b)). + +- name: In a joint claim, the other member working 15 hours does not end it + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 30 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + second: + age: 30 + is_claimant_or_partner: true + hours_worked: 780 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + is_jsa_joint_claim_couple: true + jsa_income_eligible: true + jsa_income: 3_000 + +- name: In a joint claim, the other member working 16 hours ends it + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 30 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + second: + age: 30 + is_claimant_or_partner: true + # Under the 24 hours that would apply to a partner, but each member + # of a joint-claim couple is a claimant. + hours_worked: 832 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + is_jsa_joint_claim_couple: true + jsa_income_eligible: false + jsa_income: 0 + +- name: A couple entered as not a joint-claim couple use the partner's 24 hours + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 30 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + second: + age: 30 + is_claimant_or_partner: true + hours_worked: 1_040 + benunits: + couple: + members: [first, second] + # For example where one member claims alone (JSA Regs 1996 reg 3E). + is_jsa_joint_claim_couple: false + households: + household: + members: [first, second] + output: + jsa_income_eligible: true + +- name: A couple born before the prescribed date are not a joint-claim couple + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + # Born in 1945, before 28 October 1947. + age: 80 + is_claimant_or_partner: true + second: + age: 80 + is_claimant_or_partner: true + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + is_jsa_joint_claim_couple: false + +- name: A single claimant is not a joint-claim couple + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 30 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + is_jsa_joint_claim_couple: false + +- name: A couple caring for a placed child are not a joint-claim couple + period: 2025 + absolute_error_margin: 0 + input: + people: + first: + age: 40 + is_claimant_or_partner: true + second: + age: 40 + is_claimant_or_partner: true + child: + age: 8 + is_claimant_or_partner: false + is_looked_after_by_local_authority: true + benunits: + family: + members: [first, second, child] + households: + household: + members: [first, second, child] + output: + # Reg 3A(1) excludes a couple caring for a child in the circumstances + # of reg 78(4), which include a child placed by a local authority. + is_jsa_joint_claim_couple: false + +# Members outside the family. + +- name: A non-dependent adult's own award is tested on their own work alone + period: 2025 + absolute_error_margin: 0 + input: + people: + head: + age: 50 + is_claimant_or_partner: true + hours_worked: 2_080 + lodger: + age: 30 + is_claimant_or_partner: false + jsa_income_reported: 3_000 + benunits: + benunit: + members: [head, lodger] + households: + household: + members: [head, lodger] + output: + jsa_income_eligible: true + jsa_income: 3_000 + +- name: A non-dependent adult's award does not rescue the claimant's + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + hours_worked: 832 + lodger: + age: 30 + is_claimant_or_partner: false + jsa_income_reported: 3_000 + benunits: + benunit: + members: [claimant, lodger] + households: + household: + members: [claimant, lodger] + output: + jsa_income_eligible: false + +# The Income Support gate reads the screened award (s.124(1)(f)). + +- name: A carer's own JSA lost to 16 hours of work no longer bars Income Support + period: 2025 + absolute_error_margin: 0 + input: + people: + carer: + age: 40 + is_claimant_or_partner: true + receives_carer_benefit: true + income_support_reported: 1_000 + jsa_income_reported: 3_000 + # 20 hours a week: a carer is not in remunerative work for Income + # Support (IS Regs reg 6(4)(c)), but is for JSA. + hours_worked: 1_040 + benunits: + benunit: + members: [carer] + income_support_assessable_capital: 0 + households: + household: + members: [carer] + output: + jsa_income_eligible: false + jsa_income: 0 + income_support_eligible: true diff --git a/policyengine_uk/tests/test_income_support_eligibility_properties.py b/policyengine_uk/tests/test_income_support_eligibility_properties.py index 85fa01572..40280cdaf 100644 --- a/policyengine_uk/tests/test_income_support_eligibility_properties.py +++ b/policyengine_uk/tests/test_income_support_eligibility_properties.py @@ -10,7 +10,12 @@ - adding a member who is neither the claimant, the partner nor a child or young person in the family (an adult outside the family, or a child placed by a local authority, IS reg 16(4)) never changes income_support_eligible, - whatever that member's age, ESA, JSA, Income Support, caring or work; + whatever that member's age, ESA, JSA, Income Support, caring or work. One + route is held fixed: a couple caring for a placed child are not a + joint-claim couple for income-based JSA (JSA Regs 1996 reg 3A(1), reg + 78(4)(a)), which changes the partner's remunerative work limit for that + award, so the property enters each couple's joint-claim status + (is_jsa_joint_claim_couple) as it is without the added member; - income_support_eligible equals a family-by-family reading of the model's gate: one of the claimant and partner reports Income Support, is under state pension age, is a carer (or a lone parent of a child aged 5 or under, @@ -19,11 +24,14 @@ of the couple who is a non-carer working 24 hours or more (s.124(1)(aa), (c), (e), (f), (h); IS Regs 1987 regs 5(1), 5(1A) and 6(4)(c)); neither has income-related ESA or income-based JSA (s.124(1)(h), (f)), meaning the - award on their reported amounts after that benefit's capital test, or an - esa_income or jsa_income entered directly; and capital is within the - Income Support limit; + award on their reported amounts after that benefit's capital and work + tests (legacy_award_work_reference), or an esa_income or jsa_income + entered directly; and capital is within the Income Support limit; - raising any claimant's or partner's hours or JSA never makes a family - eligible, because (c) and (f) only ever bar a claim. + eligible, because (c) and (f) only ever bar a claim, except where the work + ends an income-related ESA or income-based JSA award that barred it: a + carer is not in remunerative work for Income Support (reg 6(4)(c)) but is + for JSA, so a carer's own JSA award ends at 16 hours. The second property is a reference check of the bounded model gate, not of legal entitlement: caring, work hours, ESA, JSA and Income Support are the @@ -44,7 +52,9 @@ from hypothesis import HealthCheck, event, given, settings from hypothesis import strategies as st +from legacy_award_work_reference import esa_screen, jsa_joint_claim, jsa_screen from policyengine_uk import Simulation +from policyengine_uk.system import system YEAR = 2025 @@ -199,7 +209,7 @@ def label(units, capital_as_savings, esa_income, jsa_income): event("added member works or reports income-based JSA") -def situation(units, capital_as_savings, esa_income, jsa_income): +def situation(units, capital_as_savings, esa_income, jsa_income, joint_claim=None): label(units, capital_as_savings, esa_income, jsa_income) people, benunits, households = {}, {}, {} for i, (adults, dependants, capital, extra) in enumerate(units): @@ -224,6 +234,8 @@ def situation(units, capital_as_savings, esa_income, jsa_income): benunits[f"b{i}"]["esa_income"] = {YEAR: esa_income[i % len(esa_income)]} if jsa_income is not None: benunits[f"b{i}"]["jsa_income"] = {YEAR: jsa_income[i % len(jsa_income)]} + if joint_claim is not None: + benunits[f"b{i}"]["is_jsa_joint_claim_couple"] = {YEAR: joint_claim[i]} return {"people": people, "benunits": benunits, "households": households} @@ -248,9 +260,18 @@ def test_excluded_member_never_changes_is_eligibility(drawn, data): capital_as_savings, esa_income, jsa_income = data.draw(input_settings(len(drawn))) without = [(*family, None) for family, _ in drawn] with_extra = [(*family, extra) for family, extra in drawn] + parameters = system.parameters(YEAR) + joint_claim = [ + jsa_joint_claim(adults, bool(dependants), YEAR, parameters) + for adults, dependants, _ in [family for family, _ in drawn] + ] sim = Simulation( situation=situation( - without + with_extra, capital_as_savings, esa_income, jsa_income + without + with_extra, + capital_as_savings, + esa_income, + jsa_income, + joint_claim * 2, ) ) eligible = sim.calculate("income_support_eligible", YEAR) @@ -287,7 +308,7 @@ def income_related_award(reported, capital, rules): def reference_eligibility( - adults, dependants, capital, esa_income, jsa_income, sp_age, parameters + adults, dependants, capital, esa_income, jsa_income, sp_age, parameters, extra ): """The model's Income Support gate, read family by family.""" IS = parameters.gov.dwp.income_support @@ -320,6 +341,8 @@ def is_claimant(i): and not any(works(other, WORK.partner_hours) for other in others) ) + # The awards on the claimant's and partner's reports. Members outside the + # family are not candidates when either of them reports one. if esa_income is not None: income_related_esa = esa_income > 0 else: @@ -327,12 +350,21 @@ 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) if jsa_income is not None: income_based_jsa = jsa_income > 0 else: - income_based_jsa = JSA.active and income_related_award( - sum(a["jsa_income_reported"] for a in adults), capital, JSA.capital + # A child placed with the couple ends a joint claim (reg 3A(1)). + family_has_child = bool(dependants) or bool( + extra and extra.get("is_looked_after_by_local_authority") + ) + joint_claim = jsa_joint_claim(adults, family_has_child, YEAR, parameters) + income_based_jsa = ( + JSA.active + and income_related_award( + sum(a["jsa_income_reported"] for a in adults), capital, JSA.capital + ) + and jsa_screen(adults, [], capital, joint_claim, parameters) ) return ( any(is_claimant(i) for i in range(len(adults))) @@ -365,6 +397,7 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): None if jsa_income is None else jsa_income[i], sp_age[start : start + len(adults)], parameters, + extra, ) assert eligible[i] == expected, units[i] start += len(adults) + len(dependants) + 1 @@ -385,7 +418,8 @@ def more_work_or_jsa(draw, adults): @given(st.lists(families(), min_size=1, max_size=8), st.data()) def test_more_work_or_jsa_never_makes_a_family_eligible(drawn, data): """(c) and (f) only ever bar a claim: raising any claimant's or partner's - hours or JSA cannot turn an ineligible family eligible.""" + hours or JSA cannot turn an ineligible family eligible, unless the work + ends an income-related ESA or income-based JSA award that barred it.""" capital_as_savings, esa_income, jsa_income = data.draw(input_settings(len(drawn))) jsa_income = None if jsa_income is None else jsa_income * 2 more = [ @@ -406,5 +440,19 @@ def test_more_work_or_jsa_never_makes_a_family_eligible(drawn, data): event("some family eligible before") if (eligible[: len(drawn)] & ~eligible[len(drawn) :]).any(): event("more work or JSA removed eligibility") - for i in range(len(drawn)): - assert not (eligible[len(drawn) + i] and not eligible[i]), (drawn[i], more[i]) + n = len(drawn) + lost_award = np.zeros(n, dtype=bool) + for variable, direct in [ + ("esa_income_eligible", esa_income), + ("jsa_income_eligible", jsa_income), + ]: + if direct is None: + screen = sim.calculate(variable, YEAR) + lost_award |= screen[:n] & ~screen[n:] + if (eligible[n:] & ~eligible[:n] & lost_award).any(): + event("more work ended an award that barred Income Support") + for i in range(n): + assert lost_award[i] or not (eligible[n + i] and not eligible[i]), ( + drawn[i], + more[i], + ) diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py new file mode 100644 index 000000000..8810de23c --- /dev/null +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -0,0 +1,242 @@ +"""Properties of the income-related ESA and income-based JSA work conditions. + +Welfare Reform Act 2007 Sch 1 para 6(1)(e) and (f) and Jobseekers Act 1995 +ss.1(2)(e), 1(2B)(b) and 3(1)(e) bar an award when the claimant, or the other +member of the couple, is engaged in remunerative work. For every family: + +- esa_income_eligible and jsa_income_eligible equal a family-by-family + reading of the law (legacy_award_work_reference), and is_jsa_joint_claim_couple + equals the reading of s.1(4) and JSA Regs reg 3A(1); +- more hours or more pay for anyone never makes a family eligible, because + the conditions only ever bar a claim; +- caring never ends an ESA award (a carer partner is not treated as in + remunerative work, ESA Regs reg 43(2)(c)) and never matters for JSA (the + JSA regulations have no carer exception); +- when the claimant or partner reports the award, adding a member outside + the family (a non-dependent adult, with any award and any work) never + changes either screen. + +The reference covers employees with no other income and pay within the basic +rate band. Roles are given explicitly (is_claimant_or_partner). Each example +builds many families in one simulation, in separate households and benefit +units. +""" + +import numpy as np +from hypothesis import HealthCheck, event, given, settings +from hypothesis import strategies as st + +from legacy_award_work_reference import esa_screen, jsa_joint_claim, jsa_screen +from policyengine_uk import Simulation + +# 0, 15, 16, 20, 23, 24 and 40 hours a week. +HOURS = [0, 0, 780, 832, 1_040, 1_196, 1_248, 2_080] +# £0, £20 (the lower limit), £21, £100, £195.50 (the 2025-26 higher limit), +# £196, £203.50 (2026-27), £204 and £300 a week. +PAY = [0, 0, 1_040, 1_092, 5_200, 10_166, 10_192, 10_582, 10_608, 15_600] + + +@st.composite +def adults(draw, outside_family=False): + pay = draw(st.sampled_from(PAY)) + adult = { + "age": draw( + st.one_of( + st.integers(18 if not outside_family else 20, 64), st.integers(70, 90) + ) + ), + "esa_income_reported": draw(st.sampled_from([0, 0, 3_000])), + "jsa_income_reported": draw(st.sampled_from([0, 0, 3_000])), + "hours_worked": draw(st.sampled_from(HOURS)), + "employment_income": pay, + "employee_pension_contributions": ( + draw(st.sampled_from([0, 0, 520])) if pay <= 12_570 else 0 + ), + "receives_carer_benefit": draw(st.booleans()), + "care_hours": draw(st.sampled_from([0, 0, 35])), + } + if outside_family: + adult["current_education"] = "NOT_IN_EDUCATION" + return adult + + +@st.composite +def families(draw): + """A claimant, an optional partner, children, an optional child placed by a + local authority, an optional member outside the family, and capital.""" + couple = [draw(adults())] + if draw(st.booleans()): + couple.append(draw(adults())) + children = [ + {"age": draw(st.integers(0, 15))} for _ in range(draw(st.integers(0, 2))) + ] + if draw(st.integers(0, 4)) == 0: + children.append( + { + "age": draw(st.integers(0, 15)), + "is_looked_after_by_local_authority": True, + } + ) + others = [draw(adults(outside_family=True))] if draw(st.booleans()) else [] + capital = draw(st.sampled_from([0, 0, 6_250, 16_000, 20_000])) + return couple, children, others, capital + + +def situation(units, year): + people, benunits, households = {}, {}, {} + for i, (couple, children, others, capital) in enumerate(units): + members = ( + [(m, True) for m in couple] + + [(m, False) for m in children] + + [(m, False) for m in others] + ) + names = [] + for j, (inputs, claimant_or_partner) in enumerate(members): + name = f"p{i}_{j}" + people[name] = {k: {year: v} for k, v in inputs.items()} + people[name]["is_claimant_or_partner"] = {year: claimant_or_partner} + people[name]["is_parent"] = {year: claimant_or_partner and bool(children)} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "esa_income_assessable_capital": {year: capital}, + "jsa_income_assessable_capital": {year: capital}, + } + households[f"h{i}"] = {"members": names} + return {"people": people, "benunits": benunits, "households": households} + + +def label(units): + adults_ = [a for couple, _, others, _ in units for a in couple + others] + for program in ["esa", "jsa"]: + if any(a[f"{program}_income_reported"] for a in adults_): + event(f"someone reports {program}") + if any(len(couple) == 2 and not children for couple, children, _, _ in units): + event("couple without children") + if any( + c.get("is_looked_after_by_local_authority") for _, cs, _, _ in units for c in cs + ): + event("placed child") + if any(others for *_, others, _ in units): + event("member outside the family") + + +SETTINGS = settings( + max_examples=40, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +FAMILIES = st.lists(families(), min_size=1, max_size=10) +YEARS = st.sampled_from([2025, 2026]) + + +@SETTINGS +@given(FAMILIES, YEARS) +def test_screens_match_a_family_by_family_reading(units, year): + label(units) + sim = Simulation(situation=situation(units, year)) + esa = sim.calculate("esa_income_eligible", year) + jsa = sim.calculate("jsa_income_eligible", year) + joint = sim.calculate("is_jsa_joint_claim_couple", year) + if esa.any(): + event("some ESA award passes") + if jsa.any(): + event("some JSA award passes") + parameters = sim.tax_benefit_system.parameters(year) + for i, (couple, children, others, capital) in enumerate(units): + expected_joint = jsa_joint_claim(couple, bool(children), year, parameters) + assert joint[i] == expected_joint, units[i] + assert esa[i] == esa_screen(couple, others, capital, parameters), units[i] + assert jsa[i] == jsa_screen( + couple, others, capital, expected_joint, parameters + ), units[i] + + +@st.composite +def more_work(draw, family): + """The same family with more hours or more pay for anyone.""" + couple, children, others, capital = family + + def more(adult): + adult = dict(adult) + adult["hours_worked"] += draw(st.sampled_from([0, 52, 260, 832, 1_248])) + adult["employment_income"] += draw(st.sampled_from([0, 52, 1_040, 5_200])) + return adult + + return [more(a) for a in couple], children, [more(a) for a in others], capital + + +@SETTINGS +@given(FAMILIES, YEARS, st.data()) +def test_more_work_never_makes_a_family_eligible(units, year, data): + more = [data.draw(more_work(family)) for family in units] + sim = Simulation(situation=situation(units + more, year)) + n = len(units) + for variable in ["esa_income_eligible", "jsa_income_eligible"]: + eligible = sim.calculate(variable, year) + if (eligible[:n] & ~eligible[n:]).any(): + event(f"more work removed {variable}") + assert not (eligible[n:] & ~eligible[:n]).any(), variable + + +@SETTINGS +@given(FAMILIES, YEARS) +def test_caring_never_ends_esa_and_never_matters_for_jsa(units, year): + def caring(family): + couple, children, others, capital = family + return ( + [dict(a, receives_carer_benefit=True) for a in couple], + children, + [dict(a, receives_carer_benefit=True) for a in others], + capital, + ) + + def not_caring(family): + couple, children, others, capital = family + return ( + [dict(a, receives_carer_benefit=False, care_hours=0) for a in couple], + children, + [dict(a, receives_carer_benefit=False, care_hours=0) for a in others], + capital, + ) + + n = len(units) + sim = Simulation( + situation=situation( + [not_caring(u) for u in units] + [caring(u) for u in units], year + ) + ) + esa = sim.calculate("esa_income_eligible", year) + jsa = sim.calculate("jsa_income_eligible", year) + if (esa[n:] & ~esa[:n]).any(): + event("a carer partner kept an ESA award") + assert not (esa[:n] & ~esa[n:]).any() + assert (jsa[:n] == jsa[n:]).all() + + +@SETTINGS +@given(FAMILIES, YEARS, st.data()) +def test_member_outside_the_family_never_changes_the_couples_screen(units, year, data): + extras = [data.draw(adults(outside_family=True)) for _ in units] + without = [ + (couple, children, [], capital) for couple, children, _, capital in units + ] + with_extra = [ + (couple, children, [extra], capital) + for (couple, children, _, capital), extra in zip(units, extras) + ] + n = len(units) + sim = Simulation(situation=situation(without + with_extra, year)) + flags = sim.calculate("is_claimant_or_partner", year) + sizes = [len(c) + len(ch) + len(o) for c, ch, o, _ in without + with_extra] + ends = np.cumsum(sizes) + for variable, reported in [ + ("esa_income_eligible", "esa_income_reported"), + ("jsa_income_eligible", "jsa_income_reported"), + ]: + eligible = sim.calculate(variable, year) + for i, (couple, *_rest) in enumerate(without): + assert not flags[ends[n + i] - 1] + if any(a[reported] > 0 for a in couple): + assert eligible[i] == eligible[n + i], (variable, units[i], extras[i]) diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py new file mode 100644 index 000000000..32e4d2b77 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -0,0 +1,77 @@ +from policyengine_uk.model_api import * + + +class esa_exempt_work_earnings(Variable): + value_type = float + entity = Person + label = "Weekly earnings for the ESA exempt work limits" + documentation = ( + "A claimant's earnings in a week, which ESA Regs 2008 reg 45(2) and " + "(4) compare with the exempt work earnings limits. They are worked " + "out under the rules for calculating earnings for income-related ESA " + "(DMG 41189; regs 92 to 99): gross employment and self-employment " + "income, less income tax and National Insurance on it and half of " + "the person's own pension contributions (regs 96(3) and 98(4)), " + "divided by 52. The income tax deducted is the tax on earnings taken " + "as the lowest slice of the person's non-savings income, so tax on a " + "pension, a taxable benefit, property, savings or dividends never " + "comes off earnings; that is the reading most favourable to the " + "claimant, where PAYE on a second job would take tax at the basic " + "rate. National Insurance is the Class 1 primary, Class 2 and Class " + "4 contributions. Enter this variable directly for other earnings, " + "for example where the regulations' averaging periods give a " + "different figure." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2008/794/regulation/45", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/96", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/98", + "https://assets.publishing.service.gov.uk/media/689db34d1fedc616bb13399c/dmg-ch-41.pdf", + ) + + def formula(person, period, parameters): + income_tax = parameters(period).gov.hmrc.income_tax + gross = add(person, period, ["employment_income", "self_employment_income"]) + national_insurance = add( + person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] + ) + # Income tax on earnings, with earnings as the lowest slice of + # non-savings income: the part of earned_taxable_income left after + # the person's other non-savings income (pensions, taxable benefits) + # is taken off the top. earned_taxable_income is adjusted net income + # less the incomes and allowances on the exclusions list. + exclusions = list(income_tax.earned_taxable_income_exclusions) + excluded_incomes = [ + variable + for variable in exclusions + if variable in income_tax.adjusted_net_income_components + ] + taxable_earnings_components = [ + variable + for variable in [ + "taxable_employment_income", + "taxable_self_employment_income", + ] + if variable not in exclusions + ] + earnings_in_base = add(person, period, taxable_earnings_components) + income_in_base = person("adjusted_net_income", period) - add( + person, period, excluded_incomes + ) + other_income = max_(0, income_in_base - earnings_in_base) + taxable_earnings = max_( + 0, person("earned_taxable_income", period) - other_income + ) + rates = income_tax.rates + tax_before_reductions = where( + person("pays_scottish_income_tax", period), + rates.scotland.rates.calc(taxable_earnings), + rates.uk.calc(taxable_earnings), + ) + reductions = add(person, period, income_tax.income_tax_subtractions) + tax = max_(0, tax_before_reductions - reductions) + pension = person("pension_contributions", period) / 2 + net = max_(0, gross - national_insurance - tax - pension) + return net / WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py b/policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py new file mode 100644 index 000000000..1ebc6830d --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py @@ -0,0 +1,50 @@ +from policyengine_uk.model_api import * + + +class esa_income_claimant_remunerative_work(Variable): + value_type = bool + entity = Person + label = "Engaged in remunerative work as an income-related ESA claimant" + documentation = ( + "Whether the person, as the claimant of income-related ESA, is " + "engaged in remunerative work (Welfare Reform Act 2007 Sch 1 para " + "6(1)(e)). For a claimant that is any work done for payment or in " + "expectation of payment, whatever the hours, other than the work reg " + "40(2) lists (ESA Regs 2008 reg 41(1)). The model applies the exempt " + "work in reg 40(2)(f) that its inputs can identify: work with " + "earnings of no more than the lower limit in any week, whatever the " + "hours (reg 45(2)), and work of less than 16 hours a week with " + "earnings of no more than the higher limit (reg 45(4)). Before 3 " + "April 2017, reg 45(4) also limited such work to a 52-week period " + "unless the claimant had limited capability for work-related " + "activity; that limit is not modelled. The other reg 40(2) work is " + "not identified: a councillor's work, tribunal duties, domestic tasks " + "at home or caring for a relative, placement care, emergency duties, " + "and the other exempt work (supervised work within the higher limit " + "whatever the hours, test trading, voluntary work and work " + "placements, reg 45(3), (5) to (7)). Nor are the reg 41(2) periods " + "after work ends for which final earnings are taken into account. " + "Enter this variable directly for those cases. Unlike Income Support " + "and the partner's test, being a carer does not take a claimant out " + "of remunerative work." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/40", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/41", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/45", + ) + + def formula(person, period, parameters): + exempt_work = parameters(period).gov.dwp.ESA.exempt_work + earnings = person("esa_exempt_work_earnings", period) + hours = person("esa_income_remunerative_work_hours", period) + # Reg 45(2): earnings of no more than the lower limit (with no + # earnings, any hours are unpaid or within it). + within_lower_limit = earnings <= exempt_work.lower_earnings_limit + # Reg 45(4): permitted work. + permitted_work = (hours < exempt_work.hours_limit) & ( + earnings <= exempt_work.higher_earnings_limit + ) + return ~(within_lower_limit | permitted_work) diff --git a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py index 481079249..514fed505 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py @@ -4,15 +4,59 @@ class esa_income_eligible(Variable): value_type = bool entity = BenUnit - label = "Whether eligible for income-related ESA after the capital test" + label = ( + "Whether a reported income-related ESA award passes the capital and work tests" + ) documentation = ( - "Bounded capital-rule screen applied to reported income-related ESA " - "awards. This is not a full entitlement model." + "Bounded screen applied to reported income-related ESA awards: the " + "capital test (Welfare Reform Act 2007 Sch 1 para 6(1)(b); ESA Regs " + "2008 reg 110) and the remunerative work conditions. The claimant " + "must not be engaged in remunerative work (para 6(1)(e); " + "esa_income_claimant_remunerative_work), and must not be a member of " + "a couple the other member of which is (para 6(1)(f)): paid work of " + "24 hours a week or more (reg 42(1)), unless the partner is a carer " + "within IS Regs 1987 Sch 1B para 4 (reg 43(2)(c)), which the model " + "reads as is_carer_for_benefits. The claimant is a member who " + "reports the award, tested against the other member of the couple. " + "When the claimant or partner reports one, only they are candidates: " + "a member outside the family, such as a non-dependent adult, claims " + "in their own right, and is tested on their own work only when " + "neither the claimant nor the partner reports an award. This is not " + "a full entitlement model." ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/2007/5/schedule/1/paragraph/6", + "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", + ) def formula(benunit, period, parameters): ESA = parameters(period).gov.dwp.ESA.income + person = benunit.members capital = benunit("esa_income_assessable_capital", period) - reported_award = add(benunit, period, ["esa_income_reported"]) > 0 - return reported_award & (capital <= ESA.capital.limit) + reports_award = person("esa_income_reported", period) > 0 + claimant_or_partner = person("is_claimant_or_partner", period) + # The candidate claimants: the claimant or partner who reports the + # award, or, if neither does, whoever in the benefit unit does. + couple_reports = benunit.any(reports_award & claimant_or_partner) + candidate = reports_award & ( + claimant_or_partner | ~benunit.project(couple_reports) + ) + # 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 + # hours a week or more; reg 43(2)(c): not if a carer. + hours = person("esa_income_remunerative_work_hours", period) + works_as_partner = ( + claimant_or_partner + & ~person("is_carer_for_benefits", period) + & (hours >= ESA.remunerative_work.partner_hours) + ) + 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) diff --git a/policyengine_uk/variables/gov/dwp/esa_income_remunerative_work_hours.py b/policyengine_uk/variables/gov/dwp/esa_income_remunerative_work_hours.py new file mode 100644 index 000000000..cf50e2251 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_income_remunerative_work_hours.py @@ -0,0 +1,42 @@ +from policyengine_uk.model_api import * + + +class esa_income_remunerative_work_hours(Variable): + value_type = float + entity = Person + label = "Weekly hours of paid work for the income-related ESA work tests" + documentation = ( + "Hours a week of work for which payment is made or which is done in " + "expectation of payment, averaged where they fluctuate. For a " + "claimant they decide whether work is permitted work (ESA Regs 2008 " + "reg 45(4), less than 16 hours); for a partner, whether the partner " + "is engaged in remunerative work (reg 42(1), 24 hours or more). Reg " + "45(8) and (9), which reg 42(2) applies to a partner, take the hours " + "expected in a week or, where they fluctuate, the average over a " + "complete recognisable cycle or the five weeks before the claim (or " + "another period that gives a more accurate average), and count paid " + "meal breaks. The model takes them to be weekly_hours, a usual week's " + "hours in paid jobs (the Enhanced FRS records usual weekly hours " + "across current jobs); for such work enter this variable directly. " + "A partner's hours in the employments reg 43(1) lists do not count " + "(reg 42(6)): child minding at home, charitable or voluntary work " + "paid only expenses, a training allowance scheme, the self-employment " + "route, part-time firefighting, coastguard, lifeboat and reserve " + "forces duties, a councillor's work, foster and other placement care, " + "and sports award activity; nor does a day of maternity, paternity, " + "shared parental, parental bereavement, adoption or neonatal care " + "leave or illness (reg 43(3)). The model's inputs do not identify " + "those employments or days, so enter the hours directly to apply " + "them. Reg 43(2)(c), which takes a carer partner out of remunerative " + "work, is applied in esa_income_eligible." + ) + definition_period = YEAR + unit = "hour" + reference = ( + "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/45", + ) + + def formula(person, period, parameters): + return person("weekly_hours", period) diff --git a/policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py b/policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py new file mode 100644 index 000000000..1ec04ef18 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py @@ -0,0 +1,51 @@ +from policyengine_uk.model_api import * + + +class is_jsa_joint_claim_couple(Variable): + value_type = bool + entity = BenUnit + label = "Joint-claim couple for income-based JSA" + documentation = ( + "Whether the claimant and partner are a joint-claim couple, who " + "claim income-based Jobseeker's Allowance together, so that each of " + "them is a claimant (Jobseekers Act 1995 ss.1(2B) and 35(1)). A " + "joint-claim couple is a couple with no child or young person in the " + "family for whom one of them is entitled to Child Benefit (s.1(4)), " + "nor a child or young person they care for in the circumstances of " + "JSA Regs 1996 reg 78(4) or (8), at least one of whom is 18 or over " + "and was born after the prescribed date (reg 3A(1)). Joint claims " + "began on 19 March 2001. The model reads a child or young person as " + "a Child Benefit child or qualifying young person in the benefit " + "unit other than the claimant and partner, including a child placed " + "with them. The circumstances in which one member of a joint-claim " + "couple may claim alone (regs 3D and 3E) are not modelled; enter this " + "variable directly for them." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1995/18/section/1", + "https://www.legislation.gov.uk/ukpga/1995/18/section/35", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/3A", + ) + + def formula(benunit, period, parameters): + joint_claim = parameters(period).gov.dwp.JSA.income.joint_claim + if not joint_claim.in_effect: + return benunit.empty_array() > 0 + person = benunit.members + claimant_or_partner = person("is_claimant_or_partner", period) + couple = benunit.sum(claimant_or_partner) == 2 + child_or_young_person = ( + person("is_child_or_qualifying_young_person_for_child_benefit", period) + & ~claimant_or_partner + ) + prescribed_member = ( + claimant_or_partner + & (person("age", period) >= 18) + & (person("birth_year", period) > joint_claim.born_after_year) + ) + return ( + couple + & ~benunit.any(child_or_young_person) + & benunit.any(prescribed_member) + ) diff --git a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py index 30f366cd5..e5b10619a 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py @@ -4,15 +4,63 @@ class jsa_income_eligible(Variable): value_type = bool entity = BenUnit - label = "Whether eligible for income-based JSA after the capital test" + label = ( + "Whether a reported income-based JSA award passes the capital and work tests" + ) documentation = ( - "Bounded capital-rule screen applied to reported income-based JSA awards. " - "This is not a full entitlement model." + "Bounded screen applied to reported income-based JSA awards: the " + "capital test (Jobseekers Act 1995 s.13(1) and (2A); JSA Regs 1996 reg 107) " + "and the remunerative work conditions. The claimant must not be " + "engaged in remunerative work, paid work of 16 hours a week or more " + "(s.1(2)(e); reg 51(1)(a)). In a joint-claim couple both members are " + "claimants, so neither may work 16 hours (s.1(2B)(b)); otherwise the " + "claimant's partner must not work 24 hours or more (s.3(1)(e); reg " + "51(1)(b)). Unlike Income Support and income-related ESA, the JSA " + "regulations do not take a carer out of remunerative work. The " + "claimant is a member who reports the award. When the claimant or " + "partner reports one, only they are candidates: a member outside the " + "family, such as a non-dependent adult, claims in their own right, " + "and is tested on their own work only when neither the claimant nor " + "the partner reports an award. This is not a full entitlement model." ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ukpga/1995/18/section/1", + "https://www.legislation.gov.uk/ukpga/1995/18/section/3", + "https://www.legislation.gov.uk/ukpga/1995/18/section/13", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/51", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/107", + ) def formula(benunit, period, parameters): - JSA = parameters(period).gov.dwp.JSA.income + JSA = parameters(period).gov.dwp.JSA + person = benunit.members capital = benunit("jsa_income_assessable_capital", period) - reported_award = add(benunit, period, ["jsa_income_reported"]) > 0 - return reported_award & (capital <= JSA.capital.limit) + reports_award = person("jsa_income_reported", period) > 0 + claimant_or_partner = person("is_claimant_or_partner", period) + # The candidate claimants: the claimant or partner who reports the + # award, or, if neither does, whoever in the benefit unit does. + couple_reports = benunit.any(reports_award & claimant_or_partner) + candidate = reports_award & ( + claimant_or_partner | ~benunit.project(couple_reports) + ) + WORK = JSA.remunerative_work + hours = person("jsa_remunerative_work_hours", period) + # s.1(2)(e) and reg 51(1)(a): 16 hours for a claimant, including each + # member of a joint-claim couple. + works_as_claimant = hours >= WORK.claimant_hours + # s.3(1)(e) and reg 51(1)(b): 24 hours for the partner, outside a + # joint claim. + joint_claim = benunit.project(benunit("is_jsa_joint_claim_couple", period)) + works_as_other_member = claimant_or_partner & where( + joint_claim, works_as_claimant, hours >= WORK.partner_hours + ) + other_member_works = claimant_or_partner & ( + ( + benunit.project(benunit.sum(works_as_other_member)) + - works_as_other_member + ) + > 0 + ) + claimant = candidate & ~works_as_claimant & ~other_member_works + return benunit.any(claimant) & (capital <= JSA.income.capital.limit) diff --git a/policyengine_uk/variables/gov/dwp/jsa_remunerative_work_hours.py b/policyengine_uk/variables/gov/dwp/jsa_remunerative_work_hours.py new file mode 100644 index 000000000..8bbd2b3ce --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/jsa_remunerative_work_hours.py @@ -0,0 +1,42 @@ +from policyengine_uk.model_api import * + + +class jsa_remunerative_work_hours(Variable): + value_type = float + entity = Person + label = "Weekly hours of paid work for the JSA remunerative work test" + documentation = ( + "Hours a week of work for which payment is made or which is done in " + "expectation of payment, averaged where they fluctuate (JSA Regs " + "1996 reg 51(1) and (2)). The model takes them to be weekly_hours, a " + "usual week's hours in paid jobs (the Enhanced FRS records usual " + "weekly hours across current jobs). Reg 51(2) instead takes the " + "hours expected in a week or, where they fluctuate, the average over " + "a complete recognisable cycle or the five weeks before the claim " + "(or another period that gives a more accurate average), and reg " + "51(3)(a) counts paid meal breaks; for such work enter this variable " + "directly. Hours in the employments and schemes reg 53(a) to (h) " + "lists do not count (reg 51(3)(b)): charitable or voluntary work " + "paid only expenses, a training allowance scheme, the self-employment " + "route, employment while living in a care home and needing personal " + "care, part-time firefighting, coastguard, lifeboat and reserve " + "forces duties, a councillor's duties, foster and other placement " + "care, a partner's trade dispute, and work in which a disability " + "cuts earnings or hours to 75 per cent or less of a comparable " + "worker's. Reg 53 also takes sports award activity and the Work " + "Experience and other employment schemes out of remunerative work, " + "and reg 52(1) keeps a person absent without good reason or on " + "holiday in it. The model's inputs identify none of these, so enter " + "the hours directly to apply them. Hours of unpaid caring " + "(reg 51(3)(c)) are not paid work and are not in weekly_hours." + ) + definition_period = YEAR + unit = "hour" + reference = ( + "https://www.legislation.gov.uk/uksi/1996/207/regulation/51", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/52", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/53", + ) + + def formula(person, period, parameters): + return person("weekly_hours", period) From 2c5da9868636cc6662920494f1df01a31ea02233 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 01:44:28 -0400 Subject: [PATCH 02/11] Test the ESA exempt-work limits against 16 x NMW and date the work parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stored higher limit equals 16 x the National Minimum Wage rounded up to the next 50p (ESA Regs 2008 reg 45(9A)) at every rate in DMG ch. 41 Appendix 5, and DWP's published £183.50, £195.50 and £203.50. The JSA hours parameters match the older gov.dwp.JSA.hours ones. The new property tests run 20 examples each. Co-Authored-By: Claude Opus 5.5 --- .../test_legacy_award_work_properties.py | 2 +- .../tests/test_legacy_benefit_parameters.py | 81 +++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 8810de23c..7036eadea 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -122,7 +122,7 @@ def label(units): SETTINGS = settings( - max_examples=40, + max_examples=20, deadline=None, derandomize=True, suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], diff --git a/policyengine_uk/tests/test_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index ae1a373b6..bad8ed7dd 100644 --- a/policyengine_uk/tests/test_legacy_benefit_parameters.py +++ b/policyengine_uk/tests/test_legacy_benefit_parameters.py @@ -1,3 +1,5 @@ +import math + from policyengine_uk import CountryTaxBenefitSystem @@ -30,3 +32,82 @@ def test_income_support_remunerative_work_hours_by_date(): for date, (claimant, partner) in expected.items(): assert work.claimant_hours(date) == claimant, date assert work.partner_hours(date) == partner, date + + +def _round_up_to_50p(amount): + # ESA Regs 2008 reg 45(9A): part of a pound under 50p rounds up to 50p, + # and over 50p up to the next pound. + return math.ceil(round(amount * 2, 6)) / 2 + + +# The National Minimum Wage rate that reg 45 uses (reg 2(1): the reg 11 NMW +# Regulations 1999 rate, now the National Living Wage), from DMG ch. 41 +# Appendix 5 and ADM ch. V3; April 2026 from DWP's 2026-27 rates. +NMW_RATES = { + "2010-10-01": 5.93, + "2011-10-01": 6.08, + "2012-10-01": 6.19, + "2013-10-01": 6.31, + "2014-10-01": 6.50, + "2015-10-01": 6.70, + "2016-04-01": 7.20, + "2017-04-01": 7.50, + "2018-04-01": 7.83, + "2019-04-01": 8.21, + "2020-04-01": 8.72, + "2021-04-01": 8.91, + "2022-04-01": 9.50, + "2023-04-01": 10.42, + "2024-04-01": 11.44, + "2025-04-01": 12.21, + "2026-04-01": 12.71, +} + + +def _rate_in_force(on): + return [rate for start, rate in sorted(NMW_RATES.items()) if start <= on][-1] + + +def test_esa_exempt_work_higher_limit_is_16_x_nmw_rounded_up(): + # Reg 45(4): £92 as made, £93 from 1 October 2009, then 16 x NMW from 11 + # April 2011 (SI 2011/674), rounded up under reg 45(9A). + limit = CountryTaxBenefitSystem().parameters.gov.dwp.ESA.exempt_work + limit = limit.higher_earnings_limit + assert limit("2008-10-27") == 92 + assert limit("2009-09-30") == 92 + assert limit("2009-10-01") == 93 + assert limit("2011-04-10") == 93 + # Before 2015 parameters keep their statutory dates. + for on in ["2011-04-11", "2011-10-01", "2012-10-01", "2013-10-01", "2014-10-01"]: + assert limit(on) == _round_up_to_50p(16 * _rate_in_force(on)), on + # From 2015 each year takes the value on 30 April (fiscal-year parameters). + for year in range(2015, 2027): + expected = _round_up_to_50p(16 * _rate_in_force(f"{year}-04-30")) + assert limit(str(year)) == expected, year + # DWP's published limits. + assert limit("2024") == 183.5 + assert limit("2025") == 195.5 + assert limit("2026") == 203.5 + + +def test_esa_and_jsa_remunerative_work_parameters_by_date(): + dwp = CountryTaxBenefitSystem().parameters.gov.dwp + exempt_work = dwp.ESA.exempt_work + assert exempt_work.lower_earnings_limit("2008-10-27") == 20 + assert exempt_work.lower_earnings_limit("2026") == 20 + assert exempt_work.hours_limit("2008-10-27") == 16 + assert dwp.ESA.income.remunerative_work.partner_hours("2008-10-27") == 24 + work = dwp.JSA.remunerative_work + for on in ["1996-10-07", "2001-03-19", "2025"]: + assert work.claimant_hours(on) == 16, on + assert work.partner_hours(on) == 24, on + # The older parameters say the same thing. + for year in range(2015, 2031): + assert dwp.JSA.hours.single(str(year)) == work.claimant_hours(str(year)) + assert dwp.JSA.hours.couple(str(year)) == work.partner_hours(str(year)) + joint_claim = dwp.JSA.income.joint_claim + assert not joint_claim.in_effect("2001-03-18") + assert joint_claim.in_effect("2001-03-19") + assert joint_claim.born_after_year("2001-03-19") == 1976 + assert joint_claim.born_after_year("2002-10-28") == 1957 + assert joint_claim.born_after_year("2008-02-25") == 1947 From d146b96b5e29b9926998955732fa252b32bf8c5c Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 03:01:20 -0400 Subject: [PATCH 03/11] Apply review r1: reg 3E single claims, reg 98/99 self-employment, rounding - JSA: a member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (JSA Regs 1996 reg 3E(1), (2)(g), since 19 March 2001), so the other member's limit is 24 hours for every couple. The joint-claim switch, is_jsa_joint_claim_couple and its parameters are removed. - ESA earnings: pay under reg 96(3); self-employment under reg 98(3) and reg 99 (notional basic-rate tax above the personal allowance, main-rate Class 4, half the personal pension premium); a self-employment loss is no longer set against pay (reg 98(11)). The documentation calls the PAYE attribution an approximation, not the reading most favourable to the claimant. - The projected higher earnings limit rounds up to 50p (reg 45(9A)). - The Income Support monotonicity property allows a gain only where a calculated esa_income or jsa_income award actually ended. - esa_income and jsa_income documentation names the work tests. - YAML: two self-employment cases, JSA couples without children at 15, 16, 20, 23 and 24 hours, and a couple with a placed child. Co-Authored-By: Claude Opus 5.5 --- .../esa-jsa-remunerative-work.fixed.md | 6 +- .../exempt_work/higher_earnings_limit.yaml | 9 +- .../parameters/gov/dwp/JSA/hours/couple.yaml | 2 +- .../income/joint_claim/born_after_year.yaml | 23 ---- .../dwp/JSA/income/joint_claim/in_effect.yaml | 16 --- .../JSA/remunerative_work/claimant_hours.yaml | 2 +- .../JSA/remunerative_work/partner_hours.yaml | 4 +- .../tests/legacy_award_work_reference.py | 80 +++++++----- .../esa_income_remunerative_work.yaml | 60 ++++++++- .../jsa_income_remunerative_work.yaml | 87 +++++++------ ...t_income_support_eligibility_properties.py | 26 ++-- .../test_legacy_award_work_properties.py | 56 ++++++--- .../tests/test_legacy_benefit_parameters.py | 11 +- .../gov/dwp/esa_exempt_work_earnings.py | 116 +++++++++++------- .../variables/gov/dwp/esa_income.py | 5 +- .../gov/dwp/is_jsa_joint_claim_couple.py | 51 -------- .../variables/gov/dwp/jsa_income.py | 6 +- .../variables/gov/dwp/jsa_income_eligible.py | 50 ++++---- 18 files changed, 325 insertions(+), 285 deletions(-) delete mode 100644 policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml delete mode 100644 policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml delete mode 100644 policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py diff --git a/changelog.d/esa-jsa-remunerative-work.fixed.md b/changelog.d/esa-jsa-remunerative-work.fixed.md index 0f8218151..8f16a7b41 100644 --- a/changelog.d/esa-jsa-remunerative-work.fixed.md +++ b/changelog.d/esa-jsa-remunerative-work.fixed.md @@ -1,3 +1,3 @@ -- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage, rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)), on earnings net of tax, National Insurance and half the pension contributions (new `esa_exempt_work_earnings` and `esa_income_claimant_remunerative_work`). For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)). Hours come from the new `esa_income_remunerative_work_hours`. -- End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). In a joint-claim couple, a couple without children (s.1(4); reg 3A(1); new `is_jsa_joint_claim_couple`, from 19 March 2001), each member is a claimant, so neither may work 16 hours (s.1(2B)(b)). The JSA regulations have no carer exception. Hours come from the new `jsa_remunerative_work_hours`. -- The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours`, `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}` and `gov.dwp.JSA.income.joint_claim.{in_effect, born_after_year}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. +- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less income tax, primary Class 1 and half the pension contributions (reg 96(3)), plus a self-employed profit less reg 99's notional basic-rate tax and main-rate Class 4 and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. +- End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). A member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (reg 3E(2)(g)), so 24 hours is the limit for the other member of any couple. The JSA regulations have no carer exception. Hours come from the new `jsa_remunerative_work_hours`. +- The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours` and `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. diff --git a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml index c7960df11..7ab1f87bb 100644 --- a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml @@ -67,8 +67,13 @@ metadata: period: week label: ESA exempt work higher earnings limit # The National Living Wage is set with reference to earnings, so later - # years follow average earnings until the rate is announced. - uprating: gov.economic_assumptions.indices.obr.average_earnings + # years follow average earnings until the rate is announced, rounded up to + # the next 50p as reg. 45(9A) rounds 16 x NMW. + uprating: + parameter: gov.economic_assumptions.indices.obr.average_earnings + rounding: + type: upwards + interval: 0.5 reference: - title: Employment and Support Allowance Regulations 2008 reg. 45(4) and (9A) href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45 diff --git a/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml b/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml index 95bfccb93..a717adbcb 100644 --- a/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml +++ b/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml @@ -1,4 +1,4 @@ -description: Income-based Jobseeker's Allowance treats a claimant's partner as engaged in remunerative work at this many weekly hours of paid work or more, except in a joint claim, where each member is a claimant at the claimant's hours. Kept for existing users; the model reads gov.dwp.JSA.remunerative_work.partner_hours. +description: Income-based Jobseeker's Allowance treats a claimant's partner as engaged in remunerative work at this many weekly hours of paid work or more. Kept for existing users; the model reads gov.dwp.JSA.remunerative_work.partner_hours. values: 2015-04-01: value: 24 diff --git a/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml b/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml deleted file mode 100644 index 10a69a779..000000000 --- a/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/born_after_year.yaml +++ /dev/null @@ -1,23 +0,0 @@ -description: A couple without children is a joint-claim couple for income-based Jobseeker's Allowance if at least one member is 18 or over and was born after a date in this year (19 March 1976, then 28 October 1957, then 28 October 1947). The model compares birth years, so it treats a member born later in this year as born before the date. -values: - 2001-03-19: - value: 1976 - reference: - - title: Jobseeker's Allowance Regulations 1996 reg. 3A(1) (born after 19 March 1976) - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2001-03-19 - 2002-10-28: - value: 1957 - reference: - - title: Jobseeker's Allowance (Joint Claims) Amendment Regulations 2002 (SI 2002/1701) reg. 2(a) (born after 28 October 1957) - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2002-10-28 - 2008-02-25: - value: 1947 - reference: - - title: Jobseeker's Allowance (Joint Claims) Amendment Regulations 2008 (SI 2008/13) reg. 2(2) (born after 28 October 1947) - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2008-02-25 -metadata: - unit: year - label: Income-based JSA joint claim birth year cutoff - reference: - - title: Jobseeker's Allowance Regulations 1996 reg. 3A(1) - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A diff --git a/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml b/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml deleted file mode 100644 index 8682f300f..000000000 --- a/policyengine_uk/parameters/gov/dwp/JSA/income/joint_claim/in_effect.yaml +++ /dev/null @@ -1,16 +0,0 @@ -description: Whether couples without children of the prescribed description claim income-based Jobseeker's Allowance jointly, so that each member is a claimant. -values: - 1996-10-07: false - 2001-03-19: - value: true - reference: - - title: Jobseeker's Allowance (Joint Claims) Regulations 2000 (SI 2000/1978) reg. 2(2), inserting JSA Regs 1996 Part 1A - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A/2001-03-19 -metadata: - unit: bool - label: Income-based JSA joint claims in effect - reference: - - title: Jobseekers Act 1995 ss. 1(2B), 1(4) and 35(1) - href: https://www.legislation.gov.uk/ukpga/1995/18/section/1 - - title: Jobseeker's Allowance Regulations 1996 reg. 3A - href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3A diff --git a/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml index a2d87ee3a..ebf5b28a4 100644 --- a/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml +++ b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml @@ -1,4 +1,4 @@ -description: Jobseeker's Allowance treats a claimant, including each member of a joint-claim couple, as engaged in remunerative work at this many weekly hours of paid work or more. +description: Jobseeker's Allowance treats a claimant as engaged in remunerative work at this many weekly hours of paid work or more. values: 1996-10-07: value: 16 diff --git a/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml index 76a8d0d7b..122df194d 100644 --- a/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml +++ b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml @@ -1,4 +1,4 @@ -description: Income-based Jobseeker's Allowance treats a claimant's partner, outside a joint claim, as engaged in remunerative work at this many weekly hours of paid work or more. +description: Income-based Jobseeker's Allowance treats a claimant's partner as engaged in remunerative work at this many weekly hours of paid work or more. A member of a joint-claim couple whose partner works fewer hours than this, but 16 or more, may claim alone, so this limit decides whether a couple can have an award. values: 1996-10-07: value: 24 @@ -14,3 +14,5 @@ metadata: href: https://www.legislation.gov.uk/ukpga/1995/18/section/3 - title: Jobseeker's Allowance Regulations 1996 reg. 51(1)(b) href: https://www.legislation.gov.uk/uksi/1996/207/regulation/51 + - title: Jobseeker's Allowance Regulations 1996 reg. 3E(1) and (2)(g) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/3E diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index ebcc2b1bd..59faca23e 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -4,7 +4,8 @@ tests to compare with the model (test_legacy_award_work_properties.py and test_income_support_eligibility_properties.py). Each adult is a dict of simulation inputs for one year: hours_worked (annual), employment_income, -employee_pension_contributions, receives_carer_benefit, care_hours, age and +self_employment_income, employee_pension_contributions, +personal_pension_contributions, receives_carer_benefit, care_hours, age and the reported awards. - Capital (WRA 2007 Sch 1 para 6(1)(b), ESA Regs reg 110; JSA 1995 s.13(1), @@ -13,13 +14,18 @@ - ESA claimant (para 6(1)(e), ESA Regs reg 41(1)): paid work is remunerative unless it is exempt work: earnings of no more than £20 a week (reg 45(2)), or under 16 hours with earnings within the higher limit (reg 45(4)). - Earnings are net of income tax, National Insurance and half the pension - contributions (regs 96(3), 98(4)). + Pay is net of income tax, primary Class 1 and half the pension + contributions (reg 96(3)). Self-employment is the profit less a notional + basic-rate tax on the profit above the personal allowance, notional + main-rate Class 4 and half the personal pension contributions (reg 98(3), + reg 99); a loss is not set 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)). - JSA claimant (s.1(2)(e), reg 51(1)(a)): 16 hours or more, with no carer - exception. Each member of a joint-claim couple is a claimant (s.1(2B)(b)); - otherwise the partner's limit is 24 hours (s.3(1)(e), reg 51(1)(b)). + exception. The other member of the couple: 24 hours or more (s.3(1)(e), + reg 51(1)(b)). In a joint-claim couple each member is a claimant at 16 + hours (s.1(2B)(b)), but a member may claim alone when the other works 16 to + under 24 hours (reg 3E(1), (2)(g)), so the limit is 24 either way. - The claimant is a member who reports the award; when the claimant or partner reports one, only they are candidates. """ @@ -59,21 +65,43 @@ def weekly_hours(adult): def esa_weekly_earnings(adult, parameters): - """Net weekly earnings for the exempt work limits, for an employee with no - other income and earnings within the basic rate band.""" - gross = adult.get("employment_income", 0) - pension = adult.get("employee_pension_contributions", 0) + """Net weekly earnings for the exempt work limits. Covers pay within the + basic rate band, a self-employment profit within the Class 4 upper limit + or a loss, and no other income; Class 4 is for someone under state + pension age.""" hmrc = parameters.gov.hmrc allowance = hmrc.income_tax.allowances.personal_allowance.amount basic_rate = hmrc.income_tax.rates.uk.rates[0] - # Pension contributions are only used with pay below the personal - # allowance, so how they are relieved for tax does not arise. - assert pension == 0 or gross <= allowance, adult - tax = basic_rate * max(0, gross - allowance) + pay = adult.get("employment_income", 0) + profit = max(0, adult.get("self_employment_income", 0)) + personal_pension = adult.get("personal_pension_contributions", 0) + pension_from_profit = personal_pension if profit > 0 else 0 + pension_from_pay = ( + adult.get("employee_pension_contributions", 0) + + personal_pension + - pension_from_profit + ) + # Pension contributions from pay are only used with pay below the + # personal allowance, so how they are relieved for tax does not arise. + assert pension_from_pay == 0 or pay <= allowance, adult + tax_on_pay = basic_rate * max(0, pay - allowance) class_1 = hmrc.national_insurance.class_1 threshold = class_1.thresholds.primary_threshold * WEEKS - national_insurance = class_1.rates.employee.main * max(0, gross - threshold) - return max(0, gross - tax - national_insurance - pension / 2) / WEEKS + class_1_on_pay = class_1.rates.employee.main * max(0, pay - threshold) + net_pay = max(0, pay - tax_on_pay - class_1_on_pay - pension_from_pay / 2) + # Reg 99(1): basic rate on the profit less the personal allowance, whatever + # relief pension contributions attract. Reg 99(3)(b): main-rate Class 4. + notional_tax = basic_rate * max(0, profit - allowance) + class_4 = hmrc.national_insurance.class_4 + notional_class_4 = class_4.rates.main * max( + 0, + min(profit, class_4.thresholds.upper_profits_limit) + - class_4.thresholds.lower_profits_limit, + ) + net_profit = max( + 0, profit - notional_tax - notional_class_4 - pension_from_profit / 2 + ) + return (net_pay + net_profit) / WEEKS def esa_claimant_in_remunerative_work(adult, parameters): @@ -122,27 +150,13 @@ def other_member_works(i): return passes and capital <= ESA.capital.limit -def jsa_joint_claim(couple, family_has_child, year, parameters): - joint_claim = parameters.gov.dwp.JSA.income.joint_claim - return ( - bool(joint_claim.in_effect) - and len(couple) == 2 - and not family_has_child - and any( - adult["age"] >= 18 and year - adult["age"] > joint_claim.born_after_year - for adult in couple - ) - ) - - -def jsa_screen(couple, others, capital, joint_claim, parameters): - """jsa_income_eligible for a claimant and partner (couple), other members - of the benefit unit outside the family (others) and the couple's - joint-claim status.""" +def jsa_screen(couple, others, capital, parameters): + """jsa_income_eligible for a claimant and partner (couple) and other + members of the benefit unit outside the family (others).""" JSA = parameters.gov.dwp.JSA WORK = JSA.remunerative_work members = couple + others - other_limit = WORK.claimant_hours if joint_claim else WORK.partner_hours + other_limit = WORK.partner_hours def other_member_works(i): if i >= len(couple): diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index c66ca5ae6..adf8d2718 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -12,8 +12,11 @@ # earnings in any week do not exceed 16 x National Minimum Wage", # rounded up to the next 50p (reg 45(9A)): £195.50 in 2025-26 (16 x # £12.21 = £195.36) and £203.50 in 2026-27 (16 x £12.71 = £203.36). -# Earnings are net: less income tax, National Insurance and half the -# claimant's pension contributions (DMG 41189; regs 96(3), 98(4)). +# Earnings are net (DMG 41189). Pay, reg 96(3): less the income tax and +# primary Class 1 deducted and half the pension contributions. Self-employment, +# reg 98(3) and reg 99: less a notional basic-rate tax on the profit above the +# personal allowance, notional main-rate Class 4 and half the personal pension +# contributions; a loss is not set against other earnings (reg 98(11)). # Reg 42(1): for the partner, remunerative work is paid work "for not less # than 24 hours a week"; reg 43(2)(c): a partner who would satisfy IS Regs # Sch 1B para 4 (a carer) is not treated as engaged in it. @@ -302,6 +305,57 @@ esa_income_eligible: true esa_income: 5_000 +- name: Self-employment takes reg 99's notional tax, not the actual tax + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 13_200 + personal_pension_contributions: 5_900 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Reg 99(1): 20% x (£13,200 - £12,570) = £126, whatever relief the + # pension contributions attract. Reg 99(3)(b): 6% x (£13,200 - £12,570) = + # £37.80 of Class 4. Half the pension contributions, £2,950. + # (£13,200 - £126 - £37.80 - £2,950) / 52 = £193.97, within £195.50. + esa_exempt_work_earnings: 193.97 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + +- name: A self-employment loss is not set against pay (reg 98(11)) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + # £211.54 a week of pay, over £195.50, and a £1,000 loss. + employment_income: 11_000 + self_employment_income: -1_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_exempt_work_earnings: 211.54 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + # Para 6(1)(f): the partner's work, 24 hours (reg 42(1)). - name: A partner working 23 hours a week does not end the award @@ -341,7 +395,7 @@ partner: age: 40 is_claimant_or_partner: true - # The partner's test is hours alone: any earnings, including none. + # The partner's test is hours of paid work, whatever the earnings. hours_worked: 1_248 benunits: couple: diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml index 7e54778ab..81beea365 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml @@ -2,19 +2,21 @@ # Jobseekers Act 1995 s.1(2)(e): a claimant is entitled if he "is not engaged # in remunerative work"; s.3(1)(e): the income-based conditions include that # the claimant "is not a member of a couple the other member of which is -# engaged in remunerative work". s.1(2B)(b): in a joint claim "each member of -# the couple satisfies the conditions set out in paragraphs (a) to (c) and (e) -# to (i) of subsection (2)", and s.35(1) makes each member a claimant. A -# joint-claim couple is a couple with no child or young person for whom one -# of them gets Child Benefit (s.1(4)), at least one of whom is 18 or over and -# born after 28 October 1947 (JSA Regs 1996 reg 3A(1)). -# JSA Regs 1996 reg 51(1): remunerative work is paid work of "not less than -# 16 hours per week" for "a claimant" and "not less than 24 hours per week" -# for "any partner of the claimant". Reg 53 has no exception for carers. +# engaged in remunerative work". JSA Regs 1996 reg 51(1): remunerative work is +# paid work of "not less than 16 hours per week" for "a claimant" and "not +# less than 24 hours per week" for "any partner of the claimant". +# A couple without children claim jointly, and each member of a joint-claim +# couple is a claimant (s.1(2B)(b), s.35(1)). But reg 3E(1) lets a member of a +# joint-claim couple claim alone, under s.3, where the other member "is +# engaged ... in remunerative work for 16 hours or more per week but less than +# 24 hours per week" (reg 3E(2)(g)). So whatever the family, an award ends +# only when the claimant works 16 hours or the other member 24. +# Reg 53 has no exception for carers doing unrelated paid work. # # Hours are annual hours_worked; the weekly figure is hours_worked / 52, so -# 780 is 15 hours, 832 is 16, 1,196 is 23 and 1,248 is 24. 2025 is used -# because the model pays income-based JSA until 31 March 2026. Capital is nil. +# 780 is 15 hours, 832 is 16, 1,040 is 20, 1,196 is 23 and 1,248 is 24. 2025 +# is used because the model pays income-based JSA until 31 March 2026. +# Capital is nil. - name: A single claimant working 15 hours a week keeps the award period: 2025 @@ -102,7 +104,6 @@ household: members: [claimant, partner, child] output: - is_jsa_joint_claim_couple: false jsa_income_eligible: true jsa_income: 3_000 @@ -189,9 +190,9 @@ output: jsa_income_eligible: false -# A couple without children: a joint claim (s.1(2B)(b)). +# A couple without children (a joint-claim couple, s.1(4), reg 3A(1)). -- name: In a joint claim, the other member working 15 hours does not end it +- name: Without children, the other member working 15 hours does not end it period: 2025 absolute_error_margin: 0 input: @@ -211,11 +212,10 @@ household: members: [first, second] output: - is_jsa_joint_claim_couple: true jsa_income_eligible: true jsa_income: 3_000 -- name: In a joint claim, the other member working 16 hours ends it +- name: Without children, the other member working 16 hours does not end it period: 2025 absolute_error_margin: 0 input: @@ -227,8 +227,8 @@ second: age: 30 is_claimant_or_partner: true - # Under the 24 hours that would apply to a partner, but each member - # of a joint-claim couple is a claimant. + # A joint claim would fail, but the first member can claim alone + # (reg 3E(1), (2)(g)) and the second is under 24 hours (s.3(1)(e)). hours_worked: 832 benunits: couple: @@ -237,11 +237,10 @@ household: members: [first, second] output: - is_jsa_joint_claim_couple: true - jsa_income_eligible: false - jsa_income: 0 + jsa_income_eligible: true + jsa_income: 3_000 -- name: A couple entered as not a joint-claim couple use the partner's 24 hours +- name: Without children, the other member working 20 hours does not end it period: 2025 absolute_error_margin: 0 input: @@ -257,26 +256,26 @@ benunits: couple: members: [first, second] - # For example where one member claims alone (JSA Regs 1996 reg 3E). - is_jsa_joint_claim_couple: false households: household: members: [first, second] output: jsa_income_eligible: true + jsa_income: 3_000 -- name: A couple born before the prescribed date are not a joint-claim couple +- name: Without children, the other member working 23 hours does not end it period: 2025 absolute_error_margin: 0 input: people: first: - # Born in 1945, before 28 October 1947. - age: 80 + age: 30 is_claimant_or_partner: true + jsa_income_reported: 3_000 second: - age: 80 + age: 30 is_claimant_or_partner: true + hours_worked: 1_196 benunits: couple: members: [first, second] @@ -284,25 +283,33 @@ household: members: [first, second] output: - is_jsa_joint_claim_couple: false + jsa_income_eligible: true + jsa_income: 3_000 -- name: A single claimant is not a joint-claim couple +- name: Without children, the other member working 24 hours ends it period: 2025 absolute_error_margin: 0 input: people: - claimant: + first: + age: 30 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + second: age: 30 + is_claimant_or_partner: true + hours_worked: 1_248 benunits: - benunit: - members: [claimant] + couple: + members: [first, second] households: household: - members: [claimant] + members: [first, second] output: - is_jsa_joint_claim_couple: false + jsa_income_eligible: false + jsa_income: 0 -- name: A couple caring for a placed child are not a joint-claim couple +- name: A couple caring for a placed child have the same 24-hour limit period: 2025 absolute_error_margin: 0 input: @@ -310,9 +317,11 @@ first: age: 40 is_claimant_or_partner: true + jsa_income_reported: 3_000 second: age: 40 is_claimant_or_partner: true + hours_worked: 1_040 child: age: 8 is_claimant_or_partner: false @@ -324,9 +333,9 @@ household: members: [first, second, child] output: - # Reg 3A(1) excludes a couple caring for a child in the circumstances - # of reg 78(4), which include a child placed by a local authority. - is_jsa_joint_claim_couple: false + # Not a joint-claim couple (reg 3A(1) with reg 78(4)(a)); the partner's + # 20 hours are under 24, as for a couple without children. + jsa_income_eligible: true # Members outside the family. diff --git a/policyengine_uk/tests/test_income_support_eligibility_properties.py b/policyengine_uk/tests/test_income_support_eligibility_properties.py index 4435e3466..dd6691c19 100644 --- a/policyengine_uk/tests/test_income_support_eligibility_properties.py +++ b/policyengine_uk/tests/test_income_support_eligibility_properties.py @@ -50,7 +50,7 @@ from hypothesis import HealthCheck, event, given, settings from hypothesis import strategies as st -from legacy_award_work_reference import esa_screen, jsa_joint_claim, jsa_screen +from legacy_award_work_reference import esa_screen, jsa_screen from policyengine_uk import Simulation YEAR = 2025 @@ -398,15 +398,12 @@ def is_claimant(i): if jsa_income is not None: income_based_jsa = jsa_income > 0 else: - # A child placed with the couple ends a joint claim (JSA Regs 1996 - # reg 3A(1) with reg 78(4)(a)), so every dependant counts here. - joint_claim = jsa_joint_claim(adults, bool(dependants), YEAR, parameters) income_based_jsa = ( JSA.active and income_related_award( sum(a["jsa_income_reported"] for a in adults), capital, JSA.capital ) - and jsa_screen(adults, [], capital, joint_claim, parameters) + and jsa_screen(adults, [], capital, parameters) ) return ( any(is_claimant(i) for i in range(len(adults))) @@ -476,14 +473,13 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): reported_total = sum( member.get("jsa_income_reported", 0) for member in adults + [extra] ) - joint_claim = jsa_joint_claim(adults, bool(dependants), YEAR, parameters) if not explained_by_reports( jsa_income[i], reported_total, capital, parameters, parameters.gov.dwp.JSA.income.capital, - screen=jsa_screen(adults, others, capital, joint_claim, parameters), + screen=jsa_screen(adults, others, capital, parameters), ): entered_jsa = jsa_income[i] expected = reference_eligibility( @@ -537,16 +533,16 @@ def test_more_work_or_jsa_never_makes_a_family_eligible(drawn, data): if (eligible[: len(drawn)] & ~eligible[len(drawn) :]).any(): event("more work or JSA removed eligibility") n = len(drawn) - # An award entered directly is the same on both sides; one calculated - # from reports can end when the work test fails. + # An award entered directly is the same on both sides. One calculated + # from reports can end when the work test fails: these families have no + # member outside the couple, so esa_income and jsa_income are the + # claimant's and partner's award, after tariff income and while the + # scheme is active. lost_award = np.zeros(n, dtype=bool) - for variable, direct in [ - ("esa_income_eligible", esa_income), - ("jsa_income_eligible", jsa_income), - ]: + for variable, direct in [("esa_income", esa_income), ("jsa_income", jsa_income)]: if direct is None: - screen = sim.calculate(variable, YEAR) - lost_award |= screen[:n] & ~screen[n:] + award = sim.calculate(variable, YEAR) + lost_award |= (award[:n] > 0) & (award[n:] <= 0) if (eligible[n:] & ~eligible[:n] & lost_award).any(): event("more work ended an award that barred Income Support") for i in range(n): diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 7036eadea..121e78317 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -1,12 +1,12 @@ """Properties of the income-related ESA and income-based JSA work conditions. Welfare Reform Act 2007 Sch 1 para 6(1)(e) and (f) and Jobseekers Act 1995 -ss.1(2)(e), 1(2B)(b) and 3(1)(e) bar an award when the claimant, or the other -member of the couple, is engaged in remunerative work. For every family: +ss.1(2)(e) and 3(1)(e) (with JSA Regs 1996 reg 3E for joint-claim couples) +bar an award when the claimant, or the other member of the couple, is engaged +in remunerative work. For every family: - esa_income_eligible and jsa_income_eligible equal a family-by-family - reading of the law (legacy_award_work_reference), and is_jsa_joint_claim_couple - equals the reading of s.1(4) and JSA Regs reg 3A(1); + reading of the law (legacy_award_work_reference); - more hours or more pay for anyone never makes a family eligible, because the conditions only ever bar a claim; - caring never ends an ESA award (a carer partner is not treated as in @@ -16,8 +16,10 @@ the family (a non-dependent adult, with any award and any work) never changes either screen. -The reference covers employees with no other income and pay within the basic -rate band. Roles are given explicitly (is_claimant_or_partner). Each example +Adults are employees (pay within the basic rate band), self-employed (a +profit within the Class 4 upper limit, under state pension age) or employees +with a self-employment loss, with no other income: the scope the reference +covers. Roles are given explicitly (is_claimant_or_partner). Each example builds many families in one simulation, in separate households and benefit units. """ @@ -26,7 +28,7 @@ from hypothesis import HealthCheck, event, given, settings from hypothesis import strategies as st -from legacy_award_work_reference import esa_screen, jsa_joint_claim, jsa_screen +from legacy_award_work_reference import esa_screen, jsa_screen from policyengine_uk import Simulation # 0, 15, 16, 20, 23, 24 and 40 hours a week. @@ -34,27 +36,40 @@ # £0, £20 (the lower limit), £21, £100, £195.50 (the 2025-26 higher limit), # £196, £203.50 (2026-27), £204 and £300 a week. PAY = [0, 0, 1_040, 1_092, 5_200, 10_166, 10_192, 10_582, 10_608, 15_600] +# Profits around the limits once reg 99's notional tax and Class 4 come off, +# and £13,200, which needs half a £5,900 pension premium to fall within them. +PROFIT = [1_040, 5_200, 10_166, 10_300, 13_200, 15_600] @st.composite def adults(draw, outside_family=False): - pay = draw(st.sampled_from(PAY)) + kind = draw(st.sampled_from(["employee", "employee", "self_employed", "loss"])) + min_age = 18 if not outside_family else 20 + if kind == "self_employed": + # Class 4 stops at state pension age. + age = draw(st.integers(min_age, 64)) + else: + age = draw(st.one_of(st.integers(min_age, 64), st.integers(70, 90))) + pay = 0 if kind == "self_employed" else draw(st.sampled_from(PAY)) adult = { - "age": draw( - st.one_of( - st.integers(18 if not outside_family else 20, 64), st.integers(70, 90) - ) - ), + "age": age, "esa_income_reported": draw(st.sampled_from([0, 0, 3_000])), "jsa_income_reported": draw(st.sampled_from([0, 0, 3_000])), "hours_worked": draw(st.sampled_from(HOURS)), "employment_income": pay, "employee_pension_contributions": ( - draw(st.sampled_from([0, 0, 520])) if pay <= 12_570 else 0 + draw(st.sampled_from([0, 0, 520])) if 0 < pay <= 12_570 else 0 ), "receives_carer_benefit": draw(st.booleans()), "care_hours": draw(st.sampled_from([0, 0, 35])), } + if kind == "self_employed": + adult["self_employment_income"] = draw(st.sampled_from(PROFIT)) + adult["personal_pension_contributions"] = draw( + st.sampled_from([0, 0, 520, 5_900]) + ) + elif kind == "loss": + adult["self_employment_income"] = -1_000 if outside_family: adult["current_education"] = "NOT_IN_EDUCATION" return adult @@ -119,6 +134,10 @@ def label(units): event("placed child") if any(others for *_, others, _ in units): event("member outside the family") + if any(a.get("self_employment_income", 0) > 0 for a in adults_): + event("self-employed") + if any(a.get("self_employment_income", 0) < 0 for a in adults_): + event("self-employment loss beside pay") SETTINGS = settings( @@ -138,19 +157,14 @@ def test_screens_match_a_family_by_family_reading(units, year): sim = Simulation(situation=situation(units, year)) esa = sim.calculate("esa_income_eligible", year) jsa = sim.calculate("jsa_income_eligible", year) - joint = sim.calculate("is_jsa_joint_claim_couple", year) if esa.any(): event("some ESA award passes") if jsa.any(): event("some JSA award passes") parameters = sim.tax_benefit_system.parameters(year) for i, (couple, children, others, capital) in enumerate(units): - expected_joint = jsa_joint_claim(couple, bool(children), year, parameters) - assert joint[i] == expected_joint, units[i] assert esa[i] == esa_screen(couple, others, capital, parameters), units[i] - assert jsa[i] == jsa_screen( - couple, others, capital, expected_joint, parameters - ), units[i] + assert jsa[i] == jsa_screen(couple, others, capital, parameters), units[i] @st.composite @@ -162,6 +176,8 @@ def more(adult): adult = dict(adult) adult["hours_worked"] += draw(st.sampled_from([0, 52, 260, 832, 1_248])) adult["employment_income"] += draw(st.sampled_from([0, 52, 1_040, 5_200])) + if adult.get("self_employment_income", 0) > 0: + adult["self_employment_income"] += draw(st.sampled_from([0, 52, 1_040])) return adult return [more(a) for a in couple], children, [more(a) for a in others], capital diff --git a/policyengine_uk/tests/test_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index bad8ed7dd..8f19df33a 100644 --- a/policyengine_uk/tests/test_legacy_benefit_parameters.py +++ b/policyengine_uk/tests/test_legacy_benefit_parameters.py @@ -88,6 +88,11 @@ def test_esa_exempt_work_higher_limit_is_16_x_nmw_rounded_up(): assert limit("2024") == 183.5 assert limit("2025") == 195.5 assert limit("2026") == 203.5 + # Later years follow average earnings, rounded up to 50p like reg 45(9A). + for year in range(2027, 2041): + value = limit(str(year)) + assert value == _round_up_to_50p(value), year + assert value >= limit(str(year - 1)), year def test_esa_and_jsa_remunerative_work_parameters_by_date(): @@ -105,9 +110,3 @@ def test_esa_and_jsa_remunerative_work_parameters_by_date(): for year in range(2015, 2031): assert dwp.JSA.hours.single(str(year)) == work.claimant_hours(str(year)) assert dwp.JSA.hours.couple(str(year)) == work.partner_hours(str(year)) - joint_claim = dwp.JSA.income.joint_claim - assert not joint_claim.in_effect("2001-03-18") - assert joint_claim.in_effect("2001-03-19") - assert joint_claim.born_after_year("2001-03-19") == 1976 - assert joint_claim.born_after_year("2002-10-28") == 1957 - assert joint_claim.born_after_year("2008-02-25") == 1947 diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index 32e4d2b77..43e27c42f 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -9,18 +9,29 @@ class esa_exempt_work_earnings(Variable): "A claimant's earnings in a week, which ESA Regs 2008 reg 45(2) and " "(4) compare with the exempt work earnings limits. They are worked " "out under the rules for calculating earnings for income-related ESA " - "(DMG 41189; regs 92 to 99): gross employment and self-employment " - "income, less income tax and National Insurance on it and half of " - "the person's own pension contributions (regs 96(3) and 98(4)), " - "divided by 52. The income tax deducted is the tax on earnings taken " + "(DMG 41189; regs 92 to 99), and divided by 52. " + "Employment (reg 96(3)): gross pay less the income tax and primary " + "Class 1 contributions deducted from it and half the person's " + "occupational and personal pension contributions. The regulation " + "takes the tax actually deducted, which the model's inputs do not " + "record; the model approximates it by the income tax on the pay taken " "as the lowest slice of the person's non-savings income, so tax on a " - "pension, a taxable benefit, property, savings or dividends never " - "comes off earnings; that is the reading most favourable to the " - "claimant, where PAYE on a second job would take tax at the basic " - "rate. National Insurance is the Class 1 primary, Class 2 and Class " - "4 contributions. Enter this variable directly for other earnings, " - "for example where the regulations' averaging periods give a " - "different figure." + "pension, a taxable benefit, self-employment, property, savings or " + "dividends never comes off the pay. A second job taxed at the basic " + "rate through PAYE would leave lower net earnings than this. " + "Self-employment (reg 98(3) and reg 99): the profit less a notional " + "income tax at the basic rate (the Scottish basic rate for a Scottish " + "taxpayer) on the profit above the personal allowance, notional " + "Class 4 contributions at the main rate between the lower and upper " + "profits limits, and half the personal pension contributions. The " + "model gives personal pension contributions to self-employment when " + "there is a profit and to employment otherwise. A loss in one " + "employment is not set against earnings from another (reg 98(11)), " + "so a self-employment loss counts as nil. Personal reliefs other " + "than the personal allowance (reg 99(1)) are not modelled. Enter this " + "variable directly for actual net earnings, for example where PAYE " + "deducted a different amount or the regulations' averaging periods " + "give a different figure." ) definition_period = YEAR unit = GBP @@ -28,50 +39,73 @@ class esa_exempt_work_earnings(Variable): "https://www.legislation.gov.uk/uksi/2008/794/regulation/45", "https://www.legislation.gov.uk/uksi/2008/794/regulation/96", "https://www.legislation.gov.uk/uksi/2008/794/regulation/98", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/99", "https://assets.publishing.service.gov.uk/media/689db34d1fedc616bb13399c/dmg-ch-41.pdf", ) def formula(person, period, parameters): income_tax = parameters(period).gov.hmrc.income_tax - gross = add(person, period, ["employment_income", "self_employment_income"]) - national_insurance = add( - person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] - ) - # Income tax on earnings, with earnings as the lowest slice of + rates = income_tax.rates + scottish = person("pays_scottish_income_tax", period) + profit = max_(0, person("self_employment_income", period)) + personal_pension = person("personal_pension_contributions", period) + personal_pension_from_profit = where(profit > 0, personal_pension, 0) + + # Employment, reg 96(3). Income tax on the pay as the lowest slice of # non-savings income: the part of earned_taxable_income left after - # the person's other non-savings income (pensions, taxable benefits) - # is taken off the top. earned_taxable_income is adjusted net income - # less the incomes and allowances on the exclusions list. + # the person's other non-savings income is taken off the top. + # earned_taxable_income is adjusted net income less the incomes and + # allowances on the exclusions list. + pay = person("employment_income", period) exclusions = list(income_tax.earned_taxable_income_exclusions) excluded_incomes = [ variable for variable in exclusions if variable in income_tax.adjusted_net_income_components ] - taxable_earnings_components = [ - variable - for variable in [ - "taxable_employment_income", - "taxable_self_employment_income", - ] - if variable not in exclusions - ] - earnings_in_base = add(person, period, taxable_earnings_components) + pay_in_base = ( + 0 + if "taxable_employment_income" in exclusions + else person("taxable_employment_income", period) + ) income_in_base = person("adjusted_net_income", period) - add( person, period, excluded_incomes ) - other_income = max_(0, income_in_base - earnings_in_base) - taxable_earnings = max_( - 0, person("earned_taxable_income", period) - other_income - ) - rates = income_tax.rates - tax_before_reductions = where( - person("pays_scottish_income_tax", period), - rates.scotland.rates.calc(taxable_earnings), - rates.uk.calc(taxable_earnings), + other_income = max_(0, income_in_base - pay_in_base) + taxable_pay = max_(0, person("earned_taxable_income", period) - other_income) + tax_on_pay = where( + scottish, + rates.scotland.rates.calc(taxable_pay), + rates.uk.calc(taxable_pay), ) + # Tax reductions (for example the married couple's allowance) come + # off the tax on pay first, as allowances do. reductions = add(person, period, income_tax.income_tax_subtractions) - tax = max_(0, tax_before_reductions - reductions) - pension = person("pension_contributions", period) / 2 - net = max_(0, gross - national_insurance - tax - pension) - return net / WEEKS_IN_YEAR + tax_on_pay = max_(0, tax_on_pay - reductions) + pension_from_pay = ( + person("employee_pension_contributions", period) + + personal_pension + - personal_pension_from_profit + ) + net_pay = max_( + 0, + pay + - tax_on_pay + - person("ni_class_1_employee", period) + - pension_from_pay / 2, + ) + + # Self-employment, reg 98(3) and reg 99. Reg 99(1): tax at the basic + # rate (the first UK band; the second Scottish band, after the + # starter rate) on the profit less the personal allowance. + basic_rate = where(scottish, rates.scotland.rates.rates[1], rates.uk.rates[0]) + notional_tax = basic_rate * max_( + 0, profit - person("personal_allowance", period) + ) + # Reg 99(3)(b): Class 4 at the main rate between the profits limits. + notional_class_4 = person("ni_class_4_main", period) + net_profit = max_( + 0, + profit - notional_tax - notional_class_4 - personal_pension_from_profit / 2, + ) + return (net_pay + net_profit) / WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/esa_income.py b/policyengine_uk/variables/gov/dwp/esa_income.py index 2dc6a4415..4e488b5d2 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income.py +++ b/policyengine_uk/variables/gov/dwp/esa_income.py @@ -4,7 +4,7 @@ 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 capital - test (esa_income_eligible).""" + or remunerative work tests (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) @@ -15,7 +15,8 @@ class esa_income(Variable): entity = BenUnit label = "ESA (income-based)" documentation = ( - "Reported income-related ESA screened through a bounded capital test. " + "Reported income-related ESA screened through bounded capital and " + "remunerative work tests (esa_income_eligible). " "This is not a full entitlement model." ) definition_period = YEAR diff --git a/policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py b/policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py deleted file mode 100644 index 1ec04ef18..000000000 --- a/policyengine_uk/variables/gov/dwp/is_jsa_joint_claim_couple.py +++ /dev/null @@ -1,51 +0,0 @@ -from policyengine_uk.model_api import * - - -class is_jsa_joint_claim_couple(Variable): - value_type = bool - entity = BenUnit - label = "Joint-claim couple for income-based JSA" - documentation = ( - "Whether the claimant and partner are a joint-claim couple, who " - "claim income-based Jobseeker's Allowance together, so that each of " - "them is a claimant (Jobseekers Act 1995 ss.1(2B) and 35(1)). A " - "joint-claim couple is a couple with no child or young person in the " - "family for whom one of them is entitled to Child Benefit (s.1(4)), " - "nor a child or young person they care for in the circumstances of " - "JSA Regs 1996 reg 78(4) or (8), at least one of whom is 18 or over " - "and was born after the prescribed date (reg 3A(1)). Joint claims " - "began on 19 March 2001. The model reads a child or young person as " - "a Child Benefit child or qualifying young person in the benefit " - "unit other than the claimant and partner, including a child placed " - "with them. The circumstances in which one member of a joint-claim " - "couple may claim alone (regs 3D and 3E) are not modelled; enter this " - "variable directly for them." - ) - definition_period = YEAR - reference = ( - "https://www.legislation.gov.uk/ukpga/1995/18/section/1", - "https://www.legislation.gov.uk/ukpga/1995/18/section/35", - "https://www.legislation.gov.uk/uksi/1996/207/regulation/3A", - ) - - def formula(benunit, period, parameters): - joint_claim = parameters(period).gov.dwp.JSA.income.joint_claim - if not joint_claim.in_effect: - return benunit.empty_array() > 0 - person = benunit.members - claimant_or_partner = person("is_claimant_or_partner", period) - couple = benunit.sum(claimant_or_partner) == 2 - child_or_young_person = ( - person("is_child_or_qualifying_young_person_for_child_benefit", period) - & ~claimant_or_partner - ) - prescribed_member = ( - claimant_or_partner - & (person("age", period) >= 18) - & (person("birth_year", period) > joint_claim.born_after_year) - ) - return ( - couple - & ~benunit.any(child_or_young_person) - & benunit.any(prescribed_member) - ) diff --git a/policyengine_uk/variables/gov/dwp/jsa_income.py b/policyengine_uk/variables/gov/dwp/jsa_income.py index 600faf9fa..9cf0edac6 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income.py @@ -4,7 +4,8 @@ def income_related_jsa_award(benunit, period, reported_award): """Income-based JSA paid on a reported award: the award less tariff income from capital, or nothing if the scheme is not active or the - benefit unit fails the capital test (jsa_income_eligible).""" + benefit unit fails the capital or remunerative work tests + (jsa_income_eligible).""" JSA = benunit.simulation.tax_benefit_system.parameters(period).gov.dwp.JSA if not JSA.income.active: return benunit.empty_array() @@ -18,7 +19,8 @@ class jsa_income(Variable): entity = BenUnit label = "JSA (income-based)" documentation = ( - "Reported income-based JSA screened through a bounded capital test. " + "Reported income-based JSA screened through bounded capital and " + "remunerative work tests (jsa_income_eligible). " "This is not a full entitlement model." ) definition_period = YEAR diff --git a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py index e5b10619a..7aecb8afd 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py @@ -9,25 +9,31 @@ class jsa_income_eligible(Variable): ) documentation = ( "Bounded screen applied to reported income-based JSA awards: the " - "capital test (Jobseekers Act 1995 s.13(1) and (2A); JSA Regs 1996 reg 107) " - "and the remunerative work conditions. The claimant must not be " - "engaged in remunerative work, paid work of 16 hours a week or more " - "(s.1(2)(e); reg 51(1)(a)). In a joint-claim couple both members are " - "claimants, so neither may work 16 hours (s.1(2B)(b)); otherwise the " - "claimant's partner must not work 24 hours or more (s.3(1)(e); reg " - "51(1)(b)). Unlike Income Support and income-related ESA, the JSA " - "regulations do not take a carer out of remunerative work. The " - "claimant is a member who reports the award. When the claimant or " - "partner reports one, only they are candidates: a member outside the " - "family, such as a non-dependent adult, claims in their own right, " - "and is tested on their own work only when neither the claimant nor " - "the partner reports an award. This is not a full entitlement model." + "capital test (Jobseekers Act 1995 s.13(1) and (2A); JSA Regs 1996 " + "reg 107) and the remunerative work conditions. The claimant must not " + "be engaged in remunerative work, paid work of 16 hours a week or " + "more (s.1(2)(e); reg 51(1)(a)), and the claimant's partner must not " + "work 24 hours or more (s.3(1)(e); reg 51(1)(b)). A couple without " + "children of the prescribed description claim jointly, and each of " + "them is then a claimant held to 16 hours (s.1(2B)(b), s.35(1)). But " + "a member of a joint-claim couple may claim alone when the other " + "works 16 hours or more but less than 24 (reg 3E(1) and (2)(g), since " + "joint claims began on 19 March 2001), and is then tested under s.3, " + "so an award survives in either case unless the other member works " + "24 hours or more. The JSA regulations do not take a carer out of " + "remunerative work for unrelated paid work. The claimant is a member " + "who reports the award. When the claimant or partner reports one, " + "only they are candidates: a member outside the family, such as a " + "non-dependent adult, claims in their own right, and is tested on " + "their own work only when neither the claimant nor the partner " + "reports an award. This is not a full entitlement model." ) definition_period = YEAR reference = ( "https://www.legislation.gov.uk/ukpga/1995/18/section/1", "https://www.legislation.gov.uk/ukpga/1995/18/section/3", "https://www.legislation.gov.uk/ukpga/1995/18/section/13", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/3E", "https://www.legislation.gov.uk/uksi/1996/207/regulation/51", "https://www.legislation.gov.uk/uksi/1996/207/regulation/107", ) @@ -46,21 +52,13 @@ def formula(benunit, period, parameters): ) WORK = JSA.remunerative_work hours = person("jsa_remunerative_work_hours", period) - # s.1(2)(e) and reg 51(1)(a): 16 hours for a claimant, including each - # member of a joint-claim couple. + # s.1(2)(e) and reg 51(1)(a): 16 hours for the claimant. works_as_claimant = hours >= WORK.claimant_hours - # s.3(1)(e) and reg 51(1)(b): 24 hours for the partner, outside a - # joint claim. - joint_claim = benunit.project(benunit("is_jsa_joint_claim_couple", period)) - works_as_other_member = claimant_or_partner & where( - joint_claim, works_as_claimant, hours >= WORK.partner_hours - ) + # s.3(1)(e) and reg 51(1)(b): 24 hours for the other member of the + # couple, joint claim or not (reg 3E(2)(g)). + works_as_partner = claimant_or_partner & (hours >= WORK.partner_hours) other_member_works = claimant_or_partner & ( - ( - benunit.project(benunit.sum(works_as_other_member)) - - works_as_other_member - ) - > 0 + (benunit.project(benunit.sum(works_as_partner)) - works_as_partner) > 0 ) claimant = candidate & ~works_as_claimant & ~other_member_works return benunit.any(claimant) & (capital <= JSA.income.capital.limit) From ebed8bd84f12f0b992ae4c4ad3076dd866b8e481 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 04:26:03 -0400 Subject: [PATCH 04/11] Apply review r2: PAYE on pay alone, reg 99 Class 2 and reliefs, Scottish rate - A self-employment loss larger than the pay no longer reaches the tax on the pay through the shared allowances (pension_contributions_relief goes negative there). The pay side is PAYE on the pay alone: taxable pay less employee pension contributions and the personal and blind person's allowances, less tax reductions. - Reg 99(1): the notional tax allows the blind person's allowance. - Reg 99(3)(a): Class 2 is deducted until 5 April 2024, from the small profits threshold and, from 6 April 2022, above the lower profits threshold (two dated parameters). - The Scottish basic rate is the first Scottish band before 2018-19 and the second once the starter rate came in. - YAML: a loss larger than the pay, the blind person's allowance, and a 2017-18 Scottish profit with Class 2. - The reference applies National Insurance only below state pension age and covers pay with profit and a loss larger than the pay. The differential property also compares weekly earnings. Co-Authored-By: Claude Opus 5.5 --- .../esa-jsa-remunerative-work.fixed.md | 2 +- .../above_lower_profits_threshold.yaml | 20 +++ .../self_employment_class_2/deducted.yaml | 18 +++ .../tests/legacy_award_work_reference.py | 62 ++++++---- .../esa_income_remunerative_work.yaml | 79 ++++++++++++ .../test_legacy_award_work_properties.py | 41 +++++-- .../tests/test_legacy_benefit_parameters.py | 12 ++ .../gov/dwp/esa_exempt_work_earnings.py | 114 ++++++++++-------- 8 files changed, 266 insertions(+), 82 deletions(-) create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/above_lower_profits_threshold.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/deducted.yaml diff --git a/changelog.d/esa-jsa-remunerative-work.fixed.md b/changelog.d/esa-jsa-remunerative-work.fixed.md index 8f16a7b41..e8cabe070 100644 --- a/changelog.d/esa-jsa-remunerative-work.fixed.md +++ b/changelog.d/esa-jsa-remunerative-work.fixed.md @@ -1,3 +1,3 @@ -- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less income tax, primary Class 1 and half the pension contributions (reg 96(3)), plus a self-employed profit less reg 99's notional basic-rate tax and main-rate Class 4 and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. +- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less PAYE on the pay alone, primary Class 1 and half the pension contributions (reg 96(3)), plus a self-employed profit less reg 99's notional basic-rate tax above the personal and blind person's allowances, main-rate Class 4, Class 2 until 5 April 2024 (reg 99(3)(a); new `gov.dwp.ESA.income.self_employment_class_2.{deducted, above_lower_profits_threshold}`) and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. - End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). A member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (reg 3E(2)(g)), so 24 hours is the limit for the other member of any couple. The JSA regulations have no carer exception. Hours come from the new `jsa_remunerative_work_hours`. - The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours` and `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. diff --git a/policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/above_lower_profits_threshold.yaml b/policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/above_lower_profits_threshold.yaml new file mode 100644 index 000000000..523125dc5 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/above_lower_profits_threshold.yaml @@ -0,0 +1,20 @@ +description: Whether the notional Class 2 deduction from a self-employed claimant's profit for income-related ESA applies only above the lower profits threshold, rather than from the small profits threshold. +values: + 2008-10-27: + value: false + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 99(3)(a) ("less than ... (small profits threshold)") + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/99/2015-04-06 + 2022-04-06: + value: true + reference: + - title: Social Security (Class 2 National Insurance Contributions Increase of Threshold) Regulations 2022 (SI 2022/1329) reg. 5, with effect from 6 April 2022 ("equal to or less than ... (lower profits threshold)") + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/99/2023-04-06 +metadata: + unit: bool + label: ESA self-employed Class 2 deduction above the lower profits threshold + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 99(3)(a) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/99 + - title: Social Security Contributions and Benefits Act 1992 s. 11(4) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/11 diff --git a/policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/deducted.yaml b/policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/deducted.yaml new file mode 100644 index 000000000..3bf7a6c5b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/income/self_employment_class_2/deducted.yaml @@ -0,0 +1,18 @@ +description: Whether the notional deductions from a self-employed claimant's profit for income-related ESA include Class 2 National Insurance contributions. +values: + 2008-10-27: + value: true + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 99(3)(a) (as made) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/99/2008-10-27 + 2024-04-06: + value: false + reference: + - title: Social Security (Class 2 National Insurance Contributions) (Consequential Amendments and Savings) Regulations 2024 (SI 2024/377) reg. 8(15), omitting reg. 99(3)(a) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/99/2024-04-06 +metadata: + unit: bool + label: ESA self-employed earnings deduct Class 2 + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 99(3) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/99 diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index 59faca23e..202a8655b 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -14,11 +14,12 @@ - ESA claimant (para 6(1)(e), ESA Regs reg 41(1)): paid work is remunerative unless it is exempt work: earnings of no more than £20 a week (reg 45(2)), or under 16 hours with earnings within the higher limit (reg 45(4)). - Pay is net of income tax, primary Class 1 and half the pension + Pay is net of PAYE on the pay alone, primary Class 1 and half the pension contributions (reg 96(3)). Self-employment is the profit less a notional basic-rate tax on the profit above the personal allowance, notional - main-rate Class 4 and half the personal pension contributions (reg 98(3), - reg 99); a loss is not set against pay (reg 98(11)). + main-rate Class 4 (and Class 2 before 6 April 2024) and half the personal + pension contributions (reg 98(3), reg 99); a loss is not set 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)). - JSA claimant (s.1(2)(e), reg 51(1)(a)): 16 hours or more, with no carer @@ -67,39 +68,58 @@ def weekly_hours(adult): def esa_weekly_earnings(adult, parameters): """Net weekly earnings for the exempt work limits. Covers pay within the basic rate band, a self-employment profit within the Class 4 upper limit - or a loss, and no other income; Class 4 is for someone under state - pension age.""" + or a loss, any mix of the two, and no other income, for someone in + England; National Insurance stops at state pension age (66 in the years + tested).""" hmrc = parameters.gov.hmrc allowance = hmrc.income_tax.allowances.personal_allowance.amount basic_rate = hmrc.income_tax.rates.uk.rates[0] + ni_liable = adult["age"] < 66 pay = adult.get("employment_income", 0) profit = max(0, adult.get("self_employment_income", 0)) + employee_pension = adult.get("employee_pension_contributions", 0) personal_pension = adult.get("personal_pension_contributions", 0) pension_from_profit = personal_pension if profit > 0 else 0 - pension_from_pay = ( - adult.get("employee_pension_contributions", 0) - + personal_pension - - pension_from_profit - ) - # Pension contributions from pay are only used with pay below the - # personal allowance, so how they are relieved for tax does not arise. - assert pension_from_pay == 0 or pay <= allowance, adult - tax_on_pay = basic_rate * max(0, pay - allowance) + pension_from_pay = employee_pension + personal_pension - pension_from_profit + assert pay <= hmrc.income_tax.rates.uk.thresholds[1] + allowance, adult + # Reg 96(3): PAYE on the pay alone, with employee contributions taken off + # taxable pay (net pay arrangements). + tax_on_pay = basic_rate * max(0, pay - min(employee_pension, pay) - allowance) class_1 = hmrc.national_insurance.class_1 threshold = class_1.thresholds.primary_threshold * WEEKS - class_1_on_pay = class_1.rates.employee.main * max(0, pay - threshold) + class_1_on_pay = ( + class_1.rates.employee.main * max(0, pay - threshold) if ni_liable else 0 + ) net_pay = max(0, pay - tax_on_pay - class_1_on_pay - pension_from_pay / 2) # Reg 99(1): basic rate on the profit less the personal allowance, whatever # relief pension contributions attract. Reg 99(3)(b): main-rate Class 4. + # Reg 99(3)(a): Class 2, before 6 April 2024. notional_tax = basic_rate * max(0, profit - allowance) - class_4 = hmrc.national_insurance.class_4 - notional_class_4 = class_4.rates.main * max( - 0, - min(profit, class_4.thresholds.upper_profits_limit) - - class_4.thresholds.lower_profits_limit, + nics = hmrc.national_insurance + class_4 = nics.class_4 + assert profit <= class_4.thresholds.upper_profits_limit, adult + notional_class_4 = ( + class_4.rates.main * max(0, profit - class_4.thresholds.lower_profits_limit) + if ni_liable + else 0 + ) + rule = parameters.gov.dwp.ESA.income.self_employment_class_2 + if rule.above_lower_profits_threshold: + class_2_due = profit > class_4.thresholds.lower_profits_limit + else: + class_2_due = profit >= nics.class_2.small_profits_threshold + notional_class_2 = ( + nics.class_2.flat_rate * WEEKS + if rule.deducted and class_2_due and ni_liable + else 0 ) net_profit = max( - 0, profit - notional_tax - notional_class_4 - pension_from_profit / 2 + 0, + profit + - notional_tax + - notional_class_4 + - notional_class_2 + - pension_from_profit / 2, ) return (net_pay + net_profit) / WEEKS diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index adf8d2718..d6779d851 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -356,6 +356,85 @@ esa_income_eligible: false esa_income: 0 +- name: A self-employment loss larger than the pay still counts as nil + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 11_000 + self_employment_income: -20_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # £11,000 / 52 = £211.54: the loss neither offsets the pay nor changes + # the tax on it. + esa_exempt_work_earnings: 211.54 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + +- name: Reg 99's notional tax allows the blind person's allowance + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 13_200 + personal_pension_contributions: 5_900 + blind_persons_allowance: 3_130 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # The personal (£12,570) and blind person's (£3,130) allowances cover the + # profit, so no notional tax: (£13,200 - £37.80 - £2,950) / 52 = £196.39, + # over £195.50. + esa_exempt_work_earnings: 196.39 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + +- name: In 2017-18 a Scottish profit takes the Scottish basic rate and Class 2 + period: 2017 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 13_200 + personal_pension_contributions: 11_800 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + region: SCOTLAND + output: + # Before the starter rate (6 April 2018) the Scottish basic rate was the + # first band, 20%: 20% x (£13,200 - £11,500) = £340. Class 4: 9% x + # (£13,200 - £8,164) = £453.24. Class 2 (reg 99(3)(a)): 52 x £2.85 = + # £148.20. Half the premium, £5,900. + # (£13,200 - £340 - £453.24 - £148.20 - £5,900) / 52 = £122.28, over the + # £120 limit. + esa_exempt_work_earnings: 122.28 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + # Para 6(1)(f): the partner's work, 24 hours (reg 42(1)). - name: A partner working 23 hours a week does not end the award diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 121e78317..559962c3f 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -28,7 +28,7 @@ from hypothesis import HealthCheck, event, given, settings from hypothesis import strategies as st -from legacy_award_work_reference import esa_screen, jsa_screen +from legacy_award_work_reference import esa_screen, esa_weekly_earnings, jsa_screen from policyengine_uk import Simulation # 0, 15, 16, 20, 23, 24 and 40 hours a week. @@ -43,13 +43,14 @@ @st.composite def adults(draw, outside_family=False): - kind = draw(st.sampled_from(["employee", "employee", "self_employed", "loss"])) + kind = draw( + st.sampled_from( + ["employee", "employee", "self_employed", "mixed", "loss", "big_loss"] + ) + ) min_age = 18 if not outside_family else 20 - if kind == "self_employed": - # Class 4 stops at state pension age. - age = draw(st.integers(min_age, 64)) - else: - age = draw(st.one_of(st.integers(min_age, 64), st.integers(70, 90))) + # Ages either side of state pension age, where National Insurance stops. + age = draw(st.one_of(st.integers(min_age, 64), st.integers(70, 90))) pay = 0 if kind == "self_employed" else draw(st.sampled_from(PAY)) adult = { "age": age, @@ -58,18 +59,24 @@ def adults(draw, outside_family=False): "hours_worked": draw(st.sampled_from(HOURS)), "employment_income": pay, "employee_pension_contributions": ( - draw(st.sampled_from([0, 0, 520])) if 0 < pay <= 12_570 else 0 + draw(st.sampled_from([0, 0, 520])) if pay > 0 else 0 ), "receives_carer_benefit": draw(st.booleans()), "care_hours": draw(st.sampled_from([0, 0, 35])), } - if kind == "self_employed": + if kind in ("self_employed", "mixed"): adult["self_employment_income"] = draw(st.sampled_from(PROFIT)) + if kind in ("self_employed", "mixed", "employee"): + # Personal pension contributions go to the profit when there is one, + # and to the pay otherwise. adult["personal_pension_contributions"] = draw( st.sampled_from([0, 0, 520, 5_900]) ) - elif kind == "loss": + if kind == "loss": adult["self_employment_income"] = -1_000 + elif kind == "big_loss": + # A loss larger than the pay. + adult["self_employment_income"] = -20_000 if outside_family: adult["current_education"] = "NOT_IN_EDUCATION" return adult @@ -138,6 +145,11 @@ def label(units): event("self-employed") if any(a.get("self_employment_income", 0) < 0 for a in adults_): event("self-employment loss beside pay") + if any( + a.get("self_employment_income", 0) > 0 and a.get("employment_income", 0) > 0 + for a in adults_ + ): + event("pay and profit together") SETTINGS = settings( @@ -161,10 +173,19 @@ def test_screens_match_a_family_by_family_reading(units, year): event("some ESA award passes") if jsa.any(): event("some JSA award passes") + earnings = sim.calculate("esa_exempt_work_earnings", year) parameters = sim.tax_benefit_system.parameters(year) + start = 0 for i, (couple, children, others, capital) in enumerate(units): assert esa[i] == esa_screen(couple, others, capital, parameters), units[i] assert jsa[i] == jsa_screen(couple, others, capital, parameters), units[i] + members = couple + children + others + for j, member in enumerate(members): + if member in children: + continue + expected = esa_weekly_earnings(member, parameters) + assert abs(earnings[start + j] - expected) < 0.01, (member, expected) + start += len(members) @st.composite diff --git a/policyengine_uk/tests/test_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index 8f19df33a..be4cb6cf5 100644 --- a/policyengine_uk/tests/test_legacy_benefit_parameters.py +++ b/policyengine_uk/tests/test_legacy_benefit_parameters.py @@ -110,3 +110,15 @@ def test_esa_and_jsa_remunerative_work_parameters_by_date(): for year in range(2015, 2031): assert dwp.JSA.hours.single(str(year)) == work.claimant_hours(str(year)) assert dwp.JSA.hours.couple(str(year)) == work.partner_hours(str(year)) + + +def test_esa_self_employment_class_2_by_fiscal_year(): + # Reg 99(3)(a): Class 2 from the small profits threshold, from the lower + # profits threshold from 2022-23 (SI 2022/1329), omitted from 2024-25 (SI + # 2024/377). Fiscal-year values are read at 30 April. + rule = CountryTaxBenefitSystem().parameters.gov.dwp.ESA.income + rule = rule.self_employment_class_2 + assert rule.deducted("2008-10-27") and rule.deducted("2023") + assert not rule.deducted("2024") and not rule.deducted("2026") + assert not rule.above_lower_profits_threshold("2021") + assert rule.above_lower_profits_threshold("2022") diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index 43e27c42f..8d3c7e263 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -14,24 +14,27 @@ class esa_exempt_work_earnings(Variable): "Class 1 contributions deducted from it and half the person's " "occupational and personal pension contributions. The regulation " "takes the tax actually deducted, which the model's inputs do not " - "record; the model approximates it by the income tax on the pay taken " - "as the lowest slice of the person's non-savings income, so tax on a " - "pension, a taxable benefit, self-employment, property, savings or " - "dividends never comes off the pay. A second job taxed at the basic " - "rate through PAYE would leave lower net earnings than this. " + "record. The model approximates it by PAYE on the pay alone: the " + "rates on taxable pay less employee pension contributions (net pay " + "arrangements) and the personal and blind person's allowances, less " + "tax reductions such as the married couple's allowance. Tax on other " + "income never comes off the pay, and a self-employment loss cannot " + "add to it. A second job taxed at the basic rate through PAYE would " + "leave lower net earnings than this. " "Self-employment (reg 98(3) and reg 99): the profit less a notional " "income tax at the basic rate (the Scottish basic rate for a Scottish " - "taxpayer) on the profit above the personal allowance, notional " - "Class 4 contributions at the main rate between the lower and upper " - "profits limits, and half the personal pension contributions. The " - "model gives personal pension contributions to self-employment when " - "there is a profit and to employment otherwise. A loss in one " - "employment is not set against earnings from another (reg 98(11)), " - "so a self-employment loss counts as nil. Personal reliefs other " - "than the personal allowance (reg 99(1)) are not modelled. Enter this " - "variable directly for actual net earnings, for example where PAYE " - "deducted a different amount or the regulations' averaging periods " - "give a different figure." + "taxpayer) on the profit above the personal and blind person's " + "allowances, notional Class 4 contributions at the main rate between " + "the lower and upper profits limits, notional Class 2 contributions " + "until 5 April 2024 (reg 99(3)(a)), and half the personal pension " + "contributions. The model gives personal pension contributions to " + "self-employment when there is a profit and to employment otherwise. " + "A loss in one employment is not set against earnings from another " + "(reg 98(11)), so a self-employment loss counts as nil. The " + "transferable (marriage) allowance and other reliefs reg 99(1) " + "allows are not applied. Enter this variable directly for actual net " + "earnings, for example where PAYE deducted a different amount or the " + "regulations' averaging periods give a different figure." ) definition_period = YEAR unit = GBP @@ -44,48 +47,34 @@ class esa_exempt_work_earnings(Variable): ) def formula(person, period, parameters): - income_tax = parameters(period).gov.hmrc.income_tax + p = parameters(period) + income_tax = p.gov.hmrc.income_tax rates = income_tax.rates scottish = person("pays_scottish_income_tax", period) + allowances = add( + person, period, ["personal_allowance", "blind_persons_allowance"] + ) profit = max_(0, person("self_employment_income", period)) personal_pension = person("personal_pension_contributions", period) personal_pension_from_profit = where(profit > 0, personal_pension, 0) + employee_pension = person("employee_pension_contributions", period) - # Employment, reg 96(3). Income tax on the pay as the lowest slice of - # non-savings income: the part of earned_taxable_income left after - # the person's other non-savings income is taken off the top. - # earned_taxable_income is adjusted net income less the incomes and - # allowances on the exclusions list. + # Employment, reg 96(3): PAYE on the pay alone. pay = person("employment_income", period) - exclusions = list(income_tax.earned_taxable_income_exclusions) - excluded_incomes = [ - variable - for variable in exclusions - if variable in income_tax.adjusted_net_income_components - ] - pay_in_base = ( - 0 - if "taxable_employment_income" in exclusions - else person("taxable_employment_income", period) - ) - income_in_base = person("adjusted_net_income", period) - add( - person, period, excluded_incomes + taxable_pay = person("taxable_employment_income", period) + taxable_pay_after_pension = max_( + 0, taxable_pay - min_(employee_pension, taxable_pay) ) - other_income = max_(0, income_in_base - pay_in_base) - taxable_pay = max_(0, person("earned_taxable_income", period) - other_income) + taxed_pay = max_(0, taxable_pay_after_pension - allowances) tax_on_pay = where( scottish, - rates.scotland.rates.calc(taxable_pay), - rates.uk.calc(taxable_pay), + rates.scotland.rates.calc(taxed_pay), + rates.uk.calc(taxed_pay), ) - # Tax reductions (for example the married couple's allowance) come - # off the tax on pay first, as allowances do. reductions = add(person, period, income_tax.income_tax_subtractions) tax_on_pay = max_(0, tax_on_pay - reductions) pension_from_pay = ( - person("employee_pension_contributions", period) - + personal_pension - - personal_pension_from_profit + employee_pension + personal_pension - personal_pension_from_profit ) net_pay = max_( 0, @@ -96,16 +85,41 @@ def formula(person, period, parameters): ) # Self-employment, reg 98(3) and reg 99. Reg 99(1): tax at the basic - # rate (the first UK band; the second Scottish band, after the - # starter rate) on the profit less the personal allowance. - basic_rate = where(scottish, rates.scotland.rates.rates[1], rates.uk.rates[0]) - notional_tax = basic_rate * max_( - 0, profit - person("personal_allowance", period) + # rate on the profit less the personal reliefs. The UK basic rate is + # the first UK band; the Scottish basic rate is the first Scottish + # band, or the second once the starter rate came in on 6 April 2018. + scottish_rates = rates.scotland.rates.rates + scottish_basic_rate = ( + scottish_rates[1] if period.start.year >= 2018 else scottish_rates[0] ) + basic_rate = where(scottish, scottish_basic_rate, rates.uk.rates[0]) + notional_tax = basic_rate * max_(0, profit - allowances) # Reg 99(3)(b): Class 4 at the main rate between the profits limits. notional_class_4 = person("ni_class_4_main", period) + # Reg 99(3)(a), until 5 April 2024: Class 2 at the weekly rate unless + # the profit is below the small profits threshold or, from 6 April + # 2022, at or below the lower profits threshold (SSCBA 1992 s.11(4), + # which the model takes as the Class 4 lower profits limit). + nics = p.gov.hmrc.national_insurance + class_2_rule = p.gov.dwp.ESA.income.self_employment_class_2 + class_2_due = where( + class_2_rule.above_lower_profits_threshold, + profit > nics.class_4.thresholds.lower_profits_limit, + profit >= nics.class_2.small_profits_threshold, + ) + notional_class_2 = ( + class_2_rule.deducted + * class_2_due + * person("ni_liable", period) + * nics.class_2.flat_rate + * WEEKS_IN_YEAR + ) net_profit = max_( 0, - profit - notional_tax - notional_class_4 - personal_pension_from_profit / 2, + profit + - notional_tax + - notional_class_4 + - notional_class_2 + - personal_pension_from_profit / 2, ) return (net_pay + net_profit) / WEEKS_IN_YEAR From 17ebd16be44c33de301310c5ca8b634fae6e9e22 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 05:52:56 -0400 Subject: [PATCH 05/11] Apply review r3: keep loss and tax-return credits out of the tax on pay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - The pay side takes only the married couple's allowance reduction, up to the tax on the pay itself, instead of the shared income_tax_subtractions. The shared capped_mcad is capped against the whole liability, which a loss moves through negative pension relief, and other_tax_credits holds credits settled through a tax return, such as foreign tax credit relief. - YAML: the married couple's allowance with a £20,000 loss, and a foreign tax credit on property income. - Documentation: the notional National Insurance follows the model's Class 2 and Class 4 parameters. Their 2015-16 and 2023-24 values are out of date; a separate fix is chip task_21085692. Co-Authored-By: Claude Opus 5.5 --- .../esa_income_remunerative_work.yaml | 52 +++++++++++++++++++ .../gov/dwp/esa_exempt_work_earnings.py | 20 ++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index d6779d851..d7943fb2f 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -435,6 +435,58 @@ esa_income_claimant_remunerative_work: true esa_income_eligible: false +- name: A self-employment loss does not change the married couple's allowance on pay + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 13_000 + personal_pension_contributions: 5_500 + married_couples_allowance: 4_360 + self_employment_income: -20_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Tax on the pay: 20% x (£13,000 - £12,570) = £86, less the 10% x £4,360 + # = £436 reduction, so nil. Class 1 £34.40; half the premium £2,750. + # (£13,000 - £34.40 - £2,750) / 52 = £196.45, with or without the loss. + esa_exempt_work_earnings: 196.45 + esa_income_claimant_remunerative_work: true + +- name: A foreign tax credit on other income does not reduce the tax on pay + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 13_000 + personal_pension_contributions: 5_500 + property_income: 5_000 + other_tax_credits: 1_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # (£13,000 - £86 - £34.40 - £2,750) / 52 = £194.80: the credit is settled + # through a tax return, not deducted from the pay. + esa_exempt_work_earnings: 194.80 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + # Para 6(1)(f): the partner's work, 24 hours (reg 42(1)). - name: A partner working 23 hours a week does not end the award diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index 8d3c7e263..97b0f5c42 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -17,9 +17,11 @@ class esa_exempt_work_earnings(Variable): "record. The model approximates it by PAYE on the pay alone: the " "rates on taxable pay less employee pension contributions (net pay " "arrangements) and the personal and blind person's allowances, less " - "tax reductions such as the married couple's allowance. Tax on other " - "income never comes off the pay, and a self-employment loss cannot " - "add to it. A second job taxed at the basic rate through PAYE would " + "the married couple's allowance reduction up to that tax. Tax on " + "other income never comes off the pay, a self-employment loss cannot " + "change it, and credits settled through a tax return (such as foreign " + "tax credit relief) are not taken off it. A second job taxed at the " + "basic rate through PAYE would " "leave lower net earnings than this. " "Self-employment (reg 98(3) and reg 99): the profit less a notional " "income tax at the basic rate (the Scottish basic rate for a Scottish " @@ -30,7 +32,9 @@ class esa_exempt_work_earnings(Variable): "contributions. The model gives personal pension contributions to " "self-employment when there is a profit and to employment otherwise. " "A loss in one employment is not set against earnings from another " - "(reg 98(11)), so a self-employment loss counts as nil. The " + "(reg 98(11)), so a self-employment loss counts as nil. The notional " + "National Insurance reads the model's Class 2 and Class 4 parameters, " + "so it is only as right as they are for each year. The " "transferable (marriage) allowance and other reliefs reg 99(1) " "allows are not applied. Enter this variable directly for actual net " "earnings, for example where PAYE deducted a different amount or the " @@ -71,8 +75,12 @@ def formula(person, period, parameters): rates.scotland.rates.calc(taxed_pay), rates.uk.calc(taxed_pay), ) - reductions = add(person, period, income_tax.income_tax_subtractions) - tax_on_pay = max_(0, tax_on_pay - reductions) + # The married couple's allowance reduction, given through the tax + # code, up to the tax on the pay itself. The shared capped_mcad is + # capped against the person's whole liability, which other income + # and losses move. + mca_reduction = person("married_couples_allowance_deduction", period) + tax_on_pay = max_(0, tax_on_pay - mca_reduction) pension_from_pay = ( employee_pension + personal_pension - personal_pension_from_profit ) From e38ea0c7d87134f7838902b49c1ee41dc049e834 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 07:17:38 -0400 Subject: [PATCH 06/11] Apply review r4: statutory NI amounts and pay-only deductions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Correct the dated National Insurance amounts reg 99's notional contributions read: the 2015-16/2016-17 Class 2 small profits threshold (£5,965), the 2023-24 Class 2 rate (£3.45) and lower profits limit (£12,570), the 2022-23 Class 4 rates (9.73%/2.73%, Health and Social Care Levy (Repeal) Act 2022 s.2(2)) and the 2023-24 additional rate (2%). The four 2023 NI tests now take hand-derived statutory values, including the reg 100 annual maximum (£1,748.60). - Take the ESA pay's tax and primary Class 1 on the pay alone: statutory sick, maternity and paternity pay are not earnings (reg 95(2)(b)), so the shared taxable pay and Class 1 bases, which include them, are not used. - YAML: SSP/SMP/SPP controls, a shared-base isolation control, the 2023 regression and threshold-equality cases for 2023-24 and 2015-16. - Properties draw statutory payments; describe ages on both sides of pension age and the JSA carer rule as no exception for carers doing unrelated paid work. Co-Authored-By: Claude Opus 5.5 --- .../esa-jsa-remunerative-work.fixed.md | 5 +- .../national_insurance/class_2/flat_rate.yaml | 6 + .../class_2/small_profits_threshold.yaml | 7 +- .../class_4/rates/additional.yaml | 16 +- .../class_4/rates/main.yaml | 16 +- .../thresholds/lower_profits_limit.yaml | 6 + .../tests/legacy_award_work_reference.py | 30 ++- .../esa_income_remunerative_work.yaml | 243 +++++++++++++++++- .../class_2/ni_class_2.yaml | 4 +- .../class_4/ni_class_4.yaml | 6 +- .../class_4/ni_class_4_main.yaml | 6 +- .../class_4/ni_class_4_maximum.yaml | 17 +- .../test_legacy_award_work_properties.py | 33 ++- .../gov/dwp/esa_exempt_work_earnings.py | 57 ++-- 14 files changed, 390 insertions(+), 62 deletions(-) diff --git a/changelog.d/esa-jsa-remunerative-work.fixed.md b/changelog.d/esa-jsa-remunerative-work.fixed.md index e8cabe070..d357a1fc1 100644 --- a/changelog.d/esa-jsa-remunerative-work.fixed.md +++ b/changelog.d/esa-jsa-remunerative-work.fixed.md @@ -1,3 +1,4 @@ -- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less PAYE on the pay alone, primary Class 1 and half the pension contributions (reg 96(3)), plus a self-employed profit less reg 99's notional basic-rate tax above the personal and blind person's allowances, main-rate Class 4, Class 2 until 5 April 2024 (reg 99(3)(a); new `gov.dwp.ESA.income.self_employment_class_2.{deducted, above_lower_profits_threshold}`) and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. -- End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). A member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (reg 3E(2)(g)), so 24 hours is the limit for the other member of any couple. The JSA regulations have no carer exception. Hours come from the new `jsa_remunerative_work_hours`. +- End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less PAYE and primary Class 1 on the pay alone and half the pension contributions (reg 96(3)), with statutory sick, maternity and paternity pay, which are not earnings (reg 95(2)(b)), adding neither to the pay nor to the tax and Class 1 taken off it, plus a self-employed profit less reg 99's notional basic-rate tax above the personal and blind person's allowances, main-rate Class 4, Class 2 until 5 April 2024 (reg 99(3)(a); new `gov.dwp.ESA.income.self_employment_class_2.{deducted, above_lower_profits_threshold}`) and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. +- End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). A member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (reg 3E(2)(g)), so 24 hours is the limit for the other member of any couple. The JSA regulations have no exception for carers doing unrelated paid work. Hours come from the new `jsa_remunerative_work_hours`. - The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours` and `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. +- Bring dated National Insurance amounts, which reg 99's notional contributions read, into line with the statute: the Class 2 small profits threshold for 2015-16 and 2016-17 (£5,965; SSCBA 1992 s.11(4)), the 2023-24 Class 2 rate (£3.45; s.11(2)) and Class 4 lower profits limit (£12,570; s.15(3)), the 2022-23 Class 4 rates (9.73% and 2.73% for the whole year; Health and Social Care Levy (Repeal) Act 2022 s.2(2)) and the 2023-24 Class 4 additional rate (2%; s.15(3ZA)). The 2023 Class 2, Class 4 and annual maximum tests now take the statutory amounts. diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml index ff3774848..612d4192b 100644 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/flat_rate.yaml @@ -11,6 +11,12 @@ values: reference: - title: The Social Security (Contributions) (Rates, Limits and Thresholds Amendments and National Insurance Funds Payments) Regulations 2022(3)(a) href: https://www.legislation.gov.uk/uksi/2022/232/regulation/3/made#regulation-3-a + 2023-04-06: + value: 3.45 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s. 11(2), as at 6 April 2023 + href: https://www.legislation.gov.uk/ukpga/1992/4/section/11/2023-04-06 2024-01-01: value: 0 metadata: diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/small_profits_threshold.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/small_profits_threshold.yaml index 25af29997..888e3f5a7 100644 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/small_profits_threshold.yaml +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_2/small_profits_threshold.yaml @@ -1,6 +1,11 @@ description: Small profits National Insurance threshold for self-employed earners. values: - 2015-04-06: 3_965 + 2015-04-06: + value: 5_965 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s. 11(4), as at 6 April 2015 + href: https://www.legislation.gov.uk/ukpga/1992/4/section/11/2015-04-06 2017-04-06: 6_025 2018-04-06: 6_205 2019-04-06: 6_365 diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml index 5326d9ae3..833d95d91 100644 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/additional.yaml @@ -1,12 +1,20 @@ description: The additional National Insurance rate paid above the Upper Profits Limit for self-employed profits. values: 2015-04-01: 0.02 - 2022-04-01: - value: 0.035 + # Class 4 is charged on the year's profits, so the 2022-23 rate is one + # rate for the whole tax year. + 2022-04-06: + value: 0.0273 metadata: reference: - - title: Autumn Budget and Spending Review 2022 - href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf + - title: Health and Social Care Levy (Repeal) Act 2022 s. 2(2)(b) + href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 + 2023-04-06: + value: 0.02 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s. 15(3ZA)(b), as at 6 April 2023 + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 2024-01-01: value: 0.02 metadata: diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml index 1ef51cea6..6a88e93cb 100644 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/rates/main.yaml @@ -1,18 +1,20 @@ description: The main National Insurance rate paid between the Lower and Upper Profits Limits for self-employed profits. values: 2015-04-01: 0.09 - 2022-04-01: - value: 0.1025 + # Class 4 is charged on the year's profits, so the 2022-23 rate is one + # rate for the whole tax year. + 2022-04-06: + value: 0.0973 metadata: reference: - - title: Autumn Budget and Spending Review 2022 - href: https://assets.publishing.service.gov.uk/government/uploads/system/uploads/attachment_data/file/1043689/Budget_AB2021_Web_Accessible.pdf - 2022-11-06: + - title: Health and Social Care Levy (Repeal) Act 2022 s. 2(2)(a) + href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 + 2023-04-06: value: 0.09 metadata: reference: - - title: Health and Social Care Levy Act (Repeal) 2022 - href: https://www.legislation.gov.uk/ukpga/2022/43/section/2 + - title: Social Security Contributions and Benefits Act 1992 s. 15(3ZA)(a), as at 6 April 2023 + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 2024-04-04: value: 0.06 metadata: diff --git a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml index 9aa9dae5a..69bfa017f 100644 --- a/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml +++ b/policyengine_uk/parameters/gov/hmrc/national_insurance/class_4/thresholds/lower_profits_limit.yaml @@ -12,6 +12,12 @@ values: reference: - title: National Insurance Contributions (Increase of Thresholds) Act 2022 href: https://www.legislation.gov.uk/ukpga/2022/16/section/2/enacted + 2023-04-06: + value: 12_570 + metadata: + reference: + - title: Social Security Contributions and Benefits Act 1992 s. 15(3)(a), as at 6 April 2023 + href: https://www.legislation.gov.uk/ukpga/1992/4/section/15/2023-04-06 2024-01-01: value: 12_570 metadata: diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index 202a8655b..84bdfae81 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -5,8 +5,9 @@ test_income_support_eligibility_properties.py). Each adult is a dict of simulation inputs for one year: hours_worked (annual), employment_income, self_employment_income, employee_pension_contributions, -personal_pension_contributions, receives_carer_benefit, care_hours, age and -the reported awards. +personal_pension_contributions, statutory_sick_pay, statutory_maternity_pay, +statutory_paternity_pay, receives_carer_benefit, care_hours, age and the +reported awards. - Capital (WRA 2007 Sch 1 para 6(1)(b), ESA Regs reg 110; JSA 1995 s.13(1), JSA Regs reg 107): within £16,000, with £1 a week of tariff income for @@ -14,19 +15,22 @@ - ESA claimant (para 6(1)(e), ESA Regs reg 41(1)): paid work is remunerative unless it is exempt work: earnings of no more than £20 a week (reg 45(2)), or under 16 hours with earnings within the higher limit (reg 45(4)). - Pay is net of PAYE on the pay alone, primary Class 1 and half the pension - contributions (reg 96(3)). Self-employment is the profit less a notional - basic-rate tax on the profit above the personal allowance, notional - main-rate Class 4 (and Class 2 before 6 April 2024) and half the personal - pension contributions (reg 98(3), reg 99); a loss is not set against pay - (reg 98(11)). + Pay is net of PAYE and primary Class 1 on the pay alone and half the + pension contributions (reg 96(3)). Statutory sick, maternity and + paternity pay are not earnings (reg 95(2)(b)): they are ignored, and no + tax or Class 1 on them comes off the pay. Self-employment is the profit + less a notional basic-rate tax on the profit above the personal allowance, + notional main-rate Class 4 (and Class 2 before 6 April 2024) and half the + personal pension contributions (reg 98(3), reg 99); a loss is not set + 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)). -- JSA claimant (s.1(2)(e), reg 51(1)(a)): 16 hours or more, with no carer - exception. The other member of the couple: 24 hours or more (s.3(1)(e), - reg 51(1)(b)). In a joint-claim couple each member is a claimant at 16 - hours (s.1(2B)(b)), but a member may claim alone when the other works 16 to - under 24 hours (reg 3E(1), (2)(g)), so the limit is 24 either way. +- 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 + couple each member is a claimant at 16 hours (s.1(2B)(b)), but a member + may claim alone when the other works 16 to under 24 hours (reg 3E(1), + (2)(g)), so the limit is 24 either way. - The claimant is a member who reports the award; when the claimant or partner reports one, only they are candidates. """ diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index d7943fb2f..e83df89ed 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -13,7 +13,8 @@ # rounded up to the next 50p (reg 45(9A)): £195.50 in 2025-26 (16 x # £12.21 = £195.36) and £203.50 in 2026-27 (16 x £12.71 = £203.36). # Earnings are net (DMG 41189). Pay, reg 96(3): less the income tax and -# primary Class 1 deducted and half the pension contributions. Self-employment, +# primary Class 1 deducted and half the pension contributions; statutory sick, +# maternity and paternity pay are not earnings (reg 95(2)(b)). Self-employment, # reg 98(3) and reg 99: less a notional basic-rate tax on the profit above the # personal allowance, notional main-rate Class 4 and half the personal pension # contributions; a loss is not set against other earnings (reg 98(11)). @@ -487,6 +488,246 @@ esa_income_claimant_remunerative_work: false esa_income_eligible: true +# Statutory sick, maternity and paternity pay are not earnings (reg 95(2)(b)). +# £10,300 of pay is £198.08 a week, over £195.50, with no tax or Class 1 on +# it. The shared taxable employment income and Class 1 base include these +# payments, which would put tax and Class 1 on the pay and bring it under +# the limit. + +- name: Statutory sick pay adds no tax or Class 1 to the pay (reg 95(2)(b)) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 10_300 + statutory_sick_pay: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # £10,300 / 52 = £198.08. Taxing the £13,300 the shared base holds would + # take £146 of tax and £58.40 of Class 1 and give £194.15. + esa_exempt_work_earnings: 198.08 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: Statutory maternity pay adds no tax or Class 1 to the pay (reg 95(2)(b)) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 10_300 + statutory_maternity_pay: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # £10,300 / 52 = £198.08. + esa_exempt_work_earnings: 198.08 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: Statutory paternity pay adds no Class 1 to the pay (reg 95(2)(b)) + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 10_300 + statutory_paternity_pay: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # £10,300 / 52 = £198.08. The shared Class 1 base would take + # 8% x (£13,300 - 52 x £241.73) = £58.40. + esa_exempt_work_earnings: 198.08 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: The pay's tax and Class 1 do not read the shared bases + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + employment_income: 10_300 + taxable_employment_income: 13_300 + ni_class_1_income: 13_300 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Both shared bases are set to £13,300; the pay is still £10,300, so + # £198.08. + esa_exempt_work_earnings: 198.08 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +# Reg 99(3) with the dated SSCBA 1992 amounts. 2023-24 (s.11 and s.15 as at +# 6 April 2023): Class 2 of £3.45 a week only on profits over the £12,570 +# lower profits threshold, Class 4 at 9% from £12,570; the higher limit is +# £167. 2015-16 (s.11 as at 6 April 2015): Class 2 of £2.80 a week on +# profits of, or over, the £5,965 small profits threshold, Class 4 at 9% from +# £8,060; the personal allowance is £10,600 and the higher limit £104. + +- name: In 2023-24 no Class 2 or Class 4 is due below the lower profits threshold + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 12_500 + personal_pension_contributions: 7_300 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # No notional tax (under £12,570), Class 4 or Class 2. Half the premium, + # £3,650. (£12,500 - £3,650) / 52 = £170.19, over £167. + esa_exempt_work_earnings: 170.19 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: In 2023-24 a profit equal to the lower profits threshold pays no Class 2 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 12_570 + personal_pension_contributions: 7_600 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Class 2 is due only on profits that exceed £12,570. Half the premium, + # £3,800. (£12,570 - £3,800) / 52 = £168.65, over £167. Deducting Class 2 + # would give £165.20. + esa_exempt_work_earnings: 168.65 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: In 2023-24 a profit over the lower profits threshold pays Class 2 at £3.45 + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 13_000 + personal_pension_contributions: 8_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Tax 20% x £430 = £86. Class 4 9% x £430 = £38.70. Class 2 52 x £3.45 = + # £179.40. Half the premium, £4,000. + # (£13,000 - £86 - £38.70 - £179.40 - £4,000) / 52 = £167.23, over £167. + esa_exempt_work_earnings: 167.23 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: In 2015-16 no Class 2 is due below the small profits threshold + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 5_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Under £5,965, so no Class 2; no tax or Class 4 either. + # £5,000 / 52 = £96.15, within £104. + esa_exempt_work_earnings: 96.15 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + +- name: In 2015-16 a profit equal to the small profits threshold pays Class 2 + period: 2015 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 5_965 + personal_pension_contributions: 1_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Class 2 is due on profits of £5,965: 52 x £2.80 = £145.60. Half the + # premium, £500. (£5,965 - £145.60 - £500) / 52 = £102.30, within £104. + # Without the Class 2 it would be £105.10. + esa_exempt_work_earnings: 102.3 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + # Para 6(1)(f): the partner's work, 24 hours (reg 42(1)). - name: A partner working 23 hours a week does not end the award diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml index 3a458e7fa..15620c336 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_2/ni_class_2.yaml @@ -6,8 +6,10 @@ ni_class_2: 0 - name: NI Class 2 - over LPL. + # SSCBA 1992 s.11(2) as at 6 April 2023: profits over the £12,570 lower + # profits threshold pay £3.45 a week. period: 2023 input: self_employment_income: 15_000 output: - ni_class_2: 3.15 * 52 + ni_class_2: 3.45 * 52 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml index 7e59dec61..4f6292b50 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4.yaml @@ -1,10 +1,12 @@ - name: NI Class 4 - under UPL. + # SSCBA 1992 s.15(3) and (3ZA) as at 6 April 2023: 9% of profits between + # £12,570 and £50,270. (30,000 - 12,570) x 0.09 = 1,568.70. period: 2023 - absolute_error_margin: 1 + absolute_error_margin: 0.01 input: self_employment_income: 30_000 output: - ni_class_4: 1628 + ni_class_4: 1_568.70 - name: NI Class 4 - 2026 lower profits limit is frozen. period: 2026 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml index c4189d2a6..32ab71f12 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_main.yaml @@ -1,7 +1,9 @@ - name: NI Class 4 - over UPL has maximum capped. + # SSCBA 1992 s.15(3)(a) and (3ZA)(a) as at 6 April 2023: 9% of profits + # between £12,570 and £50,270. (50,270 - 12,570) x 0.09 = 3,393. period: 2023 - absolute_error_margin: 1 + absolute_error_margin: 0.01 input: self_employment_income: 100_000 output: - ni_class_4_main: 3452 + ni_class_4_main: 3_393 diff --git a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml index f1f8ea999..f88060bbe 100644 --- a/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/hmrc/national_insurance/class_4/ni_class_4_maximum.yaml @@ -1,8 +1,21 @@ - name: NI Class 4 with employment and self-employment income - over UPL has maximum capped. + # SI 2001/1004 reg 100(3) as at 6 April 2023, with the SSCBA 1992 s.11 + # and s.15 amounts for 2023-24 (lower profits limit £12,570, upper + # £50,270, main 9%, additional 2%, Class 2 £3.45 a week). + # Step 1: 50,270 - 12,570 = 37,700. Step 2: 37,700 x 9% = 3,393. + # Step 3: 3,393 + 53 x 3.45 = 3,575.85. + # Step 4: less Class 2 of 52 x 3.45 = 179.40 and primary Class 1 at the + # main percentage on £100,000 of pay, which is at least 10% of the + # £37,700 between the primary threshold and the upper earnings limit + # (£3,770), so the result is negative: Case 3, treated as nil. + # Step 5: 0 x 100/9 = 0. Step 6: min(50,270, 100,000) - 12,570 = 37,700. + # Step 7: 37,700 - 0 = 37,700. + # Step 8: 37,700 x 2% = 754. Step 9: (100,000 - 50,270) x 2% = 994.60. + # Maximum: 0 + 754 + 994.60 = 1,748.60. period: 2023 - absolute_error_margin: 1 + absolute_error_margin: 0.01 input: self_employment_income: 100_000 employment_income: 100_000 output: - ni_class_4_maximum: 3_083.22 + ni_class_4_maximum: 1_748.60 diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 559962c3f..40d2ca646 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -11,17 +11,19 @@ the conditions only ever bar a claim; - caring never ends an ESA award (a carer partner is not treated as in remunerative work, ESA Regs reg 43(2)(c)) and never matters for JSA (the - JSA regulations have no carer exception); + JSA regulations have no exception for carers doing unrelated paid work); - when the claimant or partner reports the award, adding a member outside the family (a non-dependent adult, with any award and any work) never changes either screen. Adults are employees (pay within the basic rate band), self-employed (a -profit within the Class 4 upper limit, under state pension age) or employees -with a self-employment loss, with no other income: the scope the reference -covers. Roles are given explicitly (is_claimant_or_partner). Each example -builds many families in one simulation, in separate households and benefit -units. +profit within the Class 4 upper limit) or employees with a self-employment +loss, aged either side of state pension age, where National Insurance stops. +Some also receive statutory sick, maternity or paternity pay, which is not +earnings (ESA Regs reg 95(2)(b)); they have no other income. This is the +scope the reference covers. Roles are given explicitly +(is_claimant_or_partner). Each example builds many families in one +simulation, in separate households and benefit units. """ import numpy as np @@ -39,6 +41,16 @@ # Profits around the limits once reg 99's notional tax and Class 4 come off, # and £13,200, which needs half a £5,900 pension premium to fall within them. PROFIT = [1_040, 5_200, 10_166, 10_300, 13_200, 15_600] +# Payments an employer makes for sickness or leave, which are not earnings +# (reg 95(2)(b)) but which the shared taxable pay and Class 1 bases include. +STATUTORY_PAY = [ + None, + None, + None, + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", +] @st.composite @@ -77,6 +89,9 @@ def adults(draw, outside_family=False): elif kind == "big_loss": # A loss larger than the pay. adult["self_employment_income"] = -20_000 + statutory_pay = draw(st.sampled_from(STATUTORY_PAY)) + if statutory_pay is not None: + adult[statutory_pay] = draw(st.sampled_from([1_040, 3_000])) if outside_family: adult["current_education"] = "NOT_IN_EDUCATION" return adult @@ -150,6 +165,12 @@ def label(units): for a in adults_ ): event("pay and profit together") + if any( + a.get("employment_income", 0) > 0 + and any(a.get(name, 0) > 0 for name in STATUTORY_PAY if name) + for a in adults_ + ): + event("statutory pay beside pay") SETTINGS = settings( diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index 97b0f5c42..052962005 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -13,16 +13,22 @@ class esa_exempt_work_earnings(Variable): "Employment (reg 96(3)): gross pay less the income tax and primary " "Class 1 contributions deducted from it and half the person's " "occupational and personal pension contributions. The regulation " - "takes the tax actually deducted, which the model's inputs do not " - "record. The model approximates it by PAYE on the pay alone: the " - "rates on taxable pay less employee pension contributions (net pay " - "arrangements) and the personal and blind person's allowances, less " - "the married couple's allowance reduction up to that tax. Tax on " - "other income never comes off the pay, a self-employment loss cannot " - "change it, and credits settled through a tax return (such as foreign " - "tax credit relief) are not taken off it. A second job taxed at the " - "basic rate through PAYE would " - "leave lower net earnings than this. " + "takes the tax and contributions actually deducted, which the " + "model's inputs do not record. The model approximates them on the " + "pay alone. Income tax: the rates on the pay less employment " + "expenses and employee pension contributions (net pay arrangements), " + "above the personal and blind person's allowances, less the married " + "couple's allowance reduction up to that tax. The allowances are the " + "person's own, so other income can still reduce them through the " + "personal allowance taper, but the tax on other income never comes " + "off the pay, a self-employment loss cannot change it, and credits " + "settled through a tax return (such as foreign tax credit relief) are " + "not taken off it. Class 1: the main and additional primary rates on " + "the pay between the primary threshold and the upper earnings limit " + "and above it. Statutory sick, maternity and paternity pay are not " + "earnings (reg 95(2)(b)), so they add neither to the pay nor to the " + "tax and contributions taken off it. A second job taxed at the basic " + "rate through PAYE would leave lower net earnings than this. " "Self-employment (reg 98(3) and reg 99): the profit less a notional " "income tax at the basic rate (the Scottish basic rate for a Scottish " "taxpayer) on the profit above the personal and blind person's " @@ -33,10 +39,9 @@ class esa_exempt_work_earnings(Variable): "self-employment when there is a profit and to employment otherwise. " "A loss in one employment is not set against earnings from another " "(reg 98(11)), so a self-employment loss counts as nil. The notional " - "National Insurance reads the model's Class 2 and Class 4 parameters, " - "so it is only as right as they are for each year. The " - "transferable (marriage) allowance and other reliefs reg 99(1) " - "allows are not applied. Enter this variable directly for actual net " + "National Insurance reads the model's Class 2 and Class 4 parameters " + "for the year. The transferable (marriage) allowance and other " + "reliefs reg 99(1) allows are not applied. Enter this variable directly for actual net " "earnings, for example where PAYE deducted a different amount or the " "regulations' averaging periods give a different figure." ) @@ -44,6 +49,7 @@ class esa_exempt_work_earnings(Variable): unit = GBP reference = ( "https://www.legislation.gov.uk/uksi/2008/794/regulation/45", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/95", "https://www.legislation.gov.uk/uksi/2008/794/regulation/96", "https://www.legislation.gov.uk/uksi/2008/794/regulation/98", "https://www.legislation.gov.uk/uksi/2008/794/regulation/99", @@ -63,9 +69,12 @@ def formula(person, period, parameters): personal_pension_from_profit = where(profit > 0, personal_pension, 0) employee_pension = person("employee_pension_contributions", period) - # Employment, reg 96(3): PAYE on the pay alone. + # Employment, reg 96(3): PAYE and primary Class 1 on the pay alone. + # Statutory sick, maternity and paternity pay are not earnings (reg + # 95(2)(b)), so the shared taxable employment income and Class 1 + # base, which include them, are not used. pay = person("employment_income", period) - taxable_pay = person("taxable_employment_income", period) + taxable_pay = max_(0, pay - person("employment_deductions", period)) taxable_pay_after_pension = max_( 0, taxable_pay - min_(employee_pension, taxable_pay) ) @@ -81,15 +90,22 @@ def formula(person, period, parameters): # and losses move. mca_reduction = person("married_couples_allowance_deduction", period) tax_on_pay = max_(0, tax_on_pay - mca_reduction) + nics = p.gov.hmrc.national_insurance + class_1 = nics.class_1 + primary_threshold = class_1.thresholds.primary_threshold * WEEKS_IN_YEAR + upper_earnings_limit = class_1.thresholds.upper_earnings_limit * WEEKS_IN_YEAR + pay_above_upper_limit = max_(0, pay - upper_earnings_limit) + class_1_on_pay = person("ni_liable", period) * ( + class_1.rates.employee.main + * (max_(0, pay - primary_threshold) - pay_above_upper_limit) + + class_1.rates.employee.additional * pay_above_upper_limit + ) pension_from_pay = ( employee_pension + personal_pension - personal_pension_from_profit ) net_pay = max_( 0, - pay - - tax_on_pay - - person("ni_class_1_employee", period) - - pension_from_pay / 2, + pay - tax_on_pay - class_1_on_pay - pension_from_pay / 2, ) # Self-employment, reg 98(3) and reg 99. Reg 99(1): tax at the basic @@ -108,7 +124,6 @@ def formula(person, period, parameters): # the profit is below the small profits threshold or, from 6 April # 2022, at or below the lower profits threshold (SSCBA 1992 s.11(4), # which the model takes as the Class 4 lower profits limit). - nics = p.gov.hmrc.national_insurance class_2_rule = p.gov.dwp.ESA.income.self_employment_class_2 class_2_due = where( class_2_rule.above_lower_profits_threshold, From 232358d342f67cfd835ac24bc8f9e15a7a77b4ed Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 09:38:11 -0400 Subject: [PATCH 07/11] Apply review r5 nits: label the 2023 Class 2 cliff, disclose NI effects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - YAML: in 2023-24, £1 of profit over the lower profits threshold brings reg 99(3)(a)'s fixed Class 2 in and the work under the £167 limit (£165.22 against £168.65). This is the intended statutory cliff, labelled as such. - The monotonicity property's docstring now names its tested years (2025-26) and this pre-April-2024 exception. - Changelog: the shared NI corrections also change historical NI, net income and means tests in those years. Co-Authored-By: Claude Opus 5.5 --- .../esa-jsa-remunerative-work.fixed.md | 2 +- .../esa_income_remunerative_work.yaml | 31 +++++++++++++++++++ .../test_legacy_award_work_properties.py | 6 ++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/changelog.d/esa-jsa-remunerative-work.fixed.md b/changelog.d/esa-jsa-remunerative-work.fixed.md index d357a1fc1..d4ec80486 100644 --- a/changelog.d/esa-jsa-remunerative-work.fixed.md +++ b/changelog.d/esa-jsa-remunerative-work.fixed.md @@ -1,4 +1,4 @@ - End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less PAYE and primary Class 1 on the pay alone and half the pension contributions (reg 96(3)), with statutory sick, maternity and paternity pay, which are not earnings (reg 95(2)(b)), adding neither to the pay nor to the tax and Class 1 taken off it, plus a self-employed profit less reg 99's notional basic-rate tax above the personal and blind person's allowances, main-rate Class 4, Class 2 until 5 April 2024 (reg 99(3)(a); new `gov.dwp.ESA.income.self_employment_class_2.{deducted, above_lower_profits_threshold}`) and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. - End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). A member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (reg 3E(2)(g)), so 24 hours is the limit for the other member of any couple. The JSA regulations have no exception for carers doing unrelated paid work. Hours come from the new `jsa_remunerative_work_hours`. - The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours` and `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. -- Bring dated National Insurance amounts, which reg 99's notional contributions read, into line with the statute: the Class 2 small profits threshold for 2015-16 and 2016-17 (£5,965; SSCBA 1992 s.11(4)), the 2023-24 Class 2 rate (£3.45; s.11(2)) and Class 4 lower profits limit (£12,570; s.15(3)), the 2022-23 Class 4 rates (9.73% and 2.73% for the whole year; Health and Social Care Levy (Repeal) Act 2022 s.2(2)) and the 2023-24 Class 4 additional rate (2%; s.15(3ZA)). The 2023 Class 2, Class 4 and annual maximum tests now take the statutory amounts. +- Bring dated National Insurance amounts, which reg 99's notional contributions read, into line with the statute: the Class 2 small profits threshold for 2015-16 and 2016-17 (£5,965; SSCBA 1992 s.11(4)), the 2023-24 Class 2 rate (£3.45; s.11(2)) and Class 4 lower profits limit (£12,570; s.15(3)), the 2022-23 Class 4 rates (9.73% and 2.73% for the whole year; Health and Social Care Levy (Repeal) Act 2022 s.2(2)) and the 2023-24 Class 4 additional rate (2%; s.15(3ZA)). These also change National Insurance, net income and the means tests that read them in those years. The 2023 Class 2, Class 4 and annual maximum tests now take the statutory amounts. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index e83df89ed..edab4bf48 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -652,6 +652,37 @@ esa_income_eligible: false esa_income: 0 +- name: In 2023-24 £1 over the lower profits threshold brings Class 2 in and the work under the limit + # An intended cliff, not a defect: reg 99(3)(a)'s Class 2 is a fixed weekly + # amount once profit exceeds the threshold, so £1 more profit than the + # case above (£168.65, remunerative) lowers net earnings below £167. More + # pay or profit never helps from 6 April 2024, when Class 2 stops; the + # monotonicity property tests 2025 and 2026. + period: 2023 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 12_571 + personal_pension_contributions: 7_600 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Tax 20% x £1 = £0.20. Class 4 9% x £1 = £0.09. Class 2 52 x £3.45 = + # £179.40. Half the premium, £3,800. + # (£12,571 - £0.20 - £0.09 - £179.40 - £3,800) / 52 = £165.22, within £167. + esa_exempt_work_earnings: 165.22 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + - name: In 2023-24 a profit over the lower profits threshold pays Class 2 at £3.45 period: 2023 absolute_error_margin: 0.01 diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 40d2ca646..0710458cc 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -7,8 +7,10 @@ - esa_income_eligible and jsa_income_eligible equal a family-by-family reading of the law (legacy_award_work_reference); -- more hours or more pay for anyone never makes a family eligible, because - the conditions only ever bar a claim; +- more hours or more pay for anyone never makes a family eligible, in the + years tested (2025 and 2026). Before 6 April 2024 reg 99(3)(a) took a + fixed Class 2 deduction once profit reached a threshold, so £1 more + profit could lower net earnings; that intended cliff is a YAML case; - caring never ends an ESA award (a carer partner is not treated as in remunerative work, ESA Regs reg 43(2)(c)) and never matters for JSA (the JSA regulations have no exception for carers doing unrelated paid work); From a359287f4c4011d8b8953ffdcd1867cb756cd359 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 19:46:40 -0400 Subject: [PATCH 08/11] Qualify the ESA earnings notes on the allowance taper and statutory pay Final review of b9816e433 (nits 1-3): - esa_exempt_work_earnings: another trade's loss can change the pay's personal allowance through the taper; a self-employment loss is never deducted from the pay itself. - Shared bases: taxable employment income includes statutory sick and maternity pay; the Class 1 base (ni_class_1_income) includes all three. Same fix in the ESA YAML note and the property file's comment. - Property docstring: the pay monotonicity holds within the generated scope, where adults have no other income. Comments and documentation only. Co-Authored-By: Claude Opus 5.5 --- .../esa_income_remunerative_work.yaml | 7 +++--- .../test_legacy_award_work_properties.py | 12 ++++++--- .../gov/dwp/esa_exempt_work_earnings.py | 25 +++++++++++-------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index 21c7c56a3..f3fbd389b 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -552,9 +552,10 @@ # Statutory sick, maternity and paternity pay are not earnings (reg 95(2)(b)). # £10,300 of pay is £198.08 a week, over £195.50, with no tax or Class 1 on -# it. The shared taxable employment income and Class 1 base include these -# payments, which would put tax and Class 1 on the pay and bring it under -# the limit. +# it. The shared bases include these payments: taxable employment income +# includes statutory sick and maternity pay, and the Class 1 base all three. +# Using them would put tax or Class 1 on the pay and bring it under the +# limit. - name: Statutory sick pay adds no tax or Class 1 to the pay (reg 95(2)(b)) period: 2025 diff --git a/policyengine_uk/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py index 0710458cc..a608afd84 100644 --- a/policyengine_uk/tests/test_legacy_award_work_properties.py +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -8,9 +8,12 @@ - esa_income_eligible and jsa_income_eligible equal a family-by-family reading of the law (legacy_award_work_reference); - more hours or more pay for anyone never makes a family eligible, in the - years tested (2025 and 2026). Before 6 April 2024 reg 99(3)(a) took a - fixed Class 2 deduction once profit reached a threshold, so £1 more - profit could lower net earnings; that intended cliff is a YAML case; + years tested (2025 and 2026) and within the scope below. Before 6 April + 2024 reg 99(3)(a) took a fixed Class 2 deduction once profit reached a + threshold, so £1 more profit could lower net earnings; that intended + cliff is a YAML case. Outside the scope, other income can move the + personal allowance the pay's tax is worked out with (through the taper), + so this is not claimed there; - caring never ends an ESA award (a carer partner is not treated as in remunerative work, ESA Regs reg 43(2)(c)) and never matters for JSA (the JSA regulations have no exception for carers doing unrelated paid work); @@ -44,7 +47,8 @@ # and £13,200, which needs half a £5,900 pension premium to fall within them. PROFIT = [1_040, 5_200, 10_166, 10_300, 13_200, 15_600] # Payments an employer makes for sickness or leave, which are not earnings -# (reg 95(2)(b)) but which the shared taxable pay and Class 1 bases include. +# (reg 95(2)(b)) but which the shared Class 1 base includes (all three) and +# the shared taxable pay includes (sick and maternity pay). STATUTORY_PAY = [ None, None, diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index 70a555050..cdb3916f0 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -19,15 +19,17 @@ class esa_exempt_work_earnings(Variable): "expenses and employee pension contributions (net pay arrangements), " "above the personal and blind person's allowances, less the married " "couple's allowance reduction up to that tax. The allowances are the " - "person's own, so other income can still reduce them through the " - "personal allowance taper, but the tax on other income never comes " - "off the pay, a self-employment loss cannot change it, and credits " - "settled through a tax return (such as foreign tax credit relief) are " - "not taken off it. Class 1: the main and additional primary rates on " - "the pay between the primary threshold and the upper earnings limit " - "and above it. Statutory sick, maternity and paternity pay are not " - "earnings (reg 95(2)(b)), so they add neither to the pay nor to the " - "tax and contributions taken off it. A second job taxed at the basic " + "person's own, so other income, and anything else that moves adjusted " + "net income (such as a loss from another trade), can still change " + "them through the personal allowance taper. But the tax on other " + "income never comes off the pay, a self-employment loss is never " + "deducted from the pay, and credits settled through a tax return " + "(such as foreign tax credit relief) are not taken off it. Class 1: " + "the main and additional primary rates on the pay between the primary " + "threshold and the upper earnings limit and above it. Statutory " + "sick, maternity and paternity pay are not earnings (reg 95(2)(b)), " + "so they add neither to the pay nor to the tax and contributions " + "taken off it. A second job taxed at the basic " "rate through PAYE would leave lower net earnings than this. " "Self-employment (reg 98(3) and reg 99): the profit less a notional " "income tax at the basic rate (the Scottish basic rate for a Scottish " @@ -76,8 +78,9 @@ def formula(person, period, parameters): # Employment, reg 96(3): PAYE and primary Class 1 on the pay alone. # Statutory sick, maternity and paternity pay are not earnings (reg - # 95(2)(b)), so the shared taxable employment income and Class 1 - # base, which include them, are not used. + # 95(2)(b)), so the shared bases are not used: taxable employment + # income includes statutory sick and maternity pay, and the Class 1 + # base (ni_class_1_income) includes all three. pay = person("employment_income", period) taxable_pay = max_(0, pay - person("employment_deductions", period)) taxable_pay_after_pension = max_( From ffd752576ed040e4dbc0552c46ac1893e3f4d0af Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 9 Oct 2026 21:17:16 -0400 Subject: [PATCH 09/11] Read Class 2's own lower profits threshold in the ESA reg 99(3)(a) rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reg 99(3)(a) as at 6 April 2023 spares chargeable income "equal to or less than the amount specified in section 11(4)(a) ... (lower profits threshold)". #2058 added that amount as gov.hmrc.national_insurance.class_2.lower_profits_threshold (£11,908 in 2022-23, £12,570 in 2023-24). The rule and the test reference now read it instead of the Class 4 lower profits limit. The values are the same in both years the deduction applies, so no result changes. Two 2022-23 YAML cases pin the threshold and its date. At £11,908 of profit there is no Class 2: (£11,908 - £3,900) / 52 = £154.00, over the £152 higher limit. At £11,909, Class 2 of 52 x £3.15 and Class 4 of 9.73% x £1 come off: (£11,909 - £0.0973 - £163.80 - £3,900) / 52 = £150.87, within it. ESA YAML: 43 cases. Co-Authored-By: Claude Opus 5.5 --- .../tests/legacy_award_work_reference.py | 2 +- .../esa_income_remunerative_work.yaml | 58 +++++++++++++++++++ .../gov/dwp/esa_exempt_work_earnings.py | 6 +- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index 84bdfae81..a6b6a5593 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -109,7 +109,7 @@ def esa_weekly_earnings(adult, parameters): ) rule = parameters.gov.dwp.ESA.income.self_employment_class_2 if rule.above_lower_profits_threshold: - class_2_due = profit > class_4.thresholds.lower_profits_limit + class_2_due = profit > nics.class_2.lower_profits_threshold else: class_2_due = profit >= nics.class_2.small_profits_threshold notional_class_2 = ( diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index f3fbd389b..d7ced9c69 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -772,6 +772,64 @@ esa_income_eligible: false esa_income: 0 +# 2022-23 (SI 2022/1329 from 6 April 2022): Class 2 of £3.15 a week only on +# profits over the £11,908 lower profits threshold, Class 4 at 9.73% from +# £11,908 (Health and Social Care Levy (Repeal) Act 2022 s.2(2)(a)); the +# personal allowance is £12,570 and the higher limit £152 (16 x £9.50). + +- name: In 2022-23 a profit equal to the lower profits threshold pays no Class 2 + period: 2022 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 11_908 + personal_pension_contributions: 7_800 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # No tax (under £12,570), Class 4 or Class 2 (profit does not exceed + # £11,908). Half the premium, £3,900. (£11,908 - £3,900) / 52 = £154.00, + # over £152. + esa_exempt_work_earnings: 154 + esa_income_claimant_remunerative_work: true + esa_income_eligible: false + esa_income: 0 + +- name: In 2022-23 £1 over the lower profits threshold brings Class 2 in and the work under the limit + # The same intended reg 99(3)(a) cliff as in 2023-24. + period: 2022 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 5_000 + hours_worked: 780 + self_employment_income: 11_909 + personal_pension_contributions: 7_800 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # No tax (under £12,570). Class 4 9.73% x £1 = £0.0973. Class 2 + # 52 x £3.15 = £163.80. Half the premium, £3,900. + # (£11,909 - £0.0973 - £163.80 - £3,900) / 52 = £150.87, within £152. + esa_exempt_work_earnings: 150.87 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + - name: In 2015-16 no Class 2 is due below the small profits threshold period: 2015 absolute_error_margin: 0.01 diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index cdb3916f0..e191887e1 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -143,12 +143,12 @@ def formula(person, period, parameters): ) # Reg 99(3)(a), until 5 April 2024: Class 2 at the weekly rate unless # the profit is below the small profits threshold or, from 6 April - # 2022, at or below the lower profits threshold (SSCBA 1992 s.11(4), - # which the model takes as the Class 4 lower profits limit). + # 2022, at or below the lower profits threshold (SSCBA 1992 + # s.11(4)(a), which reg 99(3)(a) names from 2023). class_2_rule = p.gov.dwp.ESA.income.self_employment_class_2 class_2_due = where( class_2_rule.above_lower_profits_threshold, - profit > class_4.thresholds.lower_profits_limit, + profit > nics.class_2.lower_profits_threshold, profit >= nics.class_2.small_profits_threshold, ) notional_class_2 = ( From 6880c0ebd3fa689ef22bcf369051c6d21e4d29a2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 11:53:40 -0400 Subject: [PATCH 10/11] Screen an outside member's own ESA/JSA claim on their own work Final review of ffd752576 (review_final_ffd752576): 1. A member outside the couple claims in their own right, but own_report_is_paid read the benefit unit's screen, which tests only the claimant and partner when either reports. A working outsider could be on the award, and a non-working one lost it when the claimant's claim failed. It now tests their own claim: not in remunerative work as its claimant (esa_income_claimant_remunerative_work; JSA's 16 hours), the capital limit, and their report above tariff income. The benefit unit's award must be positive or nil only because its screen fails, and not neutralised; JSA must be active. 2. #2027's reader properties compared against capital-only references. The claimant-or-partner reference now applies esa_screen/jsa_screen, the status reference applies the outside member's own work test, and the generators draw hours and pay for the couple and outside adults. Deterministic regressions: a working claimant's awards screened out; outside adults' status both ways (ESA, JSA, maintenance-loan signal); Merton 2027 non-dependant deduction. Four YAML status cases. 3. reg 99(3)(a) comment: SI 2022/1329 has effect from 6 April 2022. Co-Authored-By: Claude Opus 5.5 --- .../esa-jsa-remunerative-work.fixed.md | 2 +- .../esa_income_remunerative_work.yaml | 66 ++++ .../jsa_income_remunerative_work.yaml | 60 ++++ .../test_legacy_award_readers_properties.py | 294 ++++++++++++++++-- .../variables/gov/dwp/_legacy_award_payee.py | 47 ++- .../gov/dwp/esa_exempt_work_earnings.py | 3 +- .../variables/gov/dwp/esa_income_eligible.py | 8 +- .../gov/dwp/is_on_income_based_jsa.py | 29 +- .../gov/dwp/is_on_income_related_esa.py | 24 +- .../variables/gov/dwp/jsa_income_eligible.py | 7 +- 10 files changed, 486 insertions(+), 54 deletions(-) diff --git a/changelog.d/esa-jsa-remunerative-work.fixed.md b/changelog.d/esa-jsa-remunerative-work.fixed.md index fcb119cf2..05130f500 100644 --- a/changelog.d/esa-jsa-remunerative-work.fixed.md +++ b/changelog.d/esa-jsa-remunerative-work.fixed.md @@ -1,3 +1,3 @@ - End a reported income-related ESA award when the claimant is engaged in remunerative work or the partner is (Welfare Reform Act 2007 Sch 1 para 6(1)(e), (f)). For the claimant that is any paid work other than exempt work (ESA Regs 2008 reg 41(1)): work with earnings of no more than £20 a week, or of under 16 hours a week with earnings within 16 times the National Minimum Wage rounded up to the next 50p (£195.50 in 2025-26, £203.50 in 2026-27; reg 45(2), (4), (9A)). Earnings (new `esa_exempt_work_earnings`) are pay less PAYE and primary Class 1 on the pay alone and half the pension contributions (reg 96(3)), with statutory sick, maternity and paternity pay, which are not earnings (reg 95(2)(b)), adding neither to the pay nor to the tax and Class 1 taken off it, plus a self-employed profit less reg 99's notional basic-rate tax above the personal and blind person's allowances, main-rate Class 4 on the profit itself (reg 99(4)'s chargeable income, before capital allowances, the trading allowance and loss relief; reg 98(5)), Class 2 until 5 April 2024 (reg 99(3)(a); new `gov.dwp.ESA.income.self_employment_class_2.{deducted, above_lower_profits_threshold}`) and half the personal pension contributions (reg 98(3)), with no loss set against pay (reg 98(11)). The claimant's test is the new `esa_income_claimant_remunerative_work`. For the partner it is 24 hours a week or more (reg 42(1)), except for a carer (reg 43(2)(c)); hours come from the new `esa_income_remunerative_work_hours`. - End a reported income-based JSA award when the claimant works 16 hours a week or more (Jobseekers Act 1995 s.1(2)(e); JSA Regs 1996 reg 51(1)(a)) or the partner works 24 or more (s.3(1)(e); reg 51(1)(b)). A member of a joint-claim couple whose partner works 16 to under 24 hours can claim alone (reg 3E(2)(g)), so 24 hours is the limit for the other member of any couple. The JSA regulations have no exception for carers doing unrelated paid work. Hours come from the new `jsa_remunerative_work_hours`. -- The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours` and `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. +- The claimant is a member who reports the award; when the claimant or partner reports one, only they are tested, so a non-dependant's award never changes theirs. The Income Support gate reads the screened awards. A member outside the couple, such as a non-dependent adult, claims in their own right, so `is_on_income_related_esa` and `is_on_income_based_jsa` test their own claim on their own work (the ESA claimant's exempt work limits, or JSA's 16 hours) as well as their report, tariff income and the capital limit: a working one's own report no longer exempts them from a council tax reduction non-dependant deduction, and a non-working one stays on their award when the claimant's own claim fails. New parameters `gov.dwp.ESA.exempt_work.{lower_earnings_limit, hours_limit, higher_earnings_limit}`, `gov.dwp.ESA.income.remunerative_work.partner_hours` and `gov.dwp.JSA.remunerative_work.{claimant_hours, partner_hours}`; the descriptions of `gov.dwp.JSA.hours.single` and `couple` now say what reg 51 sets. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml index d7ced9c69..25353a120 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -1113,6 +1113,72 @@ output: esa_income_eligible: false +- name: A working non-dependent adult is not on ESA, whatever the claimant reports + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_claimant_or_partner: true + esa_income_reported: 5_000 + lodger: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + # 40 hours a week for £20,000: about £344.61 a week of earnings, + # above the £195.50 higher limit. + hours_worked: 2_080 + employment_income: 20_000 + benunits: + benunit: + members: [claimant, lodger] + households: + household: + members: [claimant, lodger] + output: + # The claimant reports the award, so the benefit unit's screen tests only + # the claimant, who passes. + esa_income_eligible: true + claimant_or_partner_esa_income: 5_000 + # The lodger claims in their own right and is engaged in remunerative + # work as its claimant (WRA 2007 Sch 1 para 6(1)(e)). + esa_income_claimant_remunerative_work: [false, true] + is_on_income_related_esa: [true, false] + +- name: A non-working non-dependent adult stays on ESA when the claimant's claim fails + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_claimant_or_partner: true + esa_income_reported: 5_000 + # 16 hours a week for £100 a week: remunerative work. + hours_worked: 832 + employment_income: 5_200 + lodger: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, lodger] + households: + household: + members: [claimant, lodger] + output: + # The benefit unit's award applies the claimant's failed screen to every + # report in it, so it is nil. + esa_income_eligible: false + esa_income: 0 + claimant_or_partner_esa_income: 0 + # The lodger's own claim passes, so they are on it; the claimant is not. + is_on_income_related_esa: [false, true] + # The Income Support gate reads the screened award (s.124(1)(h)). - name: A partner's ESA lost to remunerative work no longer bars Income Support diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml index 81beea365..3eb1c723c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml @@ -385,6 +385,66 @@ output: jsa_income_eligible: false +- name: A non-dependent adult working 16 hours is not on JSA, whatever the claimant reports + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + lodger: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 3_000 + hours_worked: 832 + benunits: + benunit: + members: [claimant, lodger] + households: + household: + members: [claimant, lodger] + output: + # The claimant reports the award, so the benefit unit's screen tests only + # the claimant, who passes. + jsa_income_eligible: true + claimant_or_partner_jsa_income: 3_000 + # The lodger claims in their own right and works 16 hours a week as its + # claimant (Jobseekers Act 1995 s.1(2)(e)). + is_on_income_based_jsa: [true, false] + +- name: A non-working non-dependent adult stays on JSA when the claimant's claim fails + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 50 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + hours_worked: 832 + lodger: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 3_000 + benunits: + benunit: + members: [claimant, lodger] + households: + household: + members: [claimant, lodger] + output: + # The benefit unit's award applies the claimant's failed screen to every + # report in it, so it is nil. + jsa_income_eligible: false + jsa_income: 0 + claimant_or_partner_jsa_income: 0 + # The lodger's own claim passes, so they are on it; the claimant is not. + is_on_income_based_jsa: [false, true] + # The Income Support gate reads the screened award (s.124(1)(f)). - name: A carer's own JSA lost to 16 hours of work no longer bars Income Support diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py index 7c6ef57d7..d378370f3 100644 --- a/policyengine_uk/tests/test_legacy_award_readers_properties.py +++ b/policyengine_uk/tests/test_legacy_award_readers_properties.py @@ -31,11 +31,24 @@ - the claimant-or-partner awards are bounded by the benefit-unit awards (0 <= claimant_or_partner_esa_income <= esa_income, likewise for JSA) and equal them when no other member reports an award; +- the claimant-or-partner awards are the award on the claimant's and + partner's reports after tariff income, within the capital limit, and only + while the benefit unit passes the remunerative work screen + (esa_income_eligible and jsa_income_eligible, read independently from the + law in legacy_award_work_reference); - a person is on an award exactly when they are the payee of their couple's positive award (the claimant or partner who reports it, or the claimant) - or, for anyone else, when the award on their own report alone is positive - after tariff income from the household's capital and within the capital - limit (Income Support: when they report it). + or, for anyone else, when their own claim passes: they are not engaged in + remunerative work as its claimant (for ESA, earnings above the exempt work + limits; for JSA, 16 hours a week or more) and the award on their own report + alone is positive after tariff income from the household's capital and + within the capital limit (Income Support: when they report it). + +Adults, including the adult outside the couple, work some hours and some +earn pay, so the work screens bite for both awards. Pay of £8,000 is below +the personal allowance and the primary threshold, and £20,000 is far above +the higher limit for exempt work, so the work reference, which reads +England's income tax rates, decides the same in Scotland and Wales. Roles are given explicitly (is_claimant_or_partner), so the properties test the readers rather than role inference. The take-up mode is fixed @@ -45,9 +58,15 @@ """ import numpy as np -from hypothesis import HealthCheck, given, settings +from hypothesis import HealthCheck, event, given, settings from hypothesis import strategies as st +from legacy_award_work_reference import ( + esa_claimant_in_remunerative_work, + esa_screen, + jsa_screen, + weekly_hours, +) from policyengine_uk import Simulation YEAR = 2025 @@ -90,6 +109,19 @@ def award_reports(draw): } +# 0, 15, 16, 24 and 40 hours a week: either side of JSA's 16 hours for a +# claimant, ESA's 16 for permitted work and 24 for either partner. +HOURS = [0, 0, 780, 832, 1_248, 2_080] + + +@st.composite +def work(draw): + return { + "employment_income": draw(st.sampled_from([0, 0, 8_000, 20_000])), + "hours_worked": draw(st.sampled_from(HOURS)), + } + + @st.composite def families(draw): """A claimant, an optional partner, up to two dependants and sometimes an @@ -97,7 +129,7 @@ def families(draw): Qualifying young persons and the existing outside adult may report awards of their own, so the properties also cover members who are on an award - in their own right. + in their own right. The claimant, partner and outside adult may work. """ dependants = [] for _ in range(draw(st.integers(0, 2))): @@ -118,7 +150,7 @@ def families(draw): adults.append( { "age": draw(st.integers(max(18, eldest_dependant + 16), 60)), - "employment_income": draw(st.sampled_from([0, 0, 8_000, 20_000])), + **draw(work()), "is_parent": bool(dependants), "receives_carer_benefit": draw(st.booleans()), # A disabled claimant or partner gives the family the @@ -143,6 +175,7 @@ def families(draw): { "age": draw(st.integers(20, 60)), "current_education": "NOT_IN_EDUCATION", + **draw(work()), **draw(award_reports()), } ) @@ -160,12 +193,13 @@ def other_members(draw): """An adult who is neither claimant, partner nor a dependant. They are under state pension age and not in education, so a 16 to 19 year - old is not a qualifying young person. They have no income other than the - awards they report. + old is not a qualifying young person. They may work, and have no income + other than their pay and the awards they report. """ return { "age": draw(st.integers(16, 60)), "current_education": "NOT_IN_EDUCATION", + **draw(work()), **draw(award_reports()), } @@ -280,11 +314,21 @@ def reference_award(reported, capital, params): ) -def reference_claimant_or_partner_award(adults, household, report, params): - """The award on the claimant's and partner's reports.""" - return float( - reference_award(sum(a[report] for a in adults), household["savings"], params) - ) +def outside_members(non_couple, other): + """Members of the benefit unit outside the couple, in simulation order.""" + return list(non_couple) + ([other] if other is not None else []) + + +def reference_claimant_or_partner_award( + adults, non_couple, household, other, report, screen, params, parameters +): + """The award on the claimant's and partner's reports, while the benefit + unit passes the work screen. When the claimant or partner reports, the + screen tests only them; otherwise the award on their reports is nil.""" + capital = household["savings"] + if not screen(adults, outside_members(non_couple, other), capital, parameters): + return 0.0 + return float(reference_award(sum(a[report] for a in adults), capital, params)) @SETTINGS @@ -292,21 +336,31 @@ def reference_claimant_or_partner_award(adults, household, report, params): def test_claimant_or_partner_awards_match_reference(drawn): units = [(*family, other) for family, other in drawn] sim = Simulation(situation=situation(units)) - dwp = sim.tax_benefit_system.parameters(YEAR).gov.dwp - for variable, report, params, active in [ - ("claimant_or_partner_esa_income", "esa_income_reported", dwp.ESA.income, 1), + parameters = sim.tax_benefit_system.parameters(YEAR) + dwp = parameters.gov.dwp + for variable, report, screen, params, active in [ + ( + "claimant_or_partner_esa_income", + "esa_income_reported", + esa_screen, + dwp.ESA.income, + 1, + ), ( "claimant_or_partner_jsa_income", "jsa_income_reported", + jsa_screen, dwp.JSA.income, dwp.JSA.income.active, ), ]: award = sim.calculate(variable, YEAR) - for i, (adults, _, household, other) in enumerate(units): + for i, (adults, non_couple, household, other) in enumerate(units): expected = active * reference_claimant_or_partner_award( - adults, household, report, params + adults, non_couple, household, other, report, screen, params, parameters ) + if expected == 0 and sum(a[report] for a in adults) > 0: + event(f"{variable}: a reported award is screened out") assert np.isclose(award[i], expected), (variable, units[i]) @@ -331,6 +385,20 @@ def payee(sim, claimant_or_partner, reports): return result +def esa_claimant_works(member, parameters): + """ESA Regs 2008 reg 41(1) and reg 45: engaged in remunerative work as the + claimant of their own claim.""" + return esa_claimant_in_remunerative_work(member, parameters) + + +def jsa_claimant_works(member, parameters): + """Jobseekers Act 1995 s.1(2)(e), JSA Regs 1996 reg 51(1)(a): 16 hours a + week or more as the claimant of their own claim.""" + return ( + weekly_hours(member) >= parameters.gov.dwp.JSA.remunerative_work.claimant_hours + ) + + @SETTINGS @given(st.lists(st.tuples(families(), other_members()), min_size=1, max_size=6)) def test_person_is_on_award_from_couple_or_own_report(drawn): @@ -338,14 +406,23 @@ def test_person_is_on_award_from_couple_or_own_report(drawn): sim = Simulation(situation=situation(units)) claimant_or_partner = sim.calculate("is_claimant_or_partner", YEAR) capital = sim.calculate("savings", YEAR, map_to="person") - dwp = sim.tax_benefit_system.parameters(YEAR).gov.dwp - for person_variable, couple_award, report, params, active in [ + parameters = sim.tax_benefit_system.parameters(YEAR) + dwp = parameters.gov.dwp + # Everyone's inputs, in simulation order: the couple, then the members + # outside it. + members = [ + member + for adults, non_couple, _, other in units + for member in adults + outside_members(non_couple, other) + ] + for person_variable, couple_award, report, params, active, works in [ ( "is_on_income_related_esa", "claimant_or_partner_esa_income", "esa_income_reported", dwp.ESA.income, True, + esa_claimant_works, ), ( "is_on_income_based_jsa", @@ -353,6 +430,7 @@ def test_person_is_on_award_from_couple_or_own_report(drawn): "jsa_income_reported", dwp.JSA.income, dwp.JSA.income.active, + jsa_claimant_works, ), ( "is_on_income_support", @@ -360,6 +438,7 @@ def test_person_is_on_award_from_couple_or_own_report(drawn): "income_support_reported", None, dwp.income_support.active, + None, ), ]: on = sim.calculate(person_variable, YEAR) @@ -374,8 +453,18 @@ def test_person_is_on_award_from_couple_or_own_report(drawn): # Income Support: their own report, while it is in payment. own = active & (reported > 0) else: - # The award on their own report alone, on the household's capital. - own = active & (reference_award(reported, capital, params) > 0) + # Their own claim: the award on their own report alone, on the + # household's capital, while they are not in remunerative work + # as its claimant. Nobody else's report or work matters. + free = np.array( + [ + member.get(report, 0) == 0 or not works(member, parameters) + for member in members + ] + ) + own = active & free & (reference_award(reported, capital, params) > 0) + if (~claimant_or_partner & (reported > 0) & ~free).any(): + event(f"{person_variable}: an outside member's work bars them") expected = np.where(claimant_or_partner, couple, own) assert np.array_equal(on, expected), (person_variable, units) @@ -646,3 +735,164 @@ def test_late_zero_entry_overrides_the_claimants_reports(): sim = Simulation(situation=situation) sim.set_input(variable, YEAR, np.array([0.0])) assert np.isclose(sim.calculate(scoped, YEAR)[0], 0), variable + + +def _situation(people, year=YEAR): + names = list(people) + return { + "people": { + n: {k: {year: v} for k, v in inputs.items()} for n, inputs in people.items() + }, + "benunits": {"b": {"members": names}}, + "households": {"h": {"members": names}}, + } + + +FULL_TIME_PAY = {"employment_income": 20_000, "hours_worked": 2_080} + + +def test_a_working_claimants_reported_awards_are_screened_out(): + """WRA 2007 Sch 1 para 6(1)(e); Jobseekers Act 1995 s.1(2)(e). + + A claimant earning £20,000 for 40 hours a week has ESA earnings of about + £344.61 a week after PAYE (£1,486) and Class 1 (about £594.40), above the + £195.50 higher limit for exempt work in 2025-26, and works 16 hours or + more for JSA. Their reported awards are not paid, so the claimant-or- + partner awards that the readers use are nil, whichever Hypothesis + examples the properties draw.""" + claimant = {"age": 40, "is_claimant_or_partner": True} + reports = {"esa_income_reported": 3_000, "jsa_income_reported": 3_000} + idle = Simulation(situation=_situation({"c": {**claimant, **reports}})) + working = Simulation( + situation=_situation({"c": {**claimant, **reports, **FULL_TIME_PAY}}) + ) + for variable in [ + "claimant_or_partner_esa_income", + "claimant_or_partner_jsa_income", + ]: + assert np.isclose(idle.calculate(variable, YEAR)[0], 3_000), variable + assert working.calculate(variable, YEAR)[0] == 0, variable + for variable in ["is_on_income_related_esa", "is_on_income_based_jsa"]: + assert idle.calculate(variable, YEAR)[0], variable + assert not working.calculate(variable, YEAR)[0], variable + + +def _claimant_and_outside_adult(claimant_report, claimant_works, other_works): + claimant = {"age": 40, "is_claimant_or_partner": True} + other = { + "age": 30, + "is_claimant_or_partner": False, + "current_education": "NOT_IN_EDUCATION", + "esa_income_reported": 3_000, + "jsa_income_reported": 3_000, + } + for person, works in [(claimant, claimant_works), (other, other_works)]: + if works: + person.update(FULL_TIME_PAY) + claimant["esa_income_reported"] = claimant_report + claimant["jsa_income_reported"] = claimant_report + return Simulation(situation=_situation({"c": claimant, "o": other})) + + +def test_an_outside_adults_own_claim_is_screened_on_their_own_work(): + """An adult outside the couple claims in their own right, so their claim + is tested on their own work and never on the claimant's (WRA 2007 Sch 1 + para 6(1)(e); Jobseekers Act 1995 s.1(2)(e)). The benefit unit's screen + tests only the claimant when the claimant reports an award, so it cannot + stand in for theirs. + + Working full time, they are never on either award, whether or not the + non-working claimant also reports one. Not working, they stay on both + when a working claimant's own report fails the screen and leaves the + benefit unit's award nil.""" + readers = [ + "is_on_income_related_esa", + "is_on_income_based_jsa", + "maintenance_loan_entitled_to_benefits", + ] + for claimant_report in [0, 3_000]: + working_other = _claimant_and_outside_adult( + claimant_report, claimant_works=False, other_works=True + ) + idle_other = _claimant_and_outside_adult( + claimant_report, claimant_works=True, other_works=False + ) + for variable in readers: + assert not working_other.calculate(variable, YEAR)[1], ( + variable, + claimant_report, + ) + assert idle_other.calculate(variable, YEAR)[1], (variable, claimant_report) + # The working claimant's report fails the screen: the benefit unit's + # award is nil, and the claimant is on neither award. + idle_other = _claimant_and_outside_adult( + 3_000, claimant_works=True, other_works=False + ) + assert idle_other.calculate("esa_income", YEAR)[0] == 0 + assert not idle_other.calculate("is_on_income_related_esa", YEAR)[0] + + +def test_an_outside_adults_work_keeps_their_non_dependant_deduction(): + """Merton's working-age scheme, as modelled from 2027, takes no + non-dependant deduction for a non-dependant on income-related ESA, read + from is_on_income_related_esa (the common council tax reduction helper). + + In a non-dependant benefit unit of a claimant and an adult outside their + couple, the outside adult's own ESA report exempts them only when their + own claim passes: working full time, their deduction is the same as if + they reported nothing, whatever the claimant reports; not working, they + are exempt whatever the working claimant reports.""" + year = NON_DEP_YEAR + + def deduction(claimant_report, other_report, claimant_works, other_works): + def member(age, role, report, works): + inputs = { + "age": {year: age}, + "is_claimant_or_partner": {year: role}, + "current_education": {year: "NOT_IN_EDUCATION"}, + "esa_income_reported": {year: report}, + } + if works: + inputs.update({k: {year: v} for k, v in FULL_TIME_PAY.items()}) + return inputs + + people = { + "applicant": {"age": {year: 50}, "is_claimant_or_partner": {year: True}}, + "claimant": member(40, True, claimant_report, claimant_works), + "other": member(30, False, other_report, other_works), + } + sim = Simulation( + situation={ + "people": people, + "benunits": { + "applicant_unit": {"members": ["applicant"]}, + "non_dependant_unit": { + "members": ["claimant", "other"], + "universal_credit": {year: 0}, + }, + }, + "households": { + "h": { + "members": list(people), + "country": {year: "ENGLAND"}, + "local_authority": {year: "MERTON"}, + "savings": {year: 0}, + } + }, + } + ) + return sim.calculate( + "merton_council_tax_reduction_individual_non_dep_deduction", year + )[2] + + not_on_esa = deduction(0, 0, claimant_works=False, other_works=True) + assert not_on_esa > 0 + for claimant_report in [0, 3_000]: + assert np.isclose( + deduction(claimant_report, 3_000, claimant_works=False, other_works=True), + not_on_esa, + ), claimant_report + assert ( + deduction(claimant_report, 3_000, claimant_works=True, other_works=False) + == 0 + ), claimant_report diff --git a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py index 651d32fff..31b8662da 100644 --- a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py +++ b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py @@ -25,28 +25,43 @@ def is_payee_of_couple_award(person, period, reported): return where(person.benunit.any(reports), reports, claimant) -def own_report_is_paid(person, period, award): +def own_report_is_paid(person, period, award, works, 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. - It is paid when the award on their report alone is positive: the report - exceeds the tariff income from the benefit unit's capital, within the - capital limit, as income_related_esa_award and income_related_jsa_award - screen it. Their own capital is not observed, so the benefit unit's - stands in for it. The benefit unit's award must also be positive, so a - reform that removes or zeroes the benefit removes the status too. - - Another member's report never changes this: once this person reports, - the capital test reads the household's capital whoever else reports, and - the award on their report alone does not depend on other reports. On the - formula path a positive award on their report implies a positive benefit - unit award, since adding reports never lowers it.""" + + 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 + 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 + an award it tests only them. + + The benefit unit's award must also be in payment: positive, or nil only + because the benefit unit's screen fails. When the award on this person's + report alone is positive, that screen fails only when the claimant or + partner reports an award and fails the work tests, which leaves this + person's own claim standing. So a reform that neutralises the benefit + removes the status, as does one that replaces the award with nil, or a + nil award entered directly, while the screen passes. + + On the formula path another member's report or work never changes this. + Their own tests read only their own work, their own report and the + household's capital, and when the award on their report alone is + positive, the benefit unit's award is positive exactly when its screen + passes.""" benunit = person.benunit - return ( - benunit(f"{award}_eligible", period) + own_award = ( + ~works + & (benunit(f"{award}_assessable_capital", period) <= capital_limit) & ( person(f"{award}_reported", period) > benunit(f"{award}_tariff_income", period) ) - & (benunit(award, period) > 0) ) + removed = person.simulation.tax_benefit_system.get_variable(award).is_neutralized + paid = benunit(award, period) > 0 + screen_fails = ~benunit(f"{award}_eligible", period) + return own_award & (paid | (screen_fails & (not removed))) diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py index e191887e1..4f82cef51 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -144,7 +144,8 @@ def formula(person, period, parameters): # Reg 99(3)(a), until 5 April 2024: Class 2 at the weekly rate unless # the profit is below the small profits threshold or, from 6 April # 2022, at or below the lower profits threshold (SSCBA 1992 - # s.11(4)(a), which reg 99(3)(a) names from 2023). + # s.11(4)(a)), which SI 2022/1329 wrote into reg 99(3)(a) with effect + # from 6 April 2022 (reg 1). class_2_rule = p.gov.dwp.ESA.income.self_employment_class_2 class_2_due = where( class_2_rule.above_lower_profits_threshold, diff --git a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py index 514fed505..8b38b9b46 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py @@ -21,8 +21,12 @@ class esa_income_eligible(Variable): "When the claimant or partner reports one, only they are candidates: " "a member outside the family, such as a non-dependent adult, claims " "in their own right, and is tested on their own work only when " - "neither the claimant nor the partner reports an award. This is not " - "a full entitlement model." + "neither the claimant nor the partner reports an award. This one " + "screen decides the benefit unit's award (esa_income) on every report " + "in it, so when the claimant or partner reports, another member's " + "report is paid or not with their claim. Whether that member is on " + "the award themselves (is_on_income_related_esa) is tested on their " + "own claim. This is not a full entitlement model." ) definition_period = YEAR reference = ( 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 d22686eb1..f062bdd1a 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 @@ -19,21 +19,38 @@ class is_on_income_based_jsa(Variable): "while their award (claimant_or_partner_jsa_income) is positive. The " "model has no joint-claim JSA input. Any other member of the benefit " "unit, such as a non-dependent adult, claims in their own right and " - "is on it only if the award on their own report alone is positive: " - "the report exceeds the tariff income from the benefit unit's " - "capital, within the capital limit, while the benefit unit's " - "modelled award is positive." + "is on it only if their own claim passes: they do not work 16 hours " + "a week or more (jsa_remunerative_work_hours; Jobseekers Act 1995 " + "s.1(2)(e), JSA Regs 1996 reg 51(1)(a)), 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. Income-" + "based JSA must be open (gov.dwp.JSA.income.active), and the benefit " + "unit's modelled award in payment: positive, or nil only because the " + "claimant's or partner's own claim fails jsa_income_eligible, so " + "neutralising jsa_income removes the status." ) definition_period = YEAR 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/1995/18/section/1", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/51", ) def formula(person, period, parameters): couple_award = ( person.benunit("claimant_or_partner_jsa_income", period) > 0 ) & (is_payee_of_couple_award(person, period, "jsa_income_reported")) - # The award on their own report alone, never another member's. - own_award = own_report_is_paid(person, period, "jsa_income") + # Their own claim, on their own report and their own work alone. + JSA = parameters(period).gov.dwp.JSA + hours = person("jsa_remunerative_work_hours", period) + own_award = JSA.income.active & own_report_is_paid( + person, + period, + "jsa_income", + works=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 0989dfd6d..a67ebef5f 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 @@ -19,20 +19,34 @@ class is_on_income_related_esa(Variable): "does, and only while their award (claimant_or_partner_esa_income) " "is positive. Any other member of the benefit unit, such as a " "non-dependent adult, claims in their own right and is on it only if " - "the award on their own report alone is positive: the report exceeds " - "the tariff income from the benefit unit's capital, within the " - "capital limit, while the benefit unit's modelled award is positive." + "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, 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 " + "benefit unit's " + "modelled award must also be in payment: positive, or nil only " + "because the claimant's or partner's own claim fails " + "esa_income_eligible, so neutralising esa_income removes the status." ) definition_period = YEAR 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/schedule/1/paragraph/6", ) def formula(person, period, parameters): couple_award = ( person.benunit("claimant_or_partner_esa_income", period) > 0 ) & (is_payee_of_couple_award(person, period, "esa_income_reported")) - # The award on their own report alone, never another member's. - own_award = own_report_is_paid(person, period, "esa_income") + # Their own claim, on their own report and their own work alone. + own_award = own_report_is_paid( + person, + period, + "esa_income", + works=person("esa_income_claimant_remunerative_work", period), + 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/jsa_income_eligible.py b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py index 7aecb8afd..c7bf88abc 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py @@ -26,7 +26,12 @@ class jsa_income_eligible(Variable): "only they are candidates: a member outside the family, such as a " "non-dependent adult, claims in their own right, and is tested on " "their own work only when neither the claimant nor the partner " - "reports an award. This is not a full entitlement model." + "reports an award. This one screen decides the benefit unit's award " + "(jsa_income) on every report in it, so when the claimant or partner " + "reports, another member's report is paid or not with their claim. " + "Whether that member is on the award themselves " + "(is_on_income_based_jsa) is tested on their own claim. This is not a " + "full entitlement model." ) definition_period = YEAR reference = ( From 672d0a62edf9ac94650ce1197a22646284970b2d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 12:21:12 -0400 Subject: [PATCH 11/11] Say when a nil ESA/JSA award removes an outside member's status Final review of 6880c0ebd (APPROVE WITH NITS), nit 2: the status documentation said the benefit unit's award could be nil "only because" the claimant's or partner's claim fails the screen. A zero cannot say why it is zero: a nil entered directly, or a reform that replaces the award with nil, removes an outside member's status only while the screen passes; neutralising the award always removes it. Documentation only. Co-Authored-By: Claude Opus 5.5 --- .../variables/gov/dwp/_legacy_award_payee.py | 16 +++++++++------- .../variables/gov/dwp/is_on_income_based_jsa.py | 12 +++++++----- .../gov/dwp/is_on_income_related_esa.py | 11 +++++++---- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py index 31b8662da..efe7d060f 100644 --- a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py +++ b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py @@ -39,13 +39,15 @@ def own_report_is_paid(person, period, award, works, capital_limit): cannot stand in for theirs, because when the claimant or partner reports an award it tests only them. - The benefit unit's award must also be in payment: positive, or nil only - because the benefit unit's screen fails. When the award on this person's - report alone is positive, that screen fails only when the claimant or - partner reports an award and fails the work tests, which leaves this - person's own claim standing. So a reform that neutralises the benefit - removes the status, as does one that replaces the award with nil, or a - nil award entered directly, while the screen passes. + The benefit unit's award must also be in payment: positive, or nil while + the benefit unit's screen fails and the award is not neutralised. When + the award on this person's report alone is positive, that screen fails + only when the claimant or partner reports an award and fails the work + tests, which leaves this person's own claim standing; on the formula + path that is the only way the award is nil. A zero cannot say why it is + zero, so a reform that replaces the award with nil, or a nil award + entered directly, removes the status only while the screen passes. A + reform that neutralises the benefit always removes it. On the formula path another member's report or work never changes this. Their own tests read only their own work, their own report and the 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 f062bdd1a..71e0e1c56 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 @@ -25,11 +25,13 @@ class is_on_income_based_jsa(Variable): "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. Income-" - "based JSA must be open (gov.dwp.JSA.income.active), and the benefit " - "unit's modelled award in payment: positive, or nil only because the " - "claimant's or partner's own claim fails jsa_income_eligible, so " - "neutralising jsa_income removes the status." + "this. Income-based JSA must be open (gov.dwp.JSA.income.active), and " + "the benefit unit's modelled award in payment: positive, or nil while " + "jsa_income_eligible fails and jsa_income is not neutralised. On the " + "formula path that nil comes only from the claimant's or partner's " + "own claim failing the screen. Neutralising jsa_income removes the " + "status; a nil jsa_income entered directly, or a reform that replaces " + "it with nil, removes it only while the screen passes." ) definition_period = YEAR reference = ( 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 a67ebef5f..3b91c3daa 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 @@ -25,10 +25,13 @@ class is_on_income_related_esa(Variable): "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 " - "benefit unit's " - "modelled award must also be in payment: positive, or nil only " - "because the claimant's or partner's own claim fails " - "esa_income_eligible, so neutralising esa_income removes the status." + "benefit unit's modelled award must also be in payment: positive, or " + "nil while esa_income_eligible fails and esa_income is not " + "neutralised. On the formula path that nil comes only from the " + "claimant's or partner's own claim failing the screen. Neutralising " + "esa_income removes the status; a nil esa_income entered directly, or " + "a reform that replaces it with nil, removes it only while the screen " + "passes." ) definition_period = YEAR reference = (