From c9f3868798f46b2ff9832cdc81b4596391de9348 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 26 Oct 2026 22:12:35 +0400 Subject: [PATCH 1/3] Calculate the ESA permitted work limit from the national living wage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ESA Regs 2008 reg 45(4) sets the permitted work higher earnings limit at 16 x National Minimum Wage from 11 April 2011 (SI 2011/674 reg 16(5)), rounded up to the next 50p (reg 45(9A)). Reg 2(1) points to the rate in reg 11 of the NMW Regulations 1999, which DWP reads as the highest rate, now the national living wage (DMG 41190). The model listed the limit year by year and projected it with average earnings, so it ignored changes to gov.hmrc.minimum_wage and disagreed with the model's own projection of the national living wage from 2027. - esa_exempt_work_higher_earnings_limit calculates the limit: the fixed amount (£92, £93 from 1 October 2009) until 10 April 2011, then 16 x the top rate of gov.hmrc.minimum_wage.non_apprentice, rounded up to 50p. esa_income_claimant_remunerative_work reads it. - gov.dwp.ESA.exempt_work.fixed_higher_earnings_limit and higher_earnings_limit_minimum_wage_hours replace higher_earnings_limit, which is registered as removed so saved reforms fail with directions. - The minimum wage rates now start at 1 October 2010 (SI 2010/1901 and SI 2011/2345), so the limit has a rate from April 2011. JSA is unchanged: the JSA Regs 1996 never use the minimum wage, and the JSA Regs 2013 use it only to end sanctions (reg 25(3)), which the model does not include. Co-Authored-By: Claude Opus 5.5 --- ...a-permitted-work-limit-from-nlw.changed.md | 1 + .../fixed_higher_earnings_limit.yaml | 21 +++++ .../exempt_work/higher_earnings_limit.yaml | 79 ----------------- ...her_earnings_limit_minimum_wage_hours.yaml | 25 ++++++ .../gov/hmrc/minimum_wage/apprentice.yaml | 17 +++- .../gov/hmrc/minimum_wage/non_apprentice.yaml | 64 ++++++++++++-- .../tests/legacy_award_work_reference.py | 17 +++- .../tests/test_legacy_benefit_parameters.py | 71 +++++++++++----- .../tests/test_minimum_wage_rates.py | 7 +- ...ved_esa_higher_earnings_limit_parameter.py | 85 +++++++++++++++++++ policyengine_uk/utils/parameters.py | 11 +++ .../esa_exempt_work_higher_earnings_limit.py | 51 +++++++++++ .../esa_income_claimant_remunerative_work.py | 2 +- 13 files changed, 338 insertions(+), 113 deletions(-) create mode 100644 changelog.d/esa-permitted-work-limit-from-nlw.changed.md create mode 100644 policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml delete 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/higher_earnings_limit_minimum_wage_hours.yaml create mode 100644 policyengine_uk/tests/test_removed_esa_higher_earnings_limit_parameter.py create mode 100644 policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py diff --git a/changelog.d/esa-permitted-work-limit-from-nlw.changed.md b/changelog.d/esa-permitted-work-limit-from-nlw.changed.md new file mode 100644 index 0000000000..18b63b9dd1 --- /dev/null +++ b/changelog.d/esa-permitted-work-limit-from-nlw.changed.md @@ -0,0 +1 @@ +The ESA permitted work higher earnings limit is now calculated as 16 x the national living wage, rounded up to the next 50p (ESA Regs 2008 reg 45(4) and (9A)), so it follows a change to gov.hmrc.minimum_wage instead of being listed year by year and projected with average earnings. The gov.dwp.ESA.exempt_work.higher_earnings_limit parameter is removed, and the minimum wage rates now start at 1 October 2010. diff --git a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml new file mode 100644 index 0000000000..f61bed40fe --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml @@ -0,0 +1,21 @@ +description: Fixed weekly earnings limit of Employment and Support Allowance permitted work before 11 April 2011. Work done for less than the exempt work hours limit, for which a claimant's earnings in any week did not exceed this amount, was exempt work. From 11 April 2011 the limit is a number of hours a week at the National Minimum Wage instead. +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 +metadata: + unit: currency-GBP + period: week + label: ESA exempt work fixed higher earnings limit + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(4) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45 + - 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 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 deleted file mode 100644 index 7ab1f87bb5..0000000000 --- a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml +++ /dev/null @@ -1,79 +0,0 @@ -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, 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/ESA/exempt_work/higher_earnings_limit_minimum_wage_hours.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit_minimum_wage_hours.yaml new file mode 100644 index 0000000000..e52511da9b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit_minimum_wage_hours.yaml @@ -0,0 +1,25 @@ +description: >- + Hours a week at the National Minimum Wage that set the weekly earnings limit + of Employment and Support Allowance permitted work from 11 April 2011. + Regulation 45(4) makes work done for less than the exempt work hours limit + exempt work where the claimant's earnings in any week do not exceed this many + times the National Minimum Wage, which regulation 2(1) defines as the rate in + regulation 11 of the National Minimum Wage Regulations 1999, now the national + living wage. Regulation 45(9A) rounds the result up to the next 50p. +values: + 2011-04-11: + value: 16 + reference: + - title: Social Security (Miscellaneous Amendments) Regulations 2011 (SI 2011/674) reg. 16(5)(a) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45/2011-04-11 +metadata: + unit: hour + period: week + label: ESA exempt work higher earnings limit hours + reference: + - title: Employment and Support Allowance Regulations 2008 reg. 45(4) and (9A) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/45 + - title: Employment and Support Allowance Regulations 2008 reg. 2(1) ("National Minimum Wage") + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/2 + - title: Decision Makers' Guide ch. 41, para. 41190 and Appendix 5 + href: https://assets.publishing.service.gov.uk/media/689db34d1fedc616bb13399c/dmg-ch-41.pdf diff --git a/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice.yaml b/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice.yaml index 6d4d431744..14c0f8f0dd 100644 --- a/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice.yaml +++ b/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice.yaml @@ -1,9 +1,20 @@ description: Minimum wage rate for apprentices. # The apprenticeship rate applies to an apprentice who is under 19 or in the -# first 12 months of the apprenticeship (NMW Regulations 2015 reg. 5). Rates -# take effect on the commencement date of each amending S.I.: on 1 October -# until 2016, then on 1 April from 2017. +# first 12 months of the apprenticeship (NMW Regulations 2015 reg. 5). The +# rate began on 1 October 2010 (NMW Regulations 1999 reg. 13(3) as substituted +# by S.I. 2010/1901 reg. 5). Rates take effect on the commencement date of each +# amending S.I.: on 1 October until 2016, then on 1 April from 2017. values: + 2010-10-01: + value: 2.5 + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 5 (substituted regulation 13(3)) + href: https://www.legislation.gov.uk/uksi/2010/1901/made + 2011-10-01: + value: 2.6 + reference: + - title: The National Minimum Wage (Amendment) Regulations 2011, regulation 5(c) + href: https://www.legislation.gov.uk/uksi/2011/2345/made 2012-10-01: value: 2.65 reference: diff --git a/policyengine_uk/parameters/gov/hmrc/minimum_wage/non_apprentice.yaml b/policyengine_uk/parameters/gov/hmrc/minimum_wage/non_apprentice.yaml index 1423e07b24..ae74684457 100644 --- a/policyengine_uk/parameters/gov/hmrc/minimum_wage/non_apprentice.yaml +++ b/policyengine_uk/parameters/gov/hmrc/minimum_wage/non_apprentice.yaml @@ -1,10 +1,12 @@ description: Minimum wage rate for non-apprentices by age. -# The thresholds are every age boundary the law has used since October 2012 +# The thresholds are every age boundary the law has used since October 2010 # (18, 21, 23 and 25), giving the bands under 18, 18 to 20, 21 to 22, 23 to 24 # and 25 or over. The thresholds do not change. Where the law groups adjacent # bands, they carry the same rate. # +# - From 1 October 2010 the adult rate applied from age 21, not 22 (NMW +# Regulations 1999 reg. 13(1) as substituted by S.I. 2010/1901 reg. 5). # - Until 31 March 2016 one adult rate applied from age 21 (NMW Regulations # 1999 reg. 11, then NMW Regulations 2015 reg. 4(1)(a)). # - From 1 April 2016 the national living wage (NMW Regulations 2015 reg. 4) @@ -22,8 +24,18 @@ description: Minimum wage rate for non-apprentices by age. brackets: - threshold: - 2012-10-01: 0 + 2010-10-01: 0 amount: + 2010-10-01: + value: 3.64 + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 5 (substituted regulation 13(2)) + href: https://www.legislation.gov.uk/uksi/2010/1901/made + 2011-10-01: + value: 3.68 + reference: + - title: The National Minimum Wage (Amendment) Regulations 2011, regulation 5(b) + href: https://www.legislation.gov.uk/uksi/2011/2345/made 2012-10-01: value: 3.68 reference: @@ -105,8 +117,18 @@ brackets: - title: The National Minimum Wage (Amendment) Regulations 2026, regulation 2(3)(b) href: https://www.legislation.gov.uk/uksi/2026/357/made - threshold: - 2012-10-01: 18 + 2010-10-01: 18 amount: + 2010-10-01: + value: 4.92 + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 5 (substituted regulation 13(1), ages 18 to 20) + href: https://www.legislation.gov.uk/uksi/2010/1901/made + 2011-10-01: + value: 4.98 + reference: + - title: The National Minimum Wage (Amendment) Regulations 2011, regulation 5(a) + href: https://www.legislation.gov.uk/uksi/2011/2345/made 2012-10-01: value: 4.98 reference: @@ -188,8 +210,18 @@ brackets: - title: The National Minimum Wage (Amendment) Regulations 2026, regulation 2(3)(a) href: https://www.legislation.gov.uk/uksi/2026/357/made - threshold: - 2012-10-01: 21 + 2010-10-01: 21 amount: + 2010-10-01: + value: 5.93 + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 3 + href: https://www.legislation.gov.uk/uksi/2010/1901/made + 2011-10-01: + value: 6.08 + reference: + - title: The National Minimum Wage (Amendment) Regulations 2011, regulation 3 + href: https://www.legislation.gov.uk/uksi/2011/2345/made 2012-10-01: value: 6.19 reference: @@ -271,8 +303,18 @@ brackets: - title: The National Minimum Wage (Amendment) Regulations 2026, regulation 2(2) href: https://www.legislation.gov.uk/uksi/2026/357/made - threshold: - 2012-10-01: 23 + 2010-10-01: 23 amount: + 2010-10-01: + value: 5.93 + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 3 + href: https://www.legislation.gov.uk/uksi/2010/1901/made + 2011-10-01: + value: 6.08 + reference: + - title: The National Minimum Wage (Amendment) Regulations 2011, regulation 3 + href: https://www.legislation.gov.uk/uksi/2011/2345/made 2012-10-01: value: 6.19 reference: @@ -354,8 +396,18 @@ brackets: - title: The National Minimum Wage (Amendment) Regulations 2026, regulation 2(2) href: https://www.legislation.gov.uk/uksi/2026/357/made - threshold: - 2012-10-01: 25 + 2010-10-01: 25 amount: + 2010-10-01: + value: 5.93 + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 3 + href: https://www.legislation.gov.uk/uksi/2010/1901/made + 2011-10-01: + value: 6.08 + reference: + - title: The National Minimum Wage (Amendment) Regulations 2011, regulation 3 + href: https://www.legislation.gov.uk/uksi/2011/2345/made 2012-10-01: value: 6.19 reference: diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index 367f3102ee..9aa06fb191 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -131,14 +131,23 @@ def esa_weekly_earnings(adult, parameters): return (net_pay + net_profit) / WEEKS +def esa_higher_earnings_limit(parameters): + """Reg 45(4) from 11 April 2011: 16 x the highest National Minimum Wage + rate (reg 2(1), DMG 41190), with part of a pound under 50p rounded up to + 50p and over 50p up to the next pound (reg 45(9A)).""" + exempt_work = parameters.gov.dwp.ESA.exempt_work + highest_rate = max(parameters.gov.hmrc.minimum_wage.non_apprentice.amounts) + limit = exempt_work.higher_earnings_limit_minimum_wage_hours * highest_rate + return math.ceil(round(limit * 2, 6)) / 2 + + 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 - ) + permitted_work = weekly_hours( + adult + ) < exempt_work.hours_limit and earnings <= esa_higher_earnings_limit(parameters) return not (within_lower_limit or permitted_work) diff --git a/policyengine_uk/tests/test_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index be4cb6cf51..7a2b2bceff 100644 --- a/policyengine_uk/tests/test_legacy_benefit_parameters.py +++ b/policyengine_uk/tests/test_legacy_benefit_parameters.py @@ -1,6 +1,6 @@ import math -from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk import CountryTaxBenefitSystem, Simulation def test_legacy_benefit_age_threshold_parameters(): @@ -68,31 +68,64 @@ def _rate_in_force(on): return [rate for start, rate in sorted(NMW_RATES.items()) if start <= on][-1] +def _higher_earnings_limits(years, reform=None): + situation = { + "people": {"claimant": {"age": {str(year): 40 for year in years}}}, + "benunits": {"benunit": {"members": ["claimant"]}}, + "households": {"household": {"members": ["claimant"]}}, + } + simulation = Simulation(situation=situation, reform=reform) + return { + year: float( + simulation.calculate("esa_exempt_work_higher_earnings_limit", year)[0] + ) + for year in years + } + + 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). + parameters = CountryTaxBenefitSystem().parameters + exempt_work = parameters.gov.dwp.ESA.exempt_work + assert exempt_work.fixed_higher_earnings_limit("2008-10-27") == 92 + assert exempt_work.fixed_higher_earnings_limit("2009-09-30") == 92 + assert exempt_work.fixed_higher_earnings_limit("2009-10-01") == 93 + assert exempt_work.higher_earnings_limit_minimum_wage_hours("2011-04-11") == 16 + limits = _higher_earnings_limits(range(2009, 2041)) + # Before 2015 a year reads its rules on 1 January: the fixed amount until + # 2011, then 16 x the rate in force that day. + assert limits[2009] == 92 + assert limits[2010] == 93 + assert limits[2011] == 93 + for year in range(2012, 2015): + expected = _round_up_to_50p(16 * _rate_in_force(f"{year}-01-01")) + assert limits[year] == expected, year + # From 2015 each year takes the rate 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 + assert limits[year] == expected, year # DWP's published limits. - 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). + assert limits[2024] == 183.5 + assert limits[2025] == 195.5 + assert limits[2026] == 203.5 + # Later years follow the model's national living wage, whatever it is. 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 + wage = parameters(str(year)).gov.hmrc.minimum_wage.non_apprentice + assert limits[year] == _round_up_to_50p(16 * max(wage.amounts)), year + + +def test_esa_exempt_work_higher_limit_follows_a_national_living_wage_reform(): + # A national living wage of £13.18 from April 2027: 16 x £13.18 = £210.88, + # rounded up to £211 (reg 45(9A)). + reform = { + f"gov.hmrc.minimum_wage.non_apprentice[{bracket}].amount": {"2027": 13.18} + for bracket in (2, 3, 4) + } + assert _higher_earnings_limits([2026, 2027], reform=reform) == { + 2026: 203.5, + 2027: 211.0, + } def test_esa_and_jsa_remunerative_work_parameters_by_date(): diff --git a/policyengine_uk/tests/test_minimum_wage_rates.py b/policyengine_uk/tests/test_minimum_wage_rates.py index da9659640a..ef3d145f12 100644 --- a/policyengine_uk/tests/test_minimum_wage_rates.py +++ b/policyengine_uk/tests/test_minimum_wage_rates.py @@ -36,7 +36,7 @@ 2. For every year from 2015 to 2040, no rate falls as age rises, and the apprenticeship rate is never above the rate for any age from 16. 3. No rate falls from one year to the next, 2015 to 2040. This holds for - every rate set from October 2011 to April 2026; it is a property of the + every rate set from October 2010 to April 2026; it is a property of the enacted rates, not a rule in the Act, so a future cut would need this test changing. @@ -86,6 +86,11 @@ # (commencement date, S.I. and provisions, rates by band). Where an S.I. left a # rate unchanged the row repeats the rate then in force. RATES_FROM_COMMENCEMENT = [ + # 1999 Regs reg. 11 (21 and over) and reg. 13 as substituted by reg. 5: + # 13(1) 18 to 20 (the adult rate now starts at 21, not 22), 13(2) under + # 18 and 13(3) the new apprenticeship rate. + ("2010-10-01", "S.I. 2010/1901 regs. 3, 5", (2.50, 3.64, 4.92, 5.93, 5.93, 5.93)), + ("2011-10-01", "S.I. 2011/2345 regs. 3, 5", (2.60, 3.68, 4.98, 6.08, 6.08, 6.08)), # 1999 Regs reg. 11 (21 and over) and reg. 13(3) (apprentices); the # 18 to 20 and under 18 rates (reg. 13(1), (2)) stayed at the levels set # by S.I. 2011/2345 reg. 5 from 1 October 2011. diff --git a/policyengine_uk/tests/test_removed_esa_higher_earnings_limit_parameter.py b/policyengine_uk/tests/test_removed_esa_higher_earnings_limit_parameter.py new file mode 100644 index 0000000000..4947baf2ca --- /dev/null +++ b/policyengine_uk/tests/test_removed_esa_higher_earnings_limit_parameter.py @@ -0,0 +1,85 @@ +"""Saved ESA higher earnings limit policies fail with directions to the wage.""" + +import pytest +from policyengine_core.reforms import Reform + +from policyengine_uk import Simulation +from policyengine_uk.system import system +from policyengine_uk.utils.parameters import RemovedParameterNode +from policyengine_uk.utils.scenario import Scenario + +REMOVED = "gov.dwp.ESA.exempt_work.higher_earnings_limit" +REPLACEMENTS = ( + "gov.hmrc.minimum_wage.non_apprentice", + "gov.dwp.ESA.exempt_work.higher_earnings_limit_minimum_wage_hours", + "gov.dwp.ESA.exempt_work.fixed_higher_earnings_limit", +) +SITUATION = {"people": {"person": {"age": {2026: 40}}}} + + +@pytest.fixture(scope="module", autouse=True) +def reuse_country_model(): + with pytest.MonkeyPatch.context() as patch: + patch.setattr( + "policyengine_uk.simulation.CountryTaxBenefitSystem", system.clone + ) + yield + + +@pytest.fixture(scope="module") +def baseline(): + return Simulation(situation=SITUATION) + + +@pytest.fixture +def simulation(baseline): + return baseline.clone() + + +@pytest.mark.parametrize( + "entry_point", + ["core_reform", "scenario_reform", "scenario_scalar", "scenario_saved_periods"], +) +def test_removed_esa_higher_earnings_limit_reforms_give_directions( + simulation, entry_point +): + changes = {REMOVED: {"2026": 250}} + + with pytest.raises(ValueError) as error: + if entry_point == "core_reform": + Reform.from_dict(changes)(simulation.tax_benefit_system) + elif entry_point == "scenario_reform": + Scenario.from_reform(changes).apply(simulation) + elif entry_point == "scenario_scalar": + Scenario(parameter_changes={REMOVED: 250}).apply(simulation) + else: + Scenario(parameter_changes=changes).apply(simulation) + + message = str(error.value) + assert f"{REMOVED} has been removed" in message + assert "esa_exempt_work_higher_earnings_limit" in message + for replacement in REPLACEMENTS: + assert replacement in message + assert simulation.calculate("esa_exempt_work_higher_earnings_limit", 2026)[0] == ( + 203.5 + ) + + +def test_removed_esa_higher_earnings_limit_alias_uses_its_own_parent(baseline): + exempt_work = baseline.tax_benefit_system.parameters.gov.dwp.ESA.exempt_work + alias = exempt_work.children["higher_earnings_limit"] + + assert isinstance(alias, RemovedParameterNode) + assert alias.parent is exempt_work + assert alias.metadata["removed"] is True + with pytest.raises(ValueError, match="national living wage"): + alias.update(period="2026", value=250) + + +def test_minimum_wage_hours_remain_reformable(simulation): + Scenario(parameter_changes={REPLACEMENTS[1]: 20}).apply(simulation) + + # 20 x £12.71 = £254.20, rounded up to £254.50. + assert simulation.calculate("esa_exempt_work_higher_earnings_limit", 2026)[0] == ( + 254.5 + ) diff --git a/policyengine_uk/utils/parameters.py b/policyengine_uk/utils/parameters.py index 99b36fce24..2e408f7ee4 100644 --- a/policyengine_uk/utils/parameters.py +++ b/policyengine_uk/utils/parameters.py @@ -243,6 +243,17 @@ class RemovedParameter(NamedTuple): "single ticket at one fare, set that band's share to 1 and the others " "to 0, or supply local_bus_uncapped_single_fare for each person.", ), + "gov.dwp.ESA.exempt_work.higher_earnings_limit": RemovedParameter( + "removed ESA exempt work higher earnings limit", + "From 11 April 2011 the limit is 16 x the national living wage, " + "rounded up to the next 50p (ESA Regs 2008 reg 45(4) and (9A)), " + "calculated by esa_exempt_work_higher_earnings_limit. Change " + "gov.hmrc.minimum_wage.non_apprentice (the national living wage is " + "its top rate) or " + "gov.dwp.ESA.exempt_work.higher_earnings_limit_minimum_wage_hours " + "(the 16), or gov.dwp.ESA.exempt_work.fixed_higher_earnings_limit " + "for dates before 11 April 2011.", + ), } diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py new file mode 100644 index 0000000000..53d36f8ec5 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py @@ -0,0 +1,51 @@ +from policyengine_uk.model_api import * + + +class esa_exempt_work_higher_earnings_limit(Variable): + value_type = float + entity = Person + label = "ESA exempt work higher earnings limit" + documentation = ( + "The weekly earnings limit of ESA permitted work (ESA Regs 2008 reg " + "45(4)): work done for less than 16 hours a week, for which earnings " + "in any week do not exceed this amount, is exempt work. It was a " + "fixed amount until 10 April 2011. From 11 April 2011 (SI 2011/674 " + "reg 16(5)) it is 16 x National Minimum Wage, rounded up to the next " + "50p (reg 45(9A)). Reg 2(1) defines the National Minimum Wage as the " + "rate in reg 11 of the National Minimum Wage Regulations 1999, now " + "the national living wage, the highest rate (DMG 41190), so the " + "limit moves with gov.hmrc.minimum_wage.non_apprentice. That " + "parameter holds its April 2026 rate in later years, so the limit " + "stays at £203.50 from 2026-27 until the national living wage is " + "projected, as the Universal Credit benefit cap earnings exception " + "does." + ) + 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/2", + "https://www.legislation.gov.uk/uksi/2011/674/regulation/16", + "https://assets.publishing.service.gov.uk/media/689db34d1fedc616bb13399c/dmg-ch-41.pdf", + ) + + def formula(person, period, parameters): + return parameters(period).gov.dwp.ESA.exempt_work.fixed_higher_earnings_limit + + def formula_2011_04_11(person, period, parameters): + p = parameters(period) + exempt_work = p.gov.dwp.ESA.exempt_work + # Reg 2(1): "the rate of the national minimum wage specified in + # regulation 11" of the 1999 Regulations, which DWP reads as the + # national living wage (DMG 41190), the scale's top rate, as in + # uc_benefit_cap_earnings_exception_threshold. + national_living_wage = p.gov.hmrc.minimum_wage.non_apprentice.calc( + np.full(person.count, 200.0) + ) + limit = ( + national_living_wage * exempt_work.higher_earnings_limit_minimum_wage_hours + ) + # Reg 45(9A): part of a pound under 50p rounds up to 50p, and over + # 50p up to the next pound. Rounding to six places first keeps an + # exact amount such as 16 x £6.50 = £104 from rounding up. + return np.ceil(np.round(limit * 2, 6)) / 2 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 index 1ebc6830dc..abcc67f7ed 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py @@ -45,6 +45,6 @@ def formula(person, period, parameters): 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 + earnings <= person("esa_exempt_work_higher_earnings_limit", period) ) return ~(within_lower_limit | permitted_work) From 283ea2b5720206086d5fcd48c0494d047f4e192d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Mon, 26 Oct 2026 19:29:14 +0000 Subject: [PATCH 2/3] Align apprentice history and test ESA wage reforms --- ...a-permitted-work-limit-from-nlw.changed.md | 2 +- .../minimum_wage/apprentice_age_limit.yaml | 18 ++ .../test_esa_permitted_work_nlw_reforms.py | 156 ++++++++++++++++++ .../tests/test_minimum_wage_rates.py | 48 ++++++ 4 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml create mode 100644 policyengine_uk/tests/test_esa_permitted_work_nlw_reforms.py diff --git a/changelog.d/esa-permitted-work-limit-from-nlw.changed.md b/changelog.d/esa-permitted-work-limit-from-nlw.changed.md index 18b63b9dd1..44f12551df 100644 --- a/changelog.d/esa-permitted-work-limit-from-nlw.changed.md +++ b/changelog.d/esa-permitted-work-limit-from-nlw.changed.md @@ -1 +1 @@ -The ESA permitted work higher earnings limit is now calculated as 16 x the national living wage, rounded up to the next 50p (ESA Regs 2008 reg 45(4) and (9A)), so it follows a change to gov.hmrc.minimum_wage instead of being listed year by year and projected with average earnings. The gov.dwp.ESA.exempt_work.higher_earnings_limit parameter is removed, and the minimum wage rates now start at 1 October 2010. +The ESA permitted work higher earnings limit is now calculated as 16 x the national living wage, rounded up to the next 50p (ESA Regs 2008 reg 45(4) and (9A)), so it follows a change to gov.hmrc.minimum_wage instead of being listed year by year and projected with average earnings. The gov.dwp.ESA.exempt_work.higher_earnings_limit parameter is removed, and the minimum wage rates and apprentice age limit now start at 1 October 2010. Regression tests cover historical wage categories and national living wage reforms across ESA claimants. diff --git a/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml b/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml new file mode 100644 index 0000000000..8f63caf805 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml @@ -0,0 +1,18 @@ +description: Apprentices below this age qualify for the apprentice minimum wage regardless of how long they have been in their apprenticeship. +# The under-19 rule and the apprentice minimum wage rate took effect on +# 1 October 2010. +values: + 2010-10-01: 19 +metadata: + label: Apprentice minimum wage age limit + unit: year + period: year + reference: + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 5 (substituted regulation 13(3)(b)) + href: https://www.legislation.gov.uk/uksi/2010/1901/regulation/5/made + - title: The National Minimum Wage Regulations 1999 (Amendment) Regulations 2010, regulation 1 + href: https://www.legislation.gov.uk/uksi/2010/1901/regulation/1/made + - title: The National Minimum Wage Regulations 2015, regulation 5(1)(b) + href: https://www.legislation.gov.uk/uksi/2015/621/regulation/5 + - title: The National Minimum Wage Regulations 2015, regulation 1 + href: https://www.legislation.gov.uk/uksi/2015/621/regulation/1 diff --git a/policyengine_uk/tests/test_esa_permitted_work_nlw_reforms.py b/policyengine_uk/tests/test_esa_permitted_work_nlw_reforms.py new file mode 100644 index 0000000000..fa3c9ea040 --- /dev/null +++ b/policyengine_uk/tests/test_esa_permitted_work_nlw_reforms.py @@ -0,0 +1,156 @@ +"""Minimum wage reforms change each continuing ESA claim's permitted work gate. + +ESA Regulations 2008 reg. 45(4), (9A): less than 16 weekly hours with net +weekly earnings no greater than 16 times the highest minimum wage, rounded up +to 50p, is permitted work. These recipients have established limited capability +for work; reported awards are continuing awards subject to the model's bounded +screen, rather than newly calculated entitlements. +""" + +import numpy as np +import pytest + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEAR = 2027 +NLW_REFORM = { + f"gov.hmrc.minimum_wage.non_apprentice[{bracket}].amount": {str(YEAR): 13.18} + for bracket in (2, 3, 4) +} + + +def _reported_claimants(): + return { + "claimant": { + "age": 50, + "is_claimant_or_partner": True, + "esa_income_reported": 3_000, + }, + "partner": {"age": 48, "is_claimant_or_partner": True}, + "outside_adult": { + "age": 40, + "is_claimant_or_partner": False, + "employment_income": 10_700, + "hours_worked": 780, + "esa_income_reported": 5_000, + }, + } + + +def _calculate_by_person(simulation, variable, person_order): + return dict(zip(person_order, simulation.calculate(variable, YEAR))) + + +@pytest.mark.parametrize( + "person_order", + [ + ("claimant", "partner", "outside_adult"), + ("outside_adult", "claimant", "partner"), + ("partner", "outside_adult", "claimant"), + ], +) +def test_nlw_reform_changes_only_the_working_persons_gate(person_order): + people = _reported_claimants() + situation = { + "people": { + name: {variable: {YEAR: value} for variable, value in people[name].items()} + for name in person_order + }, + "benunits": { + "unit": {"members": list(person_order), "would_claim_uc": {YEAR: False}} + }, + "households": {"household": {"members": list(person_order)}}, + } + for reform, limit, outside_works in [ + (None, 203.5, True), + (NLW_REFORM, 211.0, False), + ]: + simulation = Simulation(situation=situation, reform=reform) + # No tax or primary NI is due on £10,700: weekly ESA earnings are + # £10,700 / 52 = £205.769..., between the old and reformed caps. + actual_earnings = _calculate_by_person( + simulation, "esa_exempt_work_earnings", person_order + ) + assert actual_earnings == pytest.approx( + {"claimant": 0, "partner": 0, "outside_adult": 10_700 / 52} + ) + np.testing.assert_array_equal( + simulation.calculate("esa_exempt_work_higher_earnings_limit", YEAR), + [limit] * len(person_order), + ) + assert _calculate_by_person( + simulation, "esa_income_claimant_remunerative_work", person_order + ) == { + "claimant": False, + "partner": False, + "outside_adult": outside_works, + } + + +@pytest.mark.parametrize("outside_first", [False, True]) +def test_nlw_reform_preserves_the_couple_award_and_restores_separate_claim( + outside_first, +): + people = _reported_claimants() + # A genuine second benefit unit has its own claimant. Only the person + # gate test above groups the outside adult with the couple. + people["outside_adult"]["is_claimant_or_partner"] = True + person_order = ( + ("outside_adult", "partner", "claimant") + if outside_first + else ("claimant", "partner", "outside_adult") + ) + benunits = { + "couple": {"members": ["claimant", "partner"], "would_claim_uc": {YEAR: False}}, + "separate_claim": { + "members": ["outside_adult"], + "would_claim_uc": {YEAR: False}, + }, + } + unit_order = ( + ("separate_claim", "couple") if outside_first else ("couple", "separate_claim") + ) + situation = { + "people": { + name: {variable: {YEAR: value} for variable, value in people[name].items()} + for name in person_order + }, + "benunits": {name: benunits[name] for name in unit_order}, + "households": {"household": {"members": list(person_order)}}, + } + for reform, eligible, awards in [ + ( + None, + {"couple": True, "separate_claim": False}, + {"couple": 3_000, "separate_claim": 0}, + ), + ( + NLW_REFORM, + {"couple": True, "separate_claim": True}, + {"couple": 3_000, "separate_claim": 5_000}, + ), + ]: + simulation = Simulation(situation=situation, reform=reform) + # Capital, other income and contribution-based ESA default to nil; + # both units decline UC, so neither continuing report has closed. + np.testing.assert_array_equal( + simulation.calculate("esa_income_report_closed", YEAR), + [False] * len(person_order), + ) + np.testing.assert_array_equal( + simulation.calculate("esa_income_assessable_capital", YEAR), [0, 0] + ) + assert ( + dict(zip(unit_order, simulation.calculate("esa_income_eligible", YEAR))) + == eligible + ) + for variable in ("esa_income", "esa"): + actual = dict(zip(unit_order, simulation.calculate(variable, YEAR))) + assert actual == pytest.approx(awards, abs=0.01) + assert dict( + zip( + unit_order, simulation.calculate("claimant_or_partner_esa_income", YEAR) + ) + ) == pytest.approx(awards, abs=0.01) diff --git a/policyengine_uk/tests/test_minimum_wage_rates.py b/policyengine_uk/tests/test_minimum_wage_rates.py index ef3d145f12..83d0f1fc07 100644 --- a/policyengine_uk/tests/test_minimum_wage_rates.py +++ b/policyengine_uk/tests/test_minimum_wage_rates.py @@ -374,3 +374,51 @@ def test_rates_do_not_fall_with_age(year, younger, older): def test_rates_do_not_fall_from_year_to_year(year, age): assert _model_rate(year, age) <= _model_rate(year + 1, age) assert _model_apprentice_rate(year) <= _model_apprentice_rate(year + 1) + + +@pytest.mark.parametrize("instant", ["2010-10-01", "2011-01-01", "2012-01-01"]) +def test_apprentice_age_limit_covers_the_extended_wage_history(instant): + # S.I. 2010/1901 reg. 5 inserted the under-19/first-12-month rule into + # the 1999 Regulations from 1 October 2010, before the 2015 restatement. + assert system.parameters(instant).gov.hmrc.minimum_wage.apprentice_age_limit == 19 + + +@pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +@pytest.mark.parametrize( + "year, expected_rates", + [ + # January 2011 still reads S.I. 2010/1901 regs. 3 and 5; January + # 2012 reads the rates introduced by S.I. 2011/2345 regs. 3 and 5. + (2011, [3.64, 4.92, 5.93, 5.93, 2.50]), + (2012, [3.68, 4.98, 6.08, 6.08, 2.60]), + ], +) +def test_historical_minimum_wage_and_category_remain_available(year, expected_rates): + people = { + "under_18": {"age": 16}, + "young_adult": {"age": 20}, + "adult_21": {"age": 21}, + "adult_40": {"age": 40}, + "apprentice": {"age": 18, "is_apprentice": True}, + } + names = list(people) + simulation = Simulation( + situation={ + "people": { + name: {variable: {year: value} for variable, value in inputs.items()} + for name, inputs in people.items() + }, + "benunits": {f"b_{name}": {"members": [name]} for name in names}, + "households": {"household": {"members": names}}, + } + ) + np.testing.assert_allclose( + simulation.calculate("minimum_wage", year), expected_rates, rtol=0, atol=0.001 + ) + categories = simulation.calculate("minimum_wage_category", year) + if hasattr(categories, "decode_to_str"): + categories = categories.decode_to_str() + np.testing.assert_array_equal( + categories, + ["UNDER_18", "BETWEEN_18_20", "BETWEEN_21_22", "OVER_24", "APPRENTICE"], + ) From bb685f9550a44c780dfe8f1c37fd0c77684c7e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADa=20Juaristi?= <127882282+juaristi22@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:27:12 +0000 Subject: [PATCH 3/3] Correct ESA permitted work history and adult rate lookup --- ...a-permitted-work-limit-from-nlw.changed.md | 2 +- .../fixed_higher_earnings_limit.yaml | 5 ++ .../tests/legacy_award_work_reference.py | 12 +++-- .../tests/test_legacy_benefit_parameters.py | 51 ++++++++++++++++--- .../esa_exempt_work_higher_earnings_limit.py | 14 +++-- 5 files changed, 64 insertions(+), 20 deletions(-) diff --git a/changelog.d/esa-permitted-work-limit-from-nlw.changed.md b/changelog.d/esa-permitted-work-limit-from-nlw.changed.md index 44f12551df..502f89050e 100644 --- a/changelog.d/esa-permitted-work-limit-from-nlw.changed.md +++ b/changelog.d/esa-permitted-work-limit-from-nlw.changed.md @@ -1 +1 @@ -The ESA permitted work higher earnings limit is now calculated as 16 x the national living wage, rounded up to the next 50p (ESA Regs 2008 reg 45(4) and (9A)), so it follows a change to gov.hmrc.minimum_wage instead of being listed year by year and projected with average earnings. The gov.dwp.ESA.exempt_work.higher_earnings_limit parameter is removed, and the minimum wage rates and apprentice age limit now start at 1 October 2010. Regression tests cover historical wage categories and national living wage reforms across ESA claimants. +The ESA permitted work higher earnings limit is now calculated as 16 x the national living wage, rounded up to the next 50p (ESA Regs 2008 reg 45(4) and (9A)), so it follows a change to gov.hmrc.minimum_wage instead of being listed year by year and projected with average earnings. The gov.dwp.ESA.exempt_work.higher_earnings_limit parameter is removed, and the minimum wage rates and apprentice age limit now start at 1 October 2010. Regression tests cover historical wage categories and national living wage reforms across ESA claimants. The historical fixed limit now includes £95 from 1 October 2010, and both the model and reference tests use the designated adult wage band. diff --git a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml index f61bed40fe..bd78dc4658 100644 --- a/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml @@ -10,6 +10,11 @@ values: 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 + 2010-10-01: + value: 95 + reference: + - title: Social Security (Miscellaneous Amendments) (No. 4) Regulations 2010 (SI 2010/2126) reg. 5(2) + href: https://www.legislation.gov.uk/uksi/2010/2126/regulation/5/made metadata: unit: currency-GBP period: week diff --git a/policyengine_uk/tests/legacy_award_work_reference.py b/policyengine_uk/tests/legacy_award_work_reference.py index 9aa06fb191..318019bded 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -132,12 +132,14 @@ def esa_weekly_earnings(adult, parameters): def esa_higher_earnings_limit(parameters): - """Reg 45(4) from 11 April 2011: 16 x the highest National Minimum Wage - rate (reg 2(1), DMG 41190), with part of a pound under 50p rounded up to - 50p and over 50p up to the next pound (reg 45(9A)).""" + """Reg 45(4) from 11 April 2011: 16 x the adult/NLW rate (reg 2(1), + DMG 41190), with part of a pound under 50p rounded up to 50p and over + 50p up to the next pound (reg 45(9A)).""" exempt_work = parameters.gov.dwp.ESA.exempt_work - highest_rate = max(parameters.gov.hmrc.minimum_wage.non_apprentice.amounts) - limit = exempt_work.higher_earnings_limit_minimum_wage_hours * highest_rate + # The scale is sorted by age. The final band holds the designated adult + # rate even if a reform sets a younger band's rate above it. + adult_rate = parameters.gov.hmrc.minimum_wage.non_apprentice.amounts[-1] + limit = exempt_work.higher_earnings_limit_minimum_wage_hours * adult_rate return math.ceil(round(limit * 2, 6)) / 2 diff --git a/policyengine_uk/tests/test_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index 7a2b2bceff..b49333cb51 100644 --- a/policyengine_uk/tests/test_legacy_benefit_parameters.py +++ b/policyengine_uk/tests/test_legacy_benefit_parameters.py @@ -84,8 +84,9 @@ def _higher_earnings_limits(years, reform=None): 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). + # Reg 45(4): £92 as made, £93 from 1 October 2009, £95 from 1 October + # 2010 (SI 2010/2126), then 16 x NMW from 11 April 2011 (SI 2011/674), + # rounded up under reg 45(9A). parameters = CountryTaxBenefitSystem().parameters exempt_work = parameters.gov.dwp.ESA.exempt_work assert exempt_work.fixed_higher_earnings_limit("2008-10-27") == 92 @@ -93,11 +94,11 @@ def test_esa_exempt_work_higher_limit_is_16_x_nmw_rounded_up(): assert exempt_work.fixed_higher_earnings_limit("2009-10-01") == 93 assert exempt_work.higher_earnings_limit_minimum_wage_hours("2011-04-11") == 16 limits = _higher_earnings_limits(range(2009, 2041)) - # Before 2015 a year reads its rules on 1 January: the fixed amount until - # 2011, then 16 x the rate in force that day. + # Before 2015 a year reads its rules on 1 January: fixed amounts through + # 2011, then 16 x the adult rate in force that day from 2012. assert limits[2009] == 92 assert limits[2010] == 93 - assert limits[2011] == 93 + assert limits[2011] == 95 for year in range(2012, 2015): expected = _round_up_to_50p(16 * _rate_in_force(f"{year}-01-01")) assert limits[year] == expected, year @@ -112,7 +113,7 @@ def test_esa_exempt_work_higher_limit_is_16_x_nmw_rounded_up(): # Later years follow the model's national living wage, whatever it is. for year in range(2027, 2041): wage = parameters(str(year)).gov.hmrc.minimum_wage.non_apprentice - assert limits[year] == _round_up_to_50p(16 * max(wage.amounts)), year + assert limits[year] == _round_up_to_50p(16 * wage.amounts[-1]), year def test_esa_exempt_work_higher_limit_follows_a_national_living_wage_reform(): @@ -155,3 +156,41 @@ def test_esa_self_employment_class_2_by_fiscal_year(): 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") + + +def test_esa_fixed_higher_earnings_limit_2010_amendment_boundaries(): + # S.I. 2010/2126 reg. 5(2) raised reg. 45(4)'s £93 limit to £95 from + # 1 October 2010, before S.I. 2011/674 linked it to NMW on 11 April 2011. + fixed_limit = CountryTaxBenefitSystem().parameters.gov.dwp.ESA.exempt_work.fixed_higher_earnings_limit + assert fixed_limit("2010-09-30") == 93 + assert fixed_limit("2010-10-01") == 95 + assert fixed_limit("2011-04-10") == 95 + + +def test_esa_fixed_higher_earnings_limit_annual_transition(): + # Pre-2015 annual variables read 1 January: 2010 precedes the £95 + # amendment, 2011 follows it, and 2012 reads the NMW-linked formula. + assert _higher_earnings_limits([2010, 2011, 2012]) == { + 2010: 93, + 2011: 95, + 2012: 97.5, + } + + +def test_esa_higher_earnings_limit_uses_adult_band_after_youth_only_reform(): + from legacy_award_work_reference import esa_higher_earnings_limit + + # Reform only the under-18 band above the adult/NLW rate. Reg. 2(1) + # designates the adult rate (1999 reg. 11), rather than the largest + # number anywhere in the age scale (the youth rate is in reg. 13). + simulation = Simulation( + situation={"people": {"claimant": {"age": {2026: 40}}}}, + reform={"gov.hmrc.minimum_wage.non_apprentice[0].amount": {"2026": 20}}, + ) + parameters = simulation.tax_benefit_system.parameters("2026") + assert parameters.gov.hmrc.minimum_wage.non_apprentice.amounts[0] == 20 + # Adult rate £12.71: 16 x £12.71 = £203.36, rounded up to £203.50. + assert ( + simulation.calculate("esa_exempt_work_higher_earnings_limit", 2026)[0] == 203.5 + ) + assert esa_higher_earnings_limit(parameters) == 203.5 diff --git a/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py b/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py index 53d36f8ec5..7e28cf2c6b 100644 --- a/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py @@ -13,7 +13,7 @@ class esa_exempt_work_higher_earnings_limit(Variable): "reg 16(5)) it is 16 x National Minimum Wage, rounded up to the next " "50p (reg 45(9A)). Reg 2(1) defines the National Minimum Wage as the " "rate in reg 11 of the National Minimum Wage Regulations 1999, now " - "the national living wage, the highest rate (DMG 41190), so the " + "the national living wage, the adult rate (DMG 41190), so the " "limit moves with gov.hmrc.minimum_wage.non_apprentice. That " "parameter holds its April 2026 rate in later years, so the limit " "stays at £203.50 from 2026-27 until the national living wage is " @@ -35,13 +35,11 @@ def formula(person, period, parameters): def formula_2011_04_11(person, period, parameters): p = parameters(period) exempt_work = p.gov.dwp.ESA.exempt_work - # Reg 2(1): "the rate of the national minimum wage specified in - # regulation 11" of the 1999 Regulations, which DWP reads as the - # national living wage (DMG 41190), the scale's top rate, as in - # uc_benefit_cap_earnings_exception_threshold. - national_living_wage = p.gov.hmrc.minimum_wage.non_apprentice.calc( - np.full(person.count, 200.0) - ) + # Reg 2(1) specifies the adult rate in reg 11 of the 1999 + # Regulations, now the national living wage (DMG 41190). The + # compiled scale orders brackets by age, so its last amount is + # that adult rate, even if a reform raises a younger age band. + national_living_wage = p.gov.hmrc.minimum_wage.non_apprentice.amounts[-1] limit = ( national_living_wage * exempt_work.higher_earnings_limit_minimum_wage_hours )