From 5230dbced97317dde7ca1e48b296ce325ddf6150 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Thu, 13 Aug 2026 12:50:13 +0100 Subject: [PATCH 1/4] Fix benefit cap exemption defects (#1818) Four defects in the benefit cap exception logic: 1. The earnings exception subtracted total income tax liability from earnings, so tax on property, savings, dividend and pension income reduced measured net earnings. Regulation 55(5) of the Universal Credit Regulations 2013 deducts only the tax paid in respect of the employment, primary Class 1 NI and relievable pension contributions. Now uses earned_income_tax, national_insurance and pension_contributions. 2. The earnings threshold was hardcoded as 10_152 in three files. Moved to the dated parameter gov.dwp.benefit_cap_earnings_exemption, expressed monthly as reg. 82 and DWP do, with values for 2020-21 to 2026-27 derived from 16 hours a week at the National Living Wage. 3. Armed Forces Independence Payment is a statutory exception but was never checked. Added to the health/disability qualifying benefits. 4. Removed dead copy-pasted code from all three is_benefit_cap_exempt_* variables, keeping each one's return semantics unchanged apart from defect 3. Co-Authored-By: Claude Opus 5 (1M context) --- changelog.d/1818.fixed.md | 4 + .../dwp/benefit_cap_earnings_exemption.yaml | 22 ++ .../benefit_cap/is_benefit_cap_exempt.yaml | 193 ++++++++++++++++++ .../gov/dwp/is_benefit_cap_exempt.py | 2 +- .../gov/dwp/is_benefit_cap_exempt_earnings.py | 83 +++----- ...is_benefit_cap_exempt_health_disability.py | 14 +- 6 files changed, 254 insertions(+), 64 deletions(-) create mode 100644 changelog.d/1818.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/benefit_cap_earnings_exemption.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml diff --git a/changelog.d/1818.fixed.md b/changelog.d/1818.fixed.md new file mode 100644 index 0000000000..4bda6ec7a6 --- /dev/null +++ b/changelog.d/1818.fixed.md @@ -0,0 +1,4 @@ +- Benefit cap earnings exception (Universal Credit Regulations 2013 reg. 82) now deducts only the tax attributable to earnings, rather than the claimant's whole income tax liability, so property, savings, dividend and pension income no longer erode net earnings and wrongly remove the exception. Relievable pension contributions are now deducted as reg. 55(5) requires. +- Moved the benefit cap earnings threshold out of three hardcoded copies of `10_152` into the dated parameter `gov.dwp.benefit_cap_earnings_exemption`, with values from 2020-21 to 2026-27 (2026-27 rises to £881 a month) derived from 16 hours a week at the National Living Wage. +- Armed Forces Independence Payment now excepts a benefit unit from the benefit cap, as GOV.UK and the regulations provide. The variable has no dataset source, so this changes household calculations only. +- Removed dead copy-pasted code from the three `is_benefit_cap_exempt_*` variables, which computed exemption conditions that were never returned. diff --git a/policyengine_uk/parameters/gov/dwp/benefit_cap_earnings_exemption.yaml b/policyengine_uk/parameters/gov/dwp/benefit_cap_earnings_exemption.yaml new file mode 100644 index 0000000000..ac9d86ca47 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/benefit_cap_earnings_exemption.yaml @@ -0,0 +1,22 @@ +description: Monthly earned income at or above which a Universal Credit claimant (or couple, jointly) is excepted from the benefit cap. Set by regulation 82 of the Universal Credit Regulations 2013 as 16 hours per week at the National Living Wage rate in regulation 4 of the National Minimum Wage Regulations 2015, converted to a monthly amount by multiplying by 52 and dividing by 12, and rounded down to whole pounds by DWP. Earned income for this test is net of income tax, National Insurance contributions and relievable pension contributions on the earnings, and excludes income treated as earned under the minimum income floor. +values: + 2020-04-01: 604 + 2021-04-01: 617 + 2022-04-01: 658 + 2023-04-01: 722 + 2024-04-01: 793 + 2025-04-01: 846 + 2026-04-01: 881 +metadata: + label: Benefit cap earnings exemption threshold + unit: currency-GBP + period: month + reference: + - title: The Universal Credit Regulations 2013 reg. 82 (Exceptions - earnings) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/82 + - title: The Universal Credit Regulations 2013 reg. 55 (Employed earnings) + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/55 + - title: GOV.UK - Benefit cap - When you're not affected + href: https://www.gov.uk/benefit-cap/when-youre-not-affected + - title: GOV.UK - National Minimum Wage and National Living Wage rates + href: https://www.gov.uk/national-minimum-wage-rates diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml new file mode 100644 index 0000000000..10f03798d6 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml @@ -0,0 +1,193 @@ +# Benefit cap exceptions, Universal Credit Regulations 2013 regs. 82-83. +# https://www.legislation.gov.uk/uksi/2013/376/regulation/82 +# https://www.gov.uk/benefit-cap/when-youre-not-affected + +# --- Earnings exception: only tax on the earnings is deducted (reg. 55(5)) --- + +- name: Unearned income does not reduce net earnings for the earnings exception + # Regulation 55(5) deducts the income tax paid "in respect of the + # employment", not the claimant's whole income tax liability. Property + # income of 40,000 pushes total income tax to over 8,000, but the tax + # attributable to the 13,000 of earnings is under 100, so net earnings stay + # well above the 846/month threshold and the exception applies. + period: 2025 + input: + people: + person: + age: 35 + employment_income: 13_000 + property_income: 40_000 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_earnings: true + is_benefit_cap_exempt: true + +- name: Earnings below the threshold do not except, even with large unearned income + # The same unearned income with earnings of 8,000 leaves net earnings below + # 846 x 12 = 10,152, so the exception does not apply. + period: 2025 + input: + people: + person: + age: 35 + employment_income: 8_000 + property_income: 40_000 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_earnings: false + +- name: A couple's earnings are combined for the earnings exception + # Reg. 82(1)(a) applies to "the claimant's and their partner's combined + # earned income". Neither partner alone reaches 10,152; together they do. + period: 2025 + input: + people: + person1: + age: 35 + employment_income: 6_000 + person2: + age: 35 + employment_income: 6_000 + benunits: + benunit: + members: [person1, person2] + output: + is_benefit_cap_exempt_earnings: true + +- name: Relievable pension contributions reduce earned income + # Reg. 55(5)(a) deducts relievable pension contributions from employed + # earnings, so a contribution that takes net earnings below the threshold + # removes the exception. + period: 2025 + input: + people: + person: + age: 35 + employment_income: 11_000 + employee_pension_contributions: 1_500 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_earnings: false + +# --- Earnings threshold is a dated parameter, not a hardcoded constant --- + +- name: The 2025-26 earnings threshold is 846 a month + period: 2025 + input: + people: + person: + age: 35 + employment_income: 10_300 + benunits: + benunit: + members: [person] + output: + # 10,300 clears 846 x 12 = 10,152. + is_benefit_cap_exempt_earnings: true + +- name: The 2026-27 earnings threshold rises to 881 a month + period: 2026 + input: + people: + person: + age: 35 + employment_income: 10_300 + benunits: + benunit: + members: [person] + output: + # The same earnings fall short of 881 x 12 = 10,572. + is_benefit_cap_exempt_earnings: false + +- name: Earnings above the 2026-27 threshold except from the cap + period: 2026 + input: + people: + person: + age: 35 + employment_income: 10_600 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_earnings: true + +# --- Armed Forces Independence Payment is a statutory exception --- + +- name: Armed Forces Independence Payment excepts from the benefit cap + period: 2026 + input: + people: + person: + age: 35 + armed_forces_independence_payment: 5_740 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_health_disability: true + is_benefit_cap_exempt: true + +- name: Without Armed Forces Independence Payment the cap applies + period: 2026 + input: + people: + person: + age: 35 + armed_forces_independence_payment: 0 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_health_disability: false + is_benefit_cap_exempt: false + +- name: A child's Armed Forces Independence Payment excepts the benefit unit + period: 2026 + input: + people: + parent: + age: 35 + child: + age: 10 + armed_forces_independence_payment: 5_740 + benunits: + benunit: + members: [parent, child] + output: + is_benefit_cap_exempt_health_disability: true + +# --- Other exceptions are unchanged --- + +- name: State pension age excepts from the benefit cap + period: 2026 + input: + people: + person: + age: 70 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_other: true + is_benefit_cap_exempt: true + +- name: Armed Forces Compensation Scheme payments except from the benefit cap + period: 2026 + input: + people: + person: + age: 35 + afcs_reported: 2_000 + benunits: + benunit: + members: [person] + output: + is_benefit_cap_exempt_other: true + is_benefit_cap_exempt_health_disability: true diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py index 563b340854..6bd5018997 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt.py @@ -4,7 +4,7 @@ class is_benefit_cap_exempt(Variable): value_type = bool entity = BenUnit - label = "Whether exempt from the benefits cap because of health or disability" + label = "Whether exempt from the benefits cap" definition_period = YEAR reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py index 1bdb797d6a..45019d5150 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py @@ -4,61 +4,42 @@ class is_benefit_cap_exempt_earnings(Variable): value_type = bool entity = BenUnit - label = "Whether exempt from the benefits cap for non-health/disability reasons" + label = "Whether exempt from the benefits cap because of earnings" definition_period = YEAR - reference = "https://www.gov.uk/benefit-cap/when-youre-not-affected" + reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/82" def formula(benunit, period, parameters): + # Regulation 82 of the Universal Credit Regulations 2013 excepts a + # claimant (or couple, on their combined income) from the benefit cap + # where their earned income reaches 16 hours a week at the National + # Living Wage. Earned income here is employed and self-employed + # earnings net of the income tax, National Insurance and relievable + # pension contributions attributable to those earnings (reg. 55(5), + # reg. 57), not net of total tax on all income. + # + # Reg. 82(1)(a) tests "the claimant's earned income or, if the + # claimant is a member of a couple, the couple's combined earned + # income": a dependant's earnings do not count. + # + # Only earned income is read, not the Universal Credit award itself, + # to avoid a circular dependency with the cap. person = benunit.members - - # UC-specific exemptions - # Limited capability for work and work-related activity - has_lcwra = benunit.any(person("uc_limited_capability_for_WRA", period)) - - # Carer element in UC indicates caring for someone with disability - gets_uc_carer_element = benunit("uc_carer_element", period) > 0 - - # Earnings exemption for UC (£846/month = £10,152/year) - # Note: Only check earned income, not UC amount itself to avoid circular dependency - # UC Regs 2013 reg. 82(1)(a) tests "the claimant's earned income or, - # if the claimant is a member of a couple, the couple's combined - # earned income": a dependant's earnings do not count. claimant = person("is_uc_assessed_claimant", period) - uc_earned = benunit.sum( - ( - person("employment_income", period) - + person("self_employment_income", period) - - person("income_tax", period) - - person("national_insurance", period) - ) - * claimant + earnings = person("employment_income", period) + person( + "self_employment_income", period ) - earnings_threshold = 10_152 - meets_earnings_test = uc_earned >= earnings_threshold - - # Disability and carer benefits that exempt from cap - QUAL_PERSONAL_BENEFITS = [ - "attendance_allowance", - "carers_allowance", - "dla", # Disability Living Allowance (includes components) - "pip_dl", # PIP daily living component - "pip_m", # PIP mobility component - "iidb", # Industrial injuries disability benefit - ] - - # ESA and Working Tax Credit - QUAL_BENUNIT_BENEFITS = [ - "esa_income", # Income-based ESA - "working_tax_credit", # If getting WTC, likely working enough - ] - - qualifying_personal_benefits = add(benunit, period, QUAL_PERSONAL_BENEFITS) - qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) - - # Check for Armed Forces Compensation Scheme payments - afcs = benunit("afcs", period) > 0 - - # ESA contribution-based with support component - esa_support_component = benunit("esa_contrib", period) > 0 + # earned_income_tax is the tax on non-savings, non-dividend income, + # which excludes property, savings and dividend income. It is the + # closest available measure of tax on earnings; it still includes tax + # on private and state pension income, which is the approximation we + # accept here (households with pension income are generally already + # exempt through the state pension age exception). + deductions = ( + person("earned_income_tax", period) + + person("national_insurance", period) + + person("pension_contributions", period) + ) + net_earnings = max_(0, benunit.sum((earnings - deductions) * claimant)) - return meets_earnings_test + monthly_threshold = parameters(period).gov.dwp.benefit_cap_earnings_exemption + return net_earnings >= monthly_threshold * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py index 69e1116338..8cb9885693 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py @@ -22,7 +22,6 @@ def formula(benunit, period, parameters): # not lift the cap. not_a_claimant = other_member_of_single_claim_in_shared_rules(person, period) - # UC-specific exemptions # Limited capability for work and work-related activity has_lcwra = benunit.any( person("uc_limited_capability_for_WRA", period) & ~not_a_claimant @@ -31,17 +30,6 @@ def formula(benunit, period, parameters): # Carer element in UC indicates caring for someone with disability gets_uc_carer_element = benunit("uc_carer_element", period) > 0 - # Earnings exemption for UC (£846/month = £10,152/year) - # Note: Only check earned income, not UC amount itself to avoid circular dependency - uc_earned = benunit.sum( - benunit.members("employment_income", period) - + benunit.members("self_employment_income", period) - - benunit.members("income_tax", period) - - benunit.members("national_insurance", period) - ) - earnings_threshold = 10_152 - meets_earnings_test = uc_earned >= earnings_threshold - # Disability and carer benefits that exempt from cap QUAL_PERSONAL_BENEFITS = [ "attendance_allowance", @@ -54,6 +42,8 @@ def formula(benunit, period, parameters): "pip_dl", # PIP daily living component "pip_m", # PIP mobility component "iidb", # Industrial injuries disability benefit + # Armed Forces Independence Payment is a statutory exemption + "armed_forces_independence_payment", ] # ESA and Working Tax Credit From 4a5174c8cdcd33fb374b42c57fec118eaf907117 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Thu, 8 Oct 2026 15:51:17 +0100 Subject: [PATCH 2/4] Drop the strict xfail on the dividend benefit cap case now that #1818's fix makes it pass The earnings exception to the benefit cap now nets only earned_income_tax off earnings, so tax on dividends no longer removes it (#1986). Co-Authored-By: Claude Opus 5.5 --- .../test_uc_income_from_capital_properties.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/policyengine_uk/tests/test_uc_income_from_capital_properties.py b/policyengine_uk/tests/test_uc_income_from_capital_properties.py index 4a07d757fd..fe9c64ca20 100644 --- a/policyengine_uk/tests/test_uc_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_uc_income_from_capital_properties.py @@ -33,10 +33,12 @@ s. 35), which raises the tax a person pays on their earnings and so changes the deduction from them (see test_uc_earnings_deductions_properties.py). The award after the benefit cap is compared only where the cap exemption is -also unchanged: the model's earnings exception to the cap still nets tax on -all of a person's income off their earnings -(PolicyEngine/policyengine-uk#1986), so capital income can switch the cap on. -A strict xfail pins that case. +also unchanged. The cap's earnings exception now nets only the tax on +non-savings, non-dividend income off earnings (PolicyEngine/policyengine-uk#1986), +so savings and dividend income leave it alone, but property income is taxed in +that same band and can still switch the cap on. +test_tax_on_dividends_does_not_remove_the_benefit_cap_earnings_exception pins +the dividend case. """ import numpy as np @@ -323,13 +325,6 @@ def test_tax_on_dividends_does_not_raise_a_working_familys_award(): assert values["uc_earned_income"][0] == pytest.approx(10_000, abs=0.01) -@pytest.mark.xfail( - strict=True, - reason=( - "PolicyEngine/policyengine-uk#1986: the benefit cap earnings exception " - "nets tax on all of a person's income off their earnings" - ), -) def test_tax_on_dividends_does_not_remove_the_benefit_cap_earnings_exception(): # 2026: single claimant aged 30 in London, council rent 20,000, earning # 11,000 with no tax or NI on it. Without dividends the model applies no From 70e426c01768ceb08c379cd877154c0d6bc31819 Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 12 Oct 2026 12:04:06 +0200 Subject: [PATCH 3/4] Limit the AFIP benefit cap exception to the people the regulations name A child's Armed Forces Independence Payment excepted the whole benefit unit, because AFIP was summed over every member. HB Regs 2006 reg 75F(1)(ea) names the claimant, partner or a young person they are responsible for, not a child under 16; for Universal Credit AFIP is within "attendance allowance" (UC Regs 2013 reg 2), which excepts only where a claimant receives it (reg 83(1)(c)). AFIP now counts for the claimant, partner or a Housing Benefit young person. The child test now expects false, with a new young-person test. The AFCS test drops its is_benefit_cap_exempt_other assertion, which #2076 removes when it moves AFCS into is_benefit_cap_exempt_health_disability, and asserts is_benefit_cap_exempt instead. Co-Authored-By: Claude Opus 5.5 --- changelog.d/1818.fixed.md | 2 +- .../benefit_cap/is_benefit_cap_exempt.yaml | 35 +++++++++++++++++-- ...is_benefit_cap_exempt_health_disability.py | 25 +++++++++++-- 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/changelog.d/1818.fixed.md b/changelog.d/1818.fixed.md index 4bda6ec7a6..f43c7ef0de 100644 --- a/changelog.d/1818.fixed.md +++ b/changelog.d/1818.fixed.md @@ -1,4 +1,4 @@ - Benefit cap earnings exception (Universal Credit Regulations 2013 reg. 82) now deducts only the tax attributable to earnings, rather than the claimant's whole income tax liability, so property, savings, dividend and pension income no longer erode net earnings and wrongly remove the exception. Relievable pension contributions are now deducted as reg. 55(5) requires. - Moved the benefit cap earnings threshold out of three hardcoded copies of `10_152` into the dated parameter `gov.dwp.benefit_cap_earnings_exemption`, with values from 2020-21 to 2026-27 (2026-27 rises to £881 a month) derived from 16 hours a week at the National Living Wage. -- Armed Forces Independence Payment now excepts a benefit unit from the benefit cap, as GOV.UK and the regulations provide. The variable has no dataset source, so this changes household calculations only. +- Armed Forces Independence Payment now excepts a benefit unit from the benefit cap when the claimant, their partner or a Housing Benefit young person receives it (HB Regs 2006 reg 75F(1)(ea); UC Regs 2013 regs 2 and 83(1)(c)), not a child under 16. The variable has no dataset source, so this changes household calculations only. - Removed dead copy-pasted code from the three `is_benefit_cap_exempt_*` variables, which computed exemption conditions that were never returned. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml index 10f03798d6..c58cb20870 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml @@ -148,7 +148,11 @@ is_benefit_cap_exempt_health_disability: false is_benefit_cap_exempt: false -- name: A child's Armed Forces Independence Payment excepts the benefit unit +- name: A child's Armed Forces Independence Payment does not except the benefit unit + # HB Regs 2006 reg 75F(1)(ea) names the claimant, partner or a young person + # the claimant is responsible for, not a child under 16. For Universal + # Credit, AFIP is within "attendance allowance" (UC Regs 2013 reg 2), which + # excepts only where "a claimant is receiving" it (reg 83(1)(c)). period: 2026 input: people: @@ -161,6 +165,26 @@ benunit: members: [parent, child] output: + is_benefit_cap_exempt_health_disability: false + is_benefit_cap_exempt: false + +- name: A young person's Armed Forces Independence Payment excepts the benefit unit + # HB Regs 2006 reg 75F(1)(ea) names a young person the claimant is + # responsible for: a Child Benefit qualifying young person (reg 19). + period: 2026 + input: + people: + parent: + age: 45 + young_person: + age: 17 + current_education: UPPER_SECONDARY + armed_forces_independence_payment: 5_740 + benunits: + benunit: + members: [parent, young_person] + output: + is_qualifying_young_person_for_child_benefit: [false, true] is_benefit_cap_exempt_health_disability: true # --- Other exceptions are unchanged --- @@ -179,6 +203,13 @@ is_benefit_cap_exempt: true - name: Armed Forces Compensation Scheme payments except from the benefit cap + # A guaranteed income payment (article 15(1)(c) of the Armed Forces and + # Reserve Forces (Compensation Scheme) Order 2011) and a war disablement + # pension except: UC Regs 2013 reg 83(1)(d)-(e); HB Regs 2006 reg + # 75F(1)(d), with reg 75F(2)(a). This asserts the health and disability + # exception and the overall result, not is_benefit_cap_exempt_other, which + # also checks afcs today but loses it when #2076 moves AFCS into + # is_benefit_cap_exempt_health_disability. period: 2026 input: people: @@ -189,5 +220,5 @@ benunit: members: [person] output: - is_benefit_cap_exempt_other: true is_benefit_cap_exempt_health_disability: true + is_benefit_cap_exempt: true diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py index 8cb9885693..1e5b132eff 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py @@ -42,8 +42,6 @@ def formula(benunit, period, parameters): "pip_dl", # PIP daily living component "pip_m", # PIP mobility component "iidb", # Industrial injuries disability benefit - # Armed Forces Independence Payment is a statutory exemption - "armed_forces_independence_payment", ] # ESA and Working Tax Credit @@ -57,6 +55,28 @@ def formula(benunit, period, parameters): ) qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) + # Armed Forces Independence Payment counts only for the people the + # regulations name. Housing Benefit: "the claimant, the claimant's + # partner or a young person for whom the claimant or the claimant's + # partner is responsible" (HB Regs 2006 reg 75F(1)(ea)), so not a + # child under 16. Universal Credit: AFIP is within "attendance + # allowance" (UC Regs 2013 reg 2), which excepts only where "a + # claimant is receiving" it (reg 83(1)(c)). The model applies one + # cap to both schemes, so the claimant, partner or a Housing Benefit + # young person counts. + housing_benefit_young_person = person( + "is_child_or_young_person_for_legacy_benefits", period + ) & person("is_qualifying_young_person_for_child_benefit", period) + afip_counted = ( + person("is_claimant_or_partner", period) & ~not_a_claimant + ) | housing_benefit_young_person + afip = ( + add_for_members( + benunit, period, ["armed_forces_independence_payment"], afip_counted + ) + > 0 + ) + # Check for Armed Forces Compensation Scheme payments afcs = add_for_members(benunit, period, ["afcs"], ~not_a_claimant) > 0 @@ -70,6 +90,7 @@ def formula(benunit, period, parameters): | gets_uc_carer_element | (qualifying_personal_benefits > 0) | (qualifying_benunit_benefits > 0) + | afip | afcs | esa_support_component ) From e9b20e5d40a1e29ba91570775b118e4be5b682eb Mon Sep 17 00:00:00 2001 From: vahid-ahmadi Date: Mon, 12 Oct 2026 14:50:51 +0200 Subject: [PATCH 4/4] Measure the cap's earned income per claimant and split AFIP by scheme The reg 82 earnings exception now sums each assessed claimant's uc_individual_earned_income_before_mif. Each person's earnings are floored at nil before the couple's are added (UC Regs 2013 reg 57(2)), only tax and NI in respect of employment or self-employment are deducted (reg 55(5)(b), so no tax on private pension income), and contributions paid after 75 are not deducted (FA 2004 s.188(3)(a)). Armed forces independence payment now counts only for a claimant on Universal Credit (reg 2, reg 83(1)(c)), and for the claimant, partner or a young person on Housing Benefit (HB Regs 2006 reg 75F(1)(ea)). Adds regressions for each, with final UC and HB awards, and AFIP to the single-claim property tests. Co-Authored-By: Claude Opus 5.5 --- changelog.d/1818.fixed.md | 4 +- .../benefit_cap/is_benefit_cap_exempt.yaml | 181 +++++++++++++++++- .../test_uc_income_from_capital_properties.py | 12 +- .../tests/test_uc_single_claim_properties.py | 23 ++- .../gov/dwp/is_benefit_cap_exempt_earnings.py | 52 +++-- ...is_benefit_cap_exempt_health_disability.py | 25 ++- 6 files changed, 245 insertions(+), 52 deletions(-) diff --git a/changelog.d/1818.fixed.md b/changelog.d/1818.fixed.md index f43c7ef0de..a4ba29c2be 100644 --- a/changelog.d/1818.fixed.md +++ b/changelog.d/1818.fixed.md @@ -1,4 +1,4 @@ -- Benefit cap earnings exception (Universal Credit Regulations 2013 reg. 82) now deducts only the tax attributable to earnings, rather than the claimant's whole income tax liability, so property, savings, dividend and pension income no longer erode net earnings and wrongly remove the exception. Relievable pension contributions are now deducted as reg. 55(5) requires. +- Benefit cap earnings exception (Universal Credit Regulations 2013 reg. 82) now measures each claimant's own earned income with `uc_individual_earned_income_before_mif`, rather than netting the claimant's whole income tax liability off the couple's combined earnings. Only income tax and National Insurance in respect of employment or self-employment are deducted (reg. 55(5)(b), reg. 57(2)), so tax on pension, property, savings and dividend income no longer removes the exception. Each person's earnings are floored at nil before the couple's are added, so one partner's pension contributions no longer reduce the other's earnings, and contributions paid after 75, which are not relievable (Finance Act 2004 s. 188(3)(a)), are no longer deducted. - Moved the benefit cap earnings threshold out of three hardcoded copies of `10_152` into the dated parameter `gov.dwp.benefit_cap_earnings_exemption`, with values from 2020-21 to 2026-27 (2026-27 rises to £881 a month) derived from 16 hours a week at the National Living Wage. -- Armed Forces Independence Payment now excepts a benefit unit from the benefit cap when the claimant, their partner or a Housing Benefit young person receives it (HB Regs 2006 reg 75F(1)(ea); UC Regs 2013 regs 2 and 83(1)(c)), not a child under 16. The variable has no dataset source, so this changes household calculations only. +- Armed Forces Independence Payment now excepts a benefit unit from the benefit cap, with each scheme's recipients: for a family on Universal Credit only a claimant's AFIP counts (UC Regs 2013 regs 2 and 83(1)(c)); for a family on Housing Benefit the claimant's, their partner's or a young person's (HB Regs 2006 reg 75F(1)(ea)). A child's never does. The variable has no dataset source, so this changes household calculations only. - Removed dead copy-pasted code from the three `is_benefit_cap_exempt_*` variables, which computed exemption conditions that were never returned. diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml index c58cb20870..ea522ac501 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/benefit_cap/is_benefit_cap_exempt.yaml @@ -1,4 +1,5 @@ -# Benefit cap exceptions, Universal Credit Regulations 2013 regs. 82-83. +# Benefit cap exceptions, Universal Credit Regulations 2013 regs. 82-83 and +# Housing Benefit Regulations 2006 reg. 75F. # https://www.legislation.gov.uk/uksi/2013/376/regulation/82 # https://www.gov.uk/benefit-cap/when-youre-not-affected @@ -75,6 +76,115 @@ output: is_benefit_cap_exempt_earnings: false +- name: Tax on private pension income does not reduce earned income for the earnings exception + # Reg. 55(5)(b) deducts only income tax paid "in respect of the + # employment". The 5,000 private pension uses up the personal allowance, + # so earned_income_tax (tax on non-savings, non-dividend income) is 546, + # but none of it is in respect of the 10,300 of wages. Earned income is + # 10,300, which clears 846 x 12 = 10,152, so the cap does not reduce the + # family's Universal Credit. + period: 2025 + input: + people: + parent: + age: 35 + employment_income: 10_300 + private_pension_income: 5_000 + child1: + age: 5 + child2: + age: 8 + benunits: + benunit: + members: [parent, child1, child2] + households: + household: + members: [parent, child1, child2] + region: LONDON + tenure_type: RENT_FROM_COUNCIL + rent: 30_000 + output: + earned_income_tax: [546, 0, 0] + uc_income_tax_on_earnings: [0, 0, 0] + is_benefit_cap_exempt_earnings: true + benefit_cap_reduction: 0 + universal_credit: 33_876.72 + absolute_error_margin: 1 + +- name: One partner's pension contributions do not reduce the other partner's earnings + # Reg. 82(1)(a) adds the couple's earned income, and each person's earned + # income is their own earnings less their own deductions. The + # self-employed partner's 100 of profit less 3,000 of relievable pension + # contributions is nil, not minus 2,900 (reg. 57(2), steps 3 and 4). + # Combined earned income is 10,600, which clears 881 x 12 = 10,572. + period: 2026 + input: + people: + employee: + age: 35 + employment_income: 10_600 + self_employed: + age: 35 + self_employment_income: 100 + personal_pension_contributions: 3_000 + child1: + age: 5 + child2: + age: 8 + benunits: + benunit: + members: [employee, self_employed, child1, child2] + households: + household: + members: [employee, self_employed, child1, child2] + region: LONDON + tenure_type: RENT_FROM_COUNCIL + rent: 30_000 + output: + uc_individual_earned_income_before_mif: [10_600, 0, 0, 0] + is_benefit_cap_exempt_earnings: true + benefit_cap_reduction: 0 + universal_credit: 31_074.08 + absolute_error_margin: 1 + +- name: Pension contributions after 75 are not deducted from earned income + # Contributions paid after the individual reaches 75 are not relievable + # (Finance Act 2004 s. 188(3)(a)), so reg. 55(5)(a) does not deduct them. + # The mixed-age couple's Universal Credit award is capped like any other + # (reg. 3(2)(a); no age exception reaches it), and the older partner's + # 11,000 of earnings clears 881 x 12 = 10,572 before and after the + # 1,000 contribution. + period: 2026 + input: + people: + older_partner: + age: 76 + employment_income: 11_000 + employee_pension_contributions: 1_000 + younger_partner: + age: 40 + child1: + age: 5 + child2: + age: 8 + benunits: + benunit: + members: [older_partner, younger_partner, child1, child2] + households: + household: + members: [older_partner, younger_partner, child1, child2] + region: LONDON + tenure_type: RENT_FROM_COUNCIL + rent: 30_000 + output: + is_uc_assessed_claimant: [true, true, false, false] + housing_benefit_pension_age_regulations_apply: false + uc_individual_earned_income_before_mif: [11_000, 0, 0, 0] + is_benefit_cap_exempt_earnings: true + benefit_cap_reduction: 0 + universal_credit: 42_066.40 + absolute_error_margin: 1 + # --- Earnings threshold is a dated parameter, not a hardcoded constant --- - name: The 2025-26 earnings threshold is 846 a month @@ -168,24 +278,83 @@ is_benefit_cap_exempt_health_disability: false is_benefit_cap_exempt: false -- name: A young person's Armed Forces Independence Payment excepts the benefit unit - # HB Regs 2006 reg 75F(1)(ea) names a young person the claimant is - # responsible for: a Child Benefit qualifying young person (reg 19). +- name: A dependent young person's AFIP does not lift the Universal Credit cap + # For Universal Credit, AFIP is within "attendance allowance" (UC Regs + # 2013 reg 2), and reg 83(1)(c) excepts only where "a claimant is + # receiving" it. The 17-year-old is a qualifying young person, not a + # claimant, so the family's Universal Credit is capped at the London + # family rate of 25,323 a year. + period: 2026 + input: + people: + parent: + age: 45 + young_person: + age: 17 + current_education: UPPER_SECONDARY + armed_forces_independence_payment: 5_740 + child1: + age: 10 + child2: + age: 8 + benunits: + benunit: + members: [parent, young_person, child1, child2] + households: + household: + members: [parent, young_person, child1, child2] + region: LONDON + tenure_type: RENT_FROM_COUNCIL + rent: 30_000 + output: + is_qualifying_young_person_for_child_benefit: [false, true, false, false] + is_uc_assessed_claimant: [true, false, false, false] + is_benefit_cap_exempt_health_disability: false + is_benefit_cap_exempt: false + benefit_cap: 25_323 + universal_credit_pre_benefit_cap: 52_792.44 + benefit_cap_reduction: 30_737.64 + universal_credit: 22_054.80 + absolute_error_margin: 1 + +- name: A dependent young person's AFIP lifts the Housing Benefit cap + # HB Regs 2006 reg 75F(1)(ea) names "the claimant, the claimant's partner + # or a young person for whom the claimant or the claimant's partner is + # responsible". The same family on legacy Housing Benefit (an existing + # award, no Universal Credit claim) is outside the cap, so its Housing + # Benefit is paid in full. Without the AFIP it would be capped by 7,945. period: 2026 input: people: parent: age: 45 + housing_benefit_reported: 10_000 young_person: age: 17 current_education: UPPER_SECONDARY armed_forces_independence_payment: 5_740 + child1: + age: 10 + child2: + age: 8 benunits: benunit: - members: [parent, young_person] + members: [parent, young_person, child1, child2] + would_claim_uc: false + households: + household: + members: [parent, young_person, child1, child2] + region: LONDON + tenure_type: RENT_FROM_COUNCIL + rent: 30_000 output: - is_qualifying_young_person_for_child_benefit: [false, true] + universal_credit_pre_benefit_cap: 0 is_benefit_cap_exempt_health_disability: true + is_benefit_cap_exempt: true + benefit_cap_reduction: 0 + housing_benefit_pre_benefit_cap: 30_000 + housing_benefit: 30_000 + absolute_error_margin: 1 # --- Other exceptions are unchanged --- diff --git a/policyengine_uk/tests/test_uc_income_from_capital_properties.py b/policyengine_uk/tests/test_uc_income_from_capital_properties.py index fe9c64ca20..cb83e9a805 100644 --- a/policyengine_uk/tests/test_uc_income_from_capital_properties.py +++ b/policyengine_uk/tests/test_uc_income_from_capital_properties.py @@ -33,12 +33,12 @@ s. 35), which raises the tax a person pays on their earnings and so changes the deduction from them (see test_uc_earnings_deductions_properties.py). The award after the benefit cap is compared only where the cap exemption is -also unchanged. The cap's earnings exception now nets only the tax on -non-savings, non-dividend income off earnings (PolicyEngine/policyengine-uk#1986), -so savings and dividend income leave it alone, but property income is taxed in -that same band and can still switch the cap on. -test_tax_on_dividends_does_not_remove_the_benefit_cap_earnings_exception pins -the dividend case. +also unchanged. The cap's earnings exception now reads each claimant's +Universal Credit earned income (uc_individual_earned_income_before_mif), which +deducts only the tax on their earnings (uc_income_tax_on_earnings), so +interest, dividends and rent leave it alone too, apart from the allowance +effects above. test_tax_on_dividends_does_not_remove_the_benefit_cap_earnings_exception +pins the dividend case. """ import numpy as np diff --git a/policyengine_uk/tests/test_uc_single_claim_properties.py b/policyengine_uk/tests/test_uc_single_claim_properties.py index 266cd3a93a..6e4b71cb85 100644 --- a/policyengine_uk/tests/test_uc_single_claim_properties.py +++ b/policyengine_uk/tests/test_uc_single_claim_properties.py @@ -115,6 +115,7 @@ "pip_dl": 0.0, "afcs": 0.0, "esa_contrib": 0.0, + "armed_forces_independence_payment": 0.0, } @@ -129,6 +130,7 @@ def adult(draw, ages): pip_dl=draw(pip), afcs=draw(sometimes_paid), esa_contrib=draw(sometimes_paid), + armed_forces_independence_payment=draw(sometimes_paid), ) @@ -157,6 +159,7 @@ def adult_inputs(age, **inputs): pip_dl=0.0, afcs=0.0, esa_contrib=0.0, + armed_forces_independence_payment=0.0, ) values.update(inputs) return values @@ -256,6 +259,16 @@ def example_families(): tenure="RENT_FROM_COUNCIL", region="NORTH_EAST", ), + # An other member with armed forces independence payment and nothing + # else. + dict( + claimant=adult_inputs(40), + partner=adult_inputs(40, armed_forces_independence_payment=5_000.0), + children=[], + rent=0.0, + tenure="RENT_FROM_COUNCIL", + region="NORTH_EAST", + ), ] @@ -269,6 +282,7 @@ def example_families(): afcs_partner=6, esa_partner=7, afcs_claimant=8, + afip_partner=9, ) EXAMPLE_FAMILIES = example_families() @@ -489,12 +503,12 @@ def test_deduction_is_the_joint_claimants(families, year): @given( families=populations, year=st.sampled_from([2021, 2026]), - balances=st.lists(st.floats(0, 30_000), min_size=18, max_size=18), + balances=st.lists(st.floats(0, 30_000), min_size=20, max_size=20), ) @example( families=example_families(), year=2026, - balances=[8_000.0, 16_000.0, 0.0, 30_000.0, 2_000.0, 0.0] + [5_000.0] * 12, + balances=[8_000.0, 16_000.0, 0.0, 30_000.0, 2_000.0, 0.0] + [5_000.0] * 14, ) def test_capital_includes_the_other_members(families, year, balances): families = copy.deepcopy(families) @@ -679,6 +693,11 @@ def test_examples_reach_the_cases(): assert not single["is_benefit_cap_exempt_health_disability"][family] assert joint["is_benefit_cap_exempt_health_disability"][family] assert single["is_benefit_cap_exempt_other"][i["afcs_claimant"]] + # Reg. 83(1)(c) with reg. 2: armed forces independence payment is + # attendance allowance, which lifts the cap only for a claimant. + family = i["afip_partner"] + assert not single["is_benefit_cap_exempt_health_disability"][family] + assert joint["is_benefit_cap_exempt_health_disability"][family] # Reg. 80A(2) and Sch 4 para 28(2): single rate and shared accommodation. family = i["young_renter"] assert single["is_benefit_cap_single_claimant_rate"][family] diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py index 45019d5150..8df3f0e23f 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_earnings.py @@ -6,40 +6,38 @@ class is_benefit_cap_exempt_earnings(Variable): entity = BenUnit label = "Whether exempt from the benefits cap because of earnings" definition_period = YEAR - reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/82" + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/82", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/55", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/57", + "https://www.legislation.gov.uk/ukpga/2004/12/section/188", + ) def formula(benunit, period, parameters): - # Regulation 82 of the Universal Credit Regulations 2013 excepts a - # claimant (or couple, on their combined income) from the benefit cap - # where their earned income reaches 16 hours a week at the National - # Living Wage. Earned income here is employed and self-employed - # earnings net of the income tax, National Insurance and relievable - # pension contributions attributable to those earnings (reg. 55(5), - # reg. 57), not net of total tax on all income. + # Regulation 82(1)(a) of the Universal Credit Regulations 2013 + # excepts an award from the benefit cap where "the claimant's earned + # income or, if the claimant is a member of a couple, the couple's + # combined earned income" reaches 16 hours a week at the National + # Living Wage. A dependant's earnings do not count. # - # Reg. 82(1)(a) tests "the claimant's earned income or, if the - # claimant is a member of a couple, the couple's combined earned - # income": a dependant's earnings do not count. + # Earned income is each claimant's own: their employed earnings less + # their relievable pension contributions and the income tax and + # primary Class 1 contributions paid in respect of the employment + # (reg. 55(5)), and their self-employed earnings after the reg. 57(2) + # steps, which make a loss or a nil result nil. So one partner's + # deductions never come off the other partner's earnings, tax on + # pension, property, savings or dividend income is not deducted, and + # contributions paid after 75 are not relievable (Finance Act 2004 + # s. 188(3)(a)). uc_individual_earned_income_before_mif applies all + # of this per person. It is read before the minimum income floor, + # because reg. 82(4) excludes income treated as earned under reg. 62. # # Only earned income is read, not the Universal Credit award itself, # to avoid a circular dependency with the cap. person = benunit.members claimant = person("is_uc_assessed_claimant", period) - earnings = person("employment_income", period) + person( - "self_employment_income", period + earned_income = benunit.sum( + person("uc_individual_earned_income_before_mif", period) * claimant ) - # earned_income_tax is the tax on non-savings, non-dividend income, - # which excludes property, savings and dividend income. It is the - # closest available measure of tax on earnings; it still includes tax - # on private and state pension income, which is the approximation we - # accept here (households with pension income are generally already - # exempt through the state pension age exception). - deductions = ( - person("earned_income_tax", period) - + person("national_insurance", period) - + person("pension_contributions", period) - ) - net_earnings = max_(0, benunit.sum((earnings - deductions) * claimant)) - monthly_threshold = parameters(period).gov.dwp.benefit_cap_earnings_exemption - return net_earnings >= monthly_threshold * MONTHS_IN_YEAR + return earned_income >= monthly_threshold * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py index 1e5b132eff..0c287b0a7c 100644 --- a/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py +++ b/policyengine_uk/variables/gov/dwp/is_benefit_cap_exempt_health_disability.py @@ -55,21 +55,28 @@ def formula(benunit, period, parameters): ) qualifying_benunit_benefits = add(benunit, period, QUAL_BENUNIT_BENEFITS) - # Armed Forces Independence Payment counts only for the people the - # regulations name. Housing Benefit: "the claimant, the claimant's - # partner or a young person for whom the claimant or the claimant's - # partner is responsible" (HB Regs 2006 reg 75F(1)(ea)), so not a - # child under 16. Universal Credit: AFIP is within "attendance + # Armed Forces Independence Payment counts for different people in + # each scheme. Universal Credit: AFIP is within "attendance # allowance" (UC Regs 2013 reg 2), which excepts only where "a - # claimant is receiving" it (reg 83(1)(c)). The model applies one - # cap to both schemes, so the claimant, partner or a Housing Benefit - # young person counts. + # claimant is receiving" it (reg 83(1)(c)), so a dependant's AFIP + # does not lift the cap. Housing Benefit: "the claimant, the + # claimant's partner or a young person for whom the claimant or the + # claimant's partner is responsible" (HB Regs 2006 reg 75F(1)(ea)), + # so a young person counts but a child under 16 does not. The model + # applies one cap to both schemes; it takes the Universal Credit + # route for a family with a Universal Credit award and the Housing + # Benefit route otherwise. + on_universal_credit = benunit.project( + benunit("universal_credit_pre_benefit_cap", period) > 0 + ) + uc_claimant = person("is_uc_assessed_claimant", period) & ~not_a_claimant housing_benefit_young_person = person( "is_child_or_young_person_for_legacy_benefits", period ) & person("is_qualifying_young_person_for_child_benefit", period) - afip_counted = ( + housing_benefit_person = ( person("is_claimant_or_partner", period) & ~not_a_claimant ) | housing_benefit_young_person + afip_counted = where(on_universal_credit, uc_claimant, housing_benefit_person) afip = ( add_for_members( benunit, period, ["armed_forces_independence_payment"], afip_counted