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..502f89050e --- /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 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 new file mode 100644 index 0000000000..bd78dc4658 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/fixed_higher_earnings_limit.yaml @@ -0,0 +1,26 @@ +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 + 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 + 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/apprentice_age_limit.yaml b/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml index e0fb338fa6..8f63caf805 100644 --- a/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml +++ b/policyengine_uk/parameters/gov/hmrc/minimum_wage/apprentice_age_limit.yaml @@ -1,8 +1,8 @@ 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 took effect on 1 October 2010. Values begin with the -# model's minimum wage rate history on 1 October 2012. +# The under-19 rule and the apprentice minimum wage rate took effect on +# 1 October 2010. values: - 2012-10-01: 19 + 2010-10-01: 19 metadata: label: Apprentice minimum wage age limit unit: year 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..318019bded 100644 --- a/policyengine_uk/tests/legacy_award_work_reference.py +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -131,14 +131,25 @@ 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 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 + # 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 + + 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_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_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index be4cb6cf51..b49333cb51 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,65 @@ 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). + # 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 + 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: 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] == 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 + # 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 * wage.amounts[-1]), 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(): @@ -122,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/tests/test_minimum_wage_rates.py b/policyengine_uk/tests/test_minimum_wage_rates.py index 9040b03574..da8fbcdd67 100644 --- a/policyengine_uk/tests/test_minimum_wage_rates.py +++ b/policyengine_uk/tests/test_minimum_wage_rates.py @@ -37,7 +37,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. @@ -87,6 +87,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. @@ -371,3 +376,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"], + ) 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..7e28cf2c6b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_higher_earnings_limit.py @@ -0,0 +1,49 @@ +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 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 " + "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) 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 + ) + # 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)