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 0000000000..05130f5004 --- /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)). 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. 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/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 0000000000..7ab1f87bb5 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/higher_earnings_limit.yaml @@ -0,0 +1,79 @@ +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/hours_limit.yaml b/policyengine_uk/parameters/gov/dwp/ESA/exempt_work/hours_limit.yaml new file mode 100644 index 0000000000..bf1cd5425b --- /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 0000000000..0e4e787473 --- /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 0000000000..60e6524b11 --- /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/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 0000000000..523125dc5c --- /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 0000000000..3bf7a6c5b8 --- /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/parameters/gov/dwp/JSA/hours/couple.yaml b/policyengine_uk/parameters/gov/dwp/JSA/hours/couple.yaml index eb295e1747..a717adbcb7 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. 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 a5b4d47ce7..a43990229e 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/remunerative_work/claimant_hours.yaml b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/claimant_hours.yaml new file mode 100644 index 0000000000..ebf5b28a4c --- /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 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 0000000000..122df194db --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/JSA/remunerative_work/partner_hours.yaml @@ -0,0 +1,18 @@ +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 + 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 + - 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 new file mode 100644 index 0000000000..a6b6a55936 --- /dev/null +++ b/policyengine_uk/tests/legacy_award_work_reference.py @@ -0,0 +1,198 @@ +"""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, +self_employment_income, employee_pension_contributions, +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 + 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)). + 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 + 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. +""" + +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. Covers pay within the + basic rate band, a self-employment profit within the Class 4 upper limit + 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 = 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) 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) + 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 > nics.class_2.lower_profits_threshold + 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 + - notional_class_2 + - pension_from_profit / 2, + ) + return (net_pay + net_profit) / 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_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.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 0000000000..25353a1205 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_remunerative_work.yaml @@ -0,0 +1,1214 @@ +# 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 (DMG 41189). Pay, reg 96(3): less the income tax and +# 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)). +# 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 + +- 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: Reg 99's Class 4 is on the profit before capital allowances (reg 98(5)) + 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 + capital_allowances: 1_000 + personal_pension_contributions: 5_780 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # The actual Class 4 profits are £13,200 - £1,000 = £12,200, under the + # £12,570 lower profits limit, so no Class 4 is due. Reg 99(4)'s + # chargeable income takes no capital allowance (reg 98(5)(a) and (b)): + # reg 99(3)(b) gives 6% x (£13,200 - £12,570) = £37.80, and reg 99(1) + # 20% x £630 = £126. Half the pension contributions, £2,890. + # (£13,200 - £126 - £37.80 - £2,890) / 52 = £195.12, within £195.50; on + # the actual Class 4 it would be £195.85, over the limit. + ni_class_4_main: 0 + esa_exempt_work_earnings: 195.12 + esa_income_claimant_remunerative_work: false + esa_income_eligible: true + esa_income: 5_000 + +- name: Reg 99's Class 4 takes no loss from another trade (reg 98(11)) + 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 + # A loss in a different trade (trading_loss), which Class 4 relieves + # (SSCBA 1992 Sch 2 para 3) but reg 98(11) does not set against the + # profit. + trading_loss: 1_000 + personal_pension_contributions: 5_780 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # As above: no actual Class 4 on £12,200, but £37.80 of notional Class 4 + # on the £13,200 profit, so £195.12. + ni_class_4_main: 0 + esa_exempt_work_earnings: 195.12 + 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 + +- 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 + +- 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 + +# 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 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 + 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 £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 + 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 + +# 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 + 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 + 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 of paid work, whatever the earnings. + 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 + +- 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 + 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 0000000000..3eb1c723c0 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_remunerative_work.yaml @@ -0,0 +1,474 @@ +# 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". 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,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 + 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: + 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 couple, s.1(4), reg 3A(1)). + +- name: Without children, 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: + jsa_income_eligible: true + jsa_income: 3_000 + +- name: Without children, the other member working 16 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 + # 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: + members: [first, second] + households: + household: + members: [first, second] + output: + jsa_income_eligible: true + jsa_income: 3_000 + +- name: Without children, the other member working 20 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: 1_040 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + jsa_income_eligible: true + jsa_income: 3_000 + +- name: Without children, the other member working 23 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: 1_196 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + jsa_income_eligible: true + jsa_income: 3_000 + +- name: Without children, the other member working 24 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 + hours_worked: 1_248 + benunits: + couple: + members: [first, second] + households: + household: + members: [first, second] + output: + jsa_income_eligible: false + jsa_income: 0 + +- name: A couple caring for a placed child have the same 24-hour limit + period: 2025 + absolute_error_margin: 0 + input: + people: + 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 + is_looked_after_by_local_authority: true + benunits: + family: + members: [first, second, child] + households: + household: + members: [first, second, child] + output: + # 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. + +- 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 + +- 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 + 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 2ad7e658bb..232ec21431 100644 --- a/policyengine_uk/tests/test_income_support_eligibility_properties.py +++ b/policyengine_uk/tests/test_income_support_eligibility_properties.py @@ -21,13 +21,17 @@ 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 - the reported amounts do not explain (one that equals neither the award on - everyone's reported amounts nor their plain total); the couple is not - taken to be on State Pension Credit (s.124(1)(g)); and capital is within - the Income Support limit; + amounts after that benefit's capital and remunerative work tests + (legacy_award_work_reference), or an esa_income or jsa_income the reported + amounts do not explain (one that equals neither the award on everyone's + reported amounts nor their plain total); the couple is not taken to be on + State Pension Credit (s.124(1)(g)); and capital is within the Income + Support limit; - raising any claimant's or partner's hours or JSA never makes a family - eligible. (c) and (f) only ever bar a claim. The one other path is (g): + eligible. (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 one other path is (g): income-based JSA reported by the claimant or partner can end the Housing Benefit route to the inferred SI 2019/37 saving (has_mixed_age_couple_pension_credit_saving) when neither member reports @@ -75,6 +79,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 policyengine_uk import Simulation YEAR = 2025 @@ -430,6 +435,9 @@ 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, after the capital + # and remunerative work tests. Members outside the family are not + # candidates when either of them reports one. if esa_income is not None: # Within half a penny of zero is no award. income_related_esa = esa_income > 0.005 @@ -438,13 +446,17 @@ 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: # Within half a penny of zero is no award. income_based_jsa = jsa_income > 0.005 else: - income_based_jsa = JSA.active and income_related_award( - sum(a["jsa_income_reported"] for a in adults), capital, JSA.capital + 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, parameters) ) return ( any(is_claimant(i) for i in range(len(adults))) @@ -457,9 +469,12 @@ def is_claimant(i): ) -def explained_by_reports(esa_income, reported_total, capital, parameters, rules=None): +def explained_by_reports( + esa_income, reported_total, capital, parameters, rules=None, screen=True +): """Whether an esa_income (or, with the JSA capital rules, a jsa_income) - equals what the reported amounts give: the award after the capital test, + equals what the reported amounts give: the award after the capital and + remunerative work tests (screen: whether the reports pass the work test), or their plain total. Income-based JSA is active in YEAR.""" ESA = rules if rules is not None else parameters.gov.dwp.ESA.income.capital tariff = ( @@ -471,7 +486,7 @@ def explained_by_reports(esa_income, reported_total, capital, parameters, rules= ) award = ( max(0, reported_total - tariff) - if reported_total > 0 and capital <= ESA.limit + if reported_total > 0 and capital <= ESA.limit and screen else 0 ) return abs(esa_income - award) <= 0.005 or abs(esa_income - reported_total) <= 0.005 @@ -500,12 +515,19 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): # An esa_income or jsa_income the reported amounts explain is read # through them, like a calculated one. entered_esa = entered_jsa = None + # The award on everyone's reports, which the value rule compares + # with, applies the work tests to the whole benefit unit. + others = [extra] if extra is not None else [] if esa_income is not None: reported_total = sum( member.get("esa_income_reported", 0) for member in adults + [extra] ) if not explained_by_reports( - esa_income[i], reported_total, capital, parameters + esa_income[i], + reported_total, + capital, + parameters, + screen=esa_screen(adults, others, capital, parameters), ): entered_esa = esa_income[i] if jsa_income is not None: @@ -518,6 +540,7 @@ def test_is_eligibility_matches_a_family_by_family_reading(drawn, data): capital, parameters, parameters.gov.dwp.JSA.income.capital, + screen=jsa_screen(adults, others, capital, parameters), ): entered_jsa = jsa_income[i] expected = reference_eligibility( @@ -555,9 +578,10 @@ 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. More JSA can lift - the (g) bar only for a couple with no Income Support report, which is - never eligible (see the module docstring).""" + 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. More + JSA can lift the (g) bar only for a couple with no Income Support report, + which is never eligible (see the module docstring).""" 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 = [ @@ -578,8 +602,24 @@ 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) + # 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", esa_income), ("jsa_income", jsa_income)]: + if direct is None: + 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): + assert lost_award[i] or not (eligible[n + i] and not eligible[i]), ( + drawn[i], + more[i], + ) # One fixed example: situation() records Hypothesis events, so it runs inside diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py index 7c6ef57d72..d378370f3a 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/tests/test_legacy_award_work_properties.py b/policyengine_uk/tests/test_legacy_award_work_properties.py new file mode 100644 index 0000000000..a608afd847 --- /dev/null +++ b/policyengine_uk/tests/test_legacy_award_work_properties.py @@ -0,0 +1,306 @@ +"""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) 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); +- more hours or more pay for anyone never makes a family eligible, in the + 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); +- 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) 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 +from hypothesis import HealthCheck, event, given, settings +from hypothesis import strategies as st + +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. +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] +# 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 Class 1 base includes (all three) and +# the shared taxable pay includes (sick and maternity pay). +STATUTORY_PAY = [ + None, + None, + None, + "statutory_sick_pay", + "statutory_maternity_pay", + "statutory_paternity_pay", +] + + +@st.composite +def adults(draw, outside_family=False): + kind = draw( + st.sampled_from( + ["employee", "employee", "self_employed", "mixed", "loss", "big_loss"] + ) + ) + min_age = 18 if not outside_family else 20 + # 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, + "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 > 0 else 0 + ), + "receives_carer_benefit": draw(st.booleans()), + "care_hours": draw(st.sampled_from([0, 0, 35])), + } + 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]) + ) + 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 + 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 + + +@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") + 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") + 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") + 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( + max_examples=20, + 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) + if esa.any(): + 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 +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])) + 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 + + +@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/tests/test_legacy_benefit_parameters.py b/policyengine_uk/tests/test_legacy_benefit_parameters.py index ae1a373b66..be4cb6cf51 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,93 @@ 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 + # 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(): + 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)) + + +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/_legacy_award_payee.py b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py index 651d32fff1..efe7d060ff 100644 --- a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py +++ b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py @@ -25,28 +25,45 @@ 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 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 + 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 new file mode 100644 index 0000000000..4f82cef51c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/esa_exempt_work_earnings.py @@ -0,0 +1,170 @@ +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), 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 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, 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 " + "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 notional " + "Class 4 is on the profit itself, reg 99(4)'s chargeable income: " + "capital allowances, the trading allowance and losses from another " + "trade or an earlier period do not come off it (reg 98(5)(a), (b) " + "and (d), and (11)), so the Class 4 profits the shared " + "ni_class_4_main is charged on are not used. The notional " + "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." + ) + 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/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", + "https://assets.publishing.service.gov.uk/media/689db34d1fedc616bb13399c/dmg-ch-41.pdf", + ) + + def formula(person, period, parameters): + 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): 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 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_( + 0, taxable_pay - min_(employee_pension, taxable_pay) + ) + taxed_pay = max_(0, taxable_pay_after_pension - allowances) + tax_on_pay = where( + scottish, + rates.scotland.rates.calc(taxed_pay), + rates.uk.calc(taxed_pay), + ) + # 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) + 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 - class_1_on_pay - pension_from_pay / 2, + ) + + # Self-employment, reg 98(3) and reg 99. Reg 99(1): tax at the basic + # 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 on the chargeable income + # between the profits limits. Chargeable income (reg 99(4)) is the + # reg 98 profit, before capital allowances and the trading allowance + # (reg 98(5)(a) and (b)) and with no loss from another trade or an + # earlier period set against it (reg 98(5)(d) and (11)), so the + # shared ni_class_4_main, charged on ni_class_4_profits, is not used. + class_4 = nics.class_4 + class_4_band = max_( + 0, + min_(profit, class_4.thresholds.upper_profits_limit) + - class_4.thresholds.lower_profits_limit, + ) + notional_class_4 = ( + person("ni_class_4_liable", period) * class_4.rates.main * class_4_band + ) + # 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 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, + profit > nics.class_2.lower_profits_threshold, + 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 + - notional_class_2 + - 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 ed3c43cdf0..0f6d2d37fa 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income.py +++ b/policyengine_uk/variables/gov/dwp/esa_income.py @@ -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/esa_income_claimant_remunerative_work.py b/policyengine_uk/variables/gov/dwp/esa_income_claimant_remunerative_work.py new file mode 100644 index 0000000000..1ebc6830dc --- /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 4810792498..8b38b9b46c 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_eligible.py @@ -4,15 +4,63 @@ 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 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 = ( + "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 0000000000..cf50e2251a --- /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_on_income_based_jsa.py b/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py index d22686eb13..71e0e1c564 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,40 @@ 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 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 = ( "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 0989dfd6de..3b91c3daaf 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,37 @@ 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 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 = ( "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.py b/policyengine_uk/variables/gov/dwp/jsa_income.py index 58dad5288d..beae7d3f99 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income.py @@ -18,7 +18,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 30f366cd52..c7bf88abcb 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_eligible.py @@ -4,15 +4,66 @@ 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)), 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 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 = ( + "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", + ) 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 the claimant. + works_as_claimant = hours >= WORK.claimant_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_partner)) - works_as_partner) > 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 0000000000..8bbd2b3ce8 --- /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)