From d639a0334844b256e9a9cc65bf35ee4ede011145 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 11:59:59 -0400 Subject: [PATCH 1/4] Apply the Carer's Allowance earnings limit (SSCBA 1992 s. 70(1)(b), SI 1976/409 reg. 8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2309. Carer's Allowance was paid whatever the carer earned. A person whose net weekly earnings exceed the limit (£204 from 6 April 2026) is gainfully employed and not entitled. Net earnings are pay and trading profit less the income tax and NI deducted from them and half of pension contributions (Computation of Earnings Regulations 1996 regs. 10(4), 13). Co-Authored-By: Claude Opus 5.5 --- changelog.d/2309.fixed.md | 1 + .../dwp/carers_allowance/earnings_limit.yaml | 62 ++++++++ ..._childcare_entitlement_work_condition.yaml | 8 +- .../dwp/carers_allowance_earnings_limit.yaml | 142 ++++++++++++++++++ ...ers_allowance_earnings_limit_properties.py | 120 +++++++++++++++ .../gov/dwp/carers_allowance_earnings.py | 62 ++++++++ .../gov/dwp/carers_allowance_pre_overlap.py | 7 + ...gainfully_employed_for_carers_allowance.py | 29 ++++ 8 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 changelog.d/2309.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml create mode 100644 policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py create mode 100644 policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py diff --git a/changelog.d/2309.fixed.md b/changelog.d/2309.fixed.md new file mode 100644 index 0000000000..db731a062c --- /dev/null +++ b/changelog.d/2309.fixed.md @@ -0,0 +1 @@ +- Stop paying Carer's Allowance to a carer whose net earnings exceed the earnings limit (£204 a week from April 2026). diff --git a/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml new file mode 100644 index 0000000000..76beb1c9fc --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml @@ -0,0 +1,62 @@ +description: Weekly earnings above which a person is treated as gainfully employed and so is not entitled to Carer's Allowance. The test uses net earnings in the preceding week. +metadata: + economy: false + period: week + unit: currency-GBP + label: Carer's Allowance earnings limit + reference: + - title: The Social Security (Invalid Care Allowance) Regulations 1976 reg. 8(1) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8 + - title: Social Security Contributions and Benefits Act 1992 s. 70(1)(b) + href: https://www.legislation.gov.uk/ukpga/1992/4/section/70 +values: + 2015-04-06: + value: 110 + reference: + - title: The Social Security (Invalid Care Allowance) (Amendment) Regulations 2015 (SI 2015/162) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2015-04-06 + 2017-04-10: + value: 116 + reference: + - title: The Social Security (Invalid Care Allowance) (Amendment) Regulations 2017 (SI 2017/386) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2017-04-10 + 2018-04-09: + value: 120 + reference: + - title: The Social Security (Invalid Care Allowance) (Amendment) Regulations 2018 (SI 2018/280) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2018-04-09 + 2019-04-08: + value: 123 + reference: + - title: The Social Security Benefits Up-rating Regulations 2019 (SI 2019/552) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2019-04-08 + 2020-04-06: + value: 128 + reference: + - title: The Social Security Benefits Up-rating Regulations 2020 (SI 2020/266) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2021-04-12 + 2022-04-11: + value: 132 + reference: + - title: The Social Security Benefits Up-rating Regulations 2022 (SI 2022/342) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2022-04-11 + 2023-04-10: + value: 139 + reference: + - title: The Social Security Benefits Up-rating Regulations 2023 (SI 2023/340) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2023-04-10 + 2024-04-08: + value: 151 + reference: + - title: The Social Security Benefits Up-Rating Regulations 2024 (SI 2024/386) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2024-04-08 + 2025-04-07: + value: 196 + reference: + - title: The Social Security Benefits Up-rating Regulations 2025 (SI 2025/352) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2025-04-07 + 2026-04-06: + value: 204 + reference: + - title: The Social Security Benefits Up-rating Regulations 2026 (SI 2026/218) + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8 diff --git a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml index ef56a091ad..39860f1648 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dfe/extended_childcare_entitlement/extended_childcare_entitlement_work_condition.yaml @@ -506,14 +506,18 @@ # overlapped to nil by maternity allowance: entitled but not paid, so still # excluded. - name: 2022, working partner entitled to Carer's Allowance overlapped to nil - the partner on contributory ESA does not meet the condition + # Carer's Allowance entitlement needs net earnings within the earnings + # limit (£132 a week in 2022-23): pay of £8,400 less half of £3,100 of + # pension contributions is £6,850, or £131.73 a week. period: 2022 input: people: parent1: age: 40 is_parent: true - employment_income: 20_000 - adjusted_net_income: 20_000 + employment_income: 8_400 + employee_pension_contributions: 3_100 + adjusted_net_income: 5_300 care_hours: 35 maternity_allowance_reported: 5_000 minimum_wage: 10 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml new file mode 100644 index 0000000000..fec4df3ccb --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml @@ -0,0 +1,142 @@ +# Carer's Allowance earnings limit. SSCBA 1992 s. 70(1)(b) bars anyone +# gainfully employed; reg. 8(1) of the Social Security (Invalid Care +# Allowance) Regulations 1976 treats a person as gainfully employed when net +# earnings exceed £204 a week from 6 April 2026 (SI 2026/218). Net earnings +# are gross pay less income tax, primary Class 1 contributions and half of +# pension contributions (Computation of Earnings Regulations 1996 reg. +# 10(4)), and trading profit less tax, Class 2 and Class 4 (reg. 13). +# 2026-27: personal allowance and NI primary threshold £12,570, employee +# Class 1 main rate 8%, Class 4 lower profits limit £12,570 at 6%, basic rate +# 20%. Carer's Allowance £86.45 a week. + +- name: Case 1, pay of £8,000 (£153.85 a week, no tax or NI) is under the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 8_000 + care_hours: 40 + country: ENGLAND + output: + carers_allowance_earnings: 8_000 + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 + +- name: Case 2, pay of £15,000 is £275.38 a week net, over the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 15_000 + care_hours: 40 + country: ENGLAND + output: + # Tax 20% x 2,430 = 486; NI 8% x 2,430 = 194.40. + carers_allowance_earnings: 15_000 - 486 - 194.40 + is_gainfully_employed_for_carers_allowance: true + carers_allowance_pre_overlap: 0 + carers_allowance: 0 + +- name: Case 3, earnings of exactly £204 a week have not exceeded the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 204 * 52 + care_hours: 40 + country: ENGLAND + output: + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 + +- name: Case 4, one pound a year more is over the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 204 * 52 + 1 + care_hours: 40 + country: ENGLAND + output: + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + +- name: Case 5, half of pension contributions comes off earnings. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 11_000 + employee_pension_contributions: 1_000 + care_hours: 40 + country: ENGLAND + output: + # 11,000 - 500 = 10,500, or £201.92 a week; without the deduction it + # would be £211.54 and over the limit. + carers_allowance_earnings: 10_500 + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 + +- name: Case 6, trading profit of £14,000 is £262.08 a week net, over the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + self_employment_income: 14_000 + care_hours: 40 + country: ENGLAND + output: + # Tax 20% x 1,430 = 286; Class 4 6% x 1,430 = 85.80. + carers_allowance_earnings: 14_000 - 286 - 85.80 + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + +- name: Case 7, a trading loss counts as nil earnings. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + self_employment_income: -5_000 + care_hours: 40 + country: ENGLAND + output: + carers_allowance_earnings: 0 + carers_allowance: 86.45 * 52 + +- name: Case 8, a reported recipient over the limit is not entitled either. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 15_000 + carers_allowance_reported: 4_000 + country: ENGLAND + output: + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + +- name: Case 9, savings and pension income are not earnings. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + savings_interest_income: 20_000 + private_pension_income: 20_000 + care_hours: 40 + country: ENGLAND + output: + carers_allowance_earnings: 0 + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 + +- name: Case 10, the 2025-26 limit was £196. + period: 2025 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 200 * 52 + care_hours: 40 + country: ENGLAND + output: + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 diff --git a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py new file mode 100644 index 0000000000..d59d49f038 --- /dev/null +++ b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py @@ -0,0 +1,120 @@ +"""Carer's Allowance earnings limit. + +SSCBA 1992 s. 70(1)(b) bars a person who is gainfully employed, and reg. 8(1) +of the Social Security (Invalid Care Allowance) Regulations 1976 treats a +person as gainfully employed when their earnings exceed the limit (£204 a +week from 6 April 2026). Earnings are net of income tax, primary Class 1 (and +Class 2 and 4) contributions and half of pension contributions (Computation of +Earnings Regulations 1996 regs. 10(4) and 13). + +Invariants, for a carer in England in 2026 caring 35 hours or more, with pay, +trading profit, employee pension contributions and unearned income drawn +freely: + +1. Bounds: 0 <= carers_allowance_earnings <= pay + max(0, profit). +2. Differential: the test is passed exactly when + carers_allowance_earnings / 52 <= 204, and the allowance is the full rate + then and nil otherwise. +3. Monotonic: raising pay never lowers net earnings, so a person over the + limit stays over it. +4. Unearned income is not earnings: savings interest and pension income + leave carers_allowance_earnings unchanged. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +LIMIT = 204 +RATE = 86.45 +WEEKS = 52 + + +def _person(pay, profit, pension, savings=0.0, private_pension=0.0): + situation = { + "people": { + "carer": { + "age": {"2026": 45}, + "employment_income": {"2026": pay}, + "self_employment_income": {"2026": profit}, + "employee_pension_contributions": {"2026": pension}, + "savings_interest_income": {"2026": savings}, + "private_pension_income": {"2026": private_pension}, + "care_hours": {"2026": 40}, + } + }, + "benunits": {"benunit": {"members": ["carer"]}}, + "households": { + "household": {"members": ["carer"], "country": {"2026": "ENGLAND"}} + }, + } + sim = Simulation(situation=situation) + return { + name: float(np.asarray(sim.calculate(name, 2026))[0]) + for name in ( + "carers_allowance_earnings", + "is_gainfully_employed_for_carers_allowance", + "carers_allowance", + ) + } + + +money = st.floats(min_value=0, max_value=60_000, allow_nan=False) +profit_money = st.floats(min_value=-20_000, max_value=60_000, allow_nan=False) + + +@settings( + max_examples=12, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given(pay=money, profit=profit_money, pension_share=st.floats(0, 0.3)) +def test_bounds_and_differential(pay, profit, pension_share): + pension = pay * pension_share + result = _person(pay, profit, pension) + earnings = result["carers_allowance_earnings"] + assert -0.01 <= earnings <= pay + max(0, profit) + 0.01 + over = earnings / WEEKS > LIMIT + assert bool(result["is_gainfully_employed_for_carers_allowance"]) == over + expected = 0 if over else RATE * WEEKS + assert abs(result["carers_allowance"] - expected) < 0.01 + + +@settings( + max_examples=8, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given(pay=money, rise=st.floats(min_value=1, max_value=20_000)) +def test_net_earnings_never_fall_as_pay_rises(pay, rise): + lower = _person(pay, 0, 0) + higher = _person(pay + rise, 0, 0) + assert ( + higher["carers_allowance_earnings"] >= lower["carers_allowance_earnings"] - 0.01 + ) + if lower["is_gainfully_employed_for_carers_allowance"]: + assert higher["is_gainfully_employed_for_carers_allowance"] + + +@settings( + max_examples=6, + deadline=None, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + pay=st.floats(min_value=0, max_value=15_000), + savings=money, + private_pension=money, +) +def test_unearned_income_is_not_earnings(pay, savings, private_pension): + alone = _person(pay, 0, 0) + with_unearned = _person(pay, 0, 0, savings=savings, private_pension=private_pension) + assert ( + abs( + alone["carers_allowance_earnings"] + - with_unearned["carers_allowance_earnings"] + ) + < 0.01 + ) diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py new file mode 100644 index 0000000000..f853f306ed --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py @@ -0,0 +1,62 @@ +from policyengine_uk.model_api import * + + +class carers_allowance_earnings(Variable): + value_type = float + entity = Person + label = "earnings for the Carer's Allowance earnings limit" + documentation = ( + "Net earnings from employment and self-employment, as the Carer's " + "Allowance gainful-employment test counts them: gross pay and trading " + "profit less the income tax and National Insurance deducted from them " + "and half of the person's pension contributions. A trading loss counts " + "as nil. The income tax is the tax PAYE would deduct if the earnings " + "were the person's only income: earnings less net-pay pension " + "contributions and the standard personal allowance, at the person's " + "rest-of-UK or Scottish rates. That is the tax deducted from pay, and " + "it avoids a circularity, since Carer's Allowance is itself taxable. " + "The deduction for care charges (Computation of Earnings Regulations " + "1996 Schedule 3, up to half of net earnings) and the Schedule 1 " + "disregards are not modelled, so this can overstate earnings for a " + "carer who pays for care while at work." + ) + definition_period = YEAR + unit = GBP + reference = [ + dict( + title="Social Security Benefit (Computation of Earnings) Regulations 1996 reg. 10(4)", + href="https://www.legislation.gov.uk/uksi/1996/2745/regulation/10", + ), + dict( + title="Social Security Benefit (Computation of Earnings) Regulations 1996 reg. 13", + href="https://www.legislation.gov.uk/uksi/1996/2745/regulation/13", + ), + ] + + def formula(person, period, parameters): + income_tax = parameters(period).gov.hmrc.income_tax + employment = person("employment_income", period) + trading = max_(0, person("self_employment_income", period)) + net_pay_pension = person("employee_pension_contributions", period) + taxable = max_( + 0, + employment + + trading + - net_pay_pension + - income_tax.allowances.personal_allowance.amount, + ) + tax = where( + person("pays_scottish_income_tax", period), + income_tax.rates.scotland.rates.calc(taxable), + income_tax.rates.uk.calc(taxable), + ) + national_insurance = person("uc_national_insurance_on_earnings", period) + half_pension_contributions = 0.5 * person("pension_contributions", period) + return max_( + 0, + employment + + trading + - tax + - national_insurance + - half_pension_contributions, + ) diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_pre_overlap.py b/policyengine_uk/variables/gov/dwp/carers_allowance_pre_overlap.py index ca06fafbb2..6bca6f6b93 100644 --- a/policyengine_uk/variables/gov/dwp/carers_allowance_pre_overlap.py +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_pre_overlap.py @@ -25,9 +25,16 @@ def formula(person, period, parameters): weekly_care_hours = person("care_hours", period) meets_work_condition = weekly_care_hours >= ca.min_hours would_claim = person("would_claim_carers_allowance", period) + # SSCBA 1992 s. 70(1)(b): no allowance for anyone gainfully employed, + # which reg. 8 of the 1976 Regulations defines by an earnings limit. + # It applies to reported recipients too. + gainfully_employed = person( + "is_gainfully_employed_for_carers_allowance", period + ) eligible = ( ~(in_scotland & csp_replaces_ca) & (meets_work_condition | receives_ca) + & ~gainfully_employed & would_claim ) return eligible * ca.rate * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py b/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py new file mode 100644 index 0000000000..f3befdd60b --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class is_gainfully_employed_for_carers_allowance(Variable): + value_type = bool + entity = Person + label = "gainfully employed for Carer's Allowance" + documentation = ( + "Whether the person's weekly net earnings exceed the Carer's Allowance " + "earnings limit, so that they are treated as gainfully employed and " + "are not entitled to the allowance. The regulation tests the earnings " + "of the preceding week; annual earnings are spread evenly over the year." + ) + definition_period = YEAR + reference = [ + dict( + title="Social Security Contributions and Benefits Act 1992 s. 70(1)(b)", + href="https://www.legislation.gov.uk/ukpga/1992/4/section/70", + ), + dict( + title="The Social Security (Invalid Care Allowance) Regulations 1976 reg. 8(1)", + href="https://www.legislation.gov.uk/uksi/1976/409/regulation/8", + ), + ] + + def formula(person, period, parameters): + limit = parameters(period).gov.dwp.carers_allowance.earnings_limit + weekly_earnings = person("carers_allowance_earnings", period) / WEEKS_IN_YEAR + return weekly_earnings > limit From 0d15fc733bd8a1db65c7f580e1878055f9613bc2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 14:43:25 -0400 Subject: [PATCH 2/4] Follow regs 13-14 and reg 5 in the Carer's Allowance earnings test; keep UC carer status Review changes: trading profit takes reg. 14's notional basic-rate tax and main-rate Class 4 rather than progressive PAYE, with one set of personal reliefs; a fraction of a penny of weekly earnings is disregarded (reg. 5); a reported award the person would claim still marks them as a carer for Universal Credit when the earnings limit stops payment (UC reg. 30 has no earnings test); the PAYE approximation is described as one. A property test that assumes an entitled carer now keeps the carer's pay under the limit. Co-Authored-By: Claude Opus 5.5 --- .../dwp/carers_allowance_earnings_limit.yaml | 80 +++++++++++++++- ...ers_allowance_earnings_limit_properties.py | 3 +- ...l_tax_reduction_carer_income_properties.py | 6 ++ .../gov/dwp/carers_allowance_earnings.py | 94 +++++++++++++------ ...gainfully_employed_for_carers_allowance.py | 16 +++- .../demographic/is_carer_for_benefits.py | 9 ++ 6 files changed, 171 insertions(+), 37 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml index fec4df3ccb..8e28e7a095 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml @@ -4,7 +4,8 @@ # earnings exceed £204 a week from 6 April 2026 (SI 2026/218). Net earnings # are gross pay less income tax, primary Class 1 contributions and half of # pension contributions (Computation of Earnings Regulations 1996 reg. -# 10(4)), and trading profit less tax, Class 2 and Class 4 (reg. 13). +# 10(4)), and trading profit less a notional basic-rate tax and main-rate +# Class 4 (regs. 13(4) and 14). A fraction of a penny is disregarded (reg. 5). # 2026-27: personal allowance and NI primary threshold £12,570, employee # Class 1 main rate 8%, Class 4 lower profits limit £12,570 at 6%, basic rate # 20%. Carer's Allowance £86.45 a week. @@ -103,7 +104,7 @@ carers_allowance_earnings: 0 carers_allowance: 86.45 * 52 -- name: Case 8, a reported recipient over the limit is not entitled either. +- name: Case 8, a reported recipient over the limit is not entitled either, but stays a carer for Universal Credit. period: 2026 absolute_error_margin: 0.01 input: @@ -114,6 +115,8 @@ output: is_gainfully_employed_for_carers_allowance: true carers_allowance: 0 + # UC regulation 30 does not apply the Carer's Allowance earnings limit. + is_carer_for_benefits: true - name: Case 9, savings and pension income are not earnings. period: 2026 @@ -140,3 +143,76 @@ output: is_gainfully_employed_for_carers_allowance: true carers_allowance: 0 + +- name: Case 11, trading profit takes notional basic-rate tax and main-rate Class 4 only. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + self_employment_income: 60_000 + care_hours: 40 + country: ENGLAND + output: + # Reg. 14: 20% x (60,000 - 12,570) = 9,486, not the progressive tax; + # 6% x (50,270 - 12,570) = 2,262, with no additional-rate Class 4. + carers_allowance_earnings: 60_000 - 9_486 - 2_262 + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + +- name: Case 12, half of personal pension contributions brings trading earnings under the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + self_employment_income: 15_000 + personal_pension_contributions: 7_700 + care_hours: 40 + country: ENGLAND + output: + # 15,000 - 20% x 2,430 (486) - 6% x 2,430 (145.80) - 7,700 / 2 (3,850) + # = 10,518.20, or £202.27 a week. + carers_allowance_earnings: 10_518.20 + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 + +- name: Case 13, a fraction of a penny over £204 is disregarded. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 10_608.26 + care_hours: 40 + country: ENGLAND + output: + # 10,608.26 / 52 = £204.005, which counts as £204.00 (reg. 5). + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 86.45 * 52 + +- name: Case 14, a whole penny over £204 is over the limit. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 10_608.52 + care_hours: 40 + country: ENGLAND + output: + # 10,608.52 / 52 = £204.01. + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + +- name: Case 15, pay and trading profit share one personal allowance. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 10_000 + self_employment_income: 5_000 + care_hours: 40 + country: ENGLAND + output: + # Pay uses £10,000 of the allowance, leaving £2,570: trading tax + # 20% x (5,000 - 2,570) = 486. No Class 1 on £10,000; no Class 4 under + # £12,570. 15,000 - 486 = 14,514. + carers_allowance_earnings: 14_514 + is_gainfully_employed_for_carers_allowance: true diff --git a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py index d59d49f038..e6cbb5901d 100644 --- a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py +++ b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py @@ -76,7 +76,8 @@ def test_bounds_and_differential(pay, profit, pension_share): result = _person(pay, profit, pension) earnings = result["carers_allowance_earnings"] assert -0.01 <= earnings <= pay + max(0, profit) + 0.01 - over = earnings / WEEKS > LIMIT + # Regulation 5: a fraction of a penny is disregarded. + over = np.floor(earnings / WEEKS * 100 + 1e-6) / 100 > LIMIT assert bool(result["is_gainfully_employed_for_carers_allowance"]) == over expected = 0 if over else RATE * WEEKS assert abs(result["carers_allowance"] - expected) < 0.01 diff --git a/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py b/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py index 8795cdd8c6..e317bf68e5 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_carer_income_properties.py @@ -192,6 +192,12 @@ def situation(families): "private_pension_income": {YEAR: float(pension) * (j == 0)}, } if j == fam["carer"]: + # Keep the carer's pay within the Carer's Allowance + # earnings limit (£204 a week in 2026-27), so the + # carer stays entitled; £10,000 is £192 a week. + person["employment_income"] = { + YEAR: min(a.get("employment_income", 0.0), 10_000.0) + } if fam["by_hours"]: person["care_hours"] = {YEAR: 35} else: diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py index f853f306ed..73cee0fbe6 100644 --- a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py @@ -7,18 +7,26 @@ class carers_allowance_earnings(Variable): label = "earnings for the Carer's Allowance earnings limit" documentation = ( "Net earnings from employment and self-employment, as the Carer's " - "Allowance gainful-employment test counts them: gross pay and trading " - "profit less the income tax and National Insurance deducted from them " - "and half of the person's pension contributions. A trading loss counts " - "as nil. The income tax is the tax PAYE would deduct if the earnings " - "were the person's only income: earnings less net-pay pension " - "contributions and the standard personal allowance, at the person's " - "rest-of-UK or Scottish rates. That is the tax deducted from pay, and " - "it avoids a circularity, since Carer's Allowance is itself taxable. " - "The deduction for care charges (Computation of Earnings Regulations " - "1996 Schedule 3, up to half of net earnings) and the Schedule 1 " - "disregards are not modelled, so this can overstate earnings for a " - "carer who pays for care while at work." + "Allowance gainful-employment test counts them (Computation of " + "Earnings Regulations 1996), less half of the person's pension " + "contributions. A trading loss counts as nil.\n\n" + "Employment (reg. 10(4)): gross pay less the income tax and primary " + "Class 1 contributions deducted from it. The law deducts the tax " + "actually withheld; the model approximates it as the tax PAYE would " + "take on a standard code if the pay were the person's only income " + "(pay less net-pay pension contributions and the personal allowance, " + "at rest-of-UK or Scottish rates). That also avoids a circularity, " + "since Carer's Allowance is itself taxable, and makes unearned income " + "irrelevant here by assumption.\n\n" + "Self-employment (regs. 13(4) and 14): trading profit less a notional " + "income tax at the basic rate (the Scottish basic rate for a Scottish " + "taxpayer) on the profit above whatever personal allowance the pay " + "leaves unused, and Class 4 contributions at the main rate on the " + "profit between the lower and upper profits limits. Regulation 14 " + "deducts no Class 2 contribution.\n\n" + "Not modelled: the deduction for care charges (Schedule 3, up to half " + "of net earnings) and the Schedule 1 disregards, so this can overstate " + "earnings for a carer who pays for care while at work." ) definition_period = YEAR unit = GBP @@ -31,32 +39,56 @@ class carers_allowance_earnings(Variable): title="Social Security Benefit (Computation of Earnings) Regulations 1996 reg. 13", href="https://www.legislation.gov.uk/uksi/1996/2745/regulation/13", ), + dict( + title="Social Security Benefit (Computation of Earnings) Regulations 1996 reg. 14", + href="https://www.legislation.gov.uk/uksi/1996/2745/regulation/14", + ), ] def formula(person, period, parameters): - income_tax = parameters(period).gov.hmrc.income_tax - employment = person("employment_income", period) - trading = max_(0, person("self_employment_income", period)) - net_pay_pension = person("employee_pension_contributions", period) - taxable = max_( - 0, - employment - + trading - - net_pay_pension - - income_tax.allowances.personal_allowance.amount, + p = parameters(period).gov.hmrc + income_tax = p.income_tax + class_4 = p.national_insurance.class_4 + scottish = person("pays_scottish_income_tax", period) + personal_allowance = income_tax.allowances.personal_allowance.amount + + # Employment: PAYE on the pay alone (reg. 10(4)(a)). + pay = person("employment_income", period) + taxable_pay = max_(0, pay - person("employee_pension_contributions", period)) + pay_tax = where( + scottish, + income_tax.rates.scotland.rates.calc( + max_(0, taxable_pay - personal_allowance) + ), + income_tax.rates.uk.calc(max_(0, taxable_pay - personal_allowance)), ) - tax = where( - person("pays_scottish_income_tax", period), - income_tax.rates.scotland.rates.calc(taxable), - income_tax.rates.uk.calc(taxable), + primary_class_1 = person("ni_class_1_employee", period) + + # Self-employment: notional basic-rate tax and main-rate Class 4 + # (reg. 14(1)-(2)), with one set of personal reliefs, used by the pay + # first. + profit = max_(0, person("self_employment_income", period)) + unused_allowance = max_(0, personal_allowance - taxable_pay) + basic_rate = where( + scottish, + income_tax.rates.scotland.rates.rates[1], + income_tax.rates.uk.rates[0], + ) + trading_tax = basic_rate * max_(0, profit - unused_allowance) + trading_class_4 = class_4.rates.main * max_( + 0, + min_(profit, class_4.thresholds.upper_profits_limit) + - class_4.thresholds.lower_profits_limit, ) - national_insurance = person("uc_national_insurance_on_earnings", period) + half_pension_contributions = 0.5 * person("pension_contributions", period) return max_( 0, - employment - + trading - - tax - - national_insurance + pay + - pay_tax + - primary_class_1 + + profit + - trading_tax + - trading_class_4 - half_pension_contributions, ) diff --git a/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py b/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py index f3befdd60b..b6e6b4b981 100644 --- a/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py +++ b/policyengine_uk/variables/gov/dwp/is_gainfully_employed_for_carers_allowance.py @@ -9,7 +9,10 @@ class is_gainfully_employed_for_carers_allowance(Variable): "Whether the person's weekly net earnings exceed the Carer's Allowance " "earnings limit, so that they are treated as gainfully employed and " "are not entitled to the allowance. The regulation tests the earnings " - "of the preceding week; annual earnings are spread evenly over the year." + "of the preceding week; annual earnings are spread evenly over 52 " + "weeks. A fraction of a penny is disregarded, as regulation 5 of the " + "Computation of Earnings Regulations requires when that favours the " + "claimant." ) definition_period = YEAR reference = [ @@ -21,9 +24,16 @@ class is_gainfully_employed_for_carers_allowance(Variable): title="The Social Security (Invalid Care Allowance) Regulations 1976 reg. 8(1)", href="https://www.legislation.gov.uk/uksi/1976/409/regulation/8", ), + dict( + title="Social Security Benefit (Computation of Earnings) Regulations 1996 reg. 5", + href="https://www.legislation.gov.uk/uksi/1996/2745/regulation/5", + ), ] def formula(person, period, parameters): limit = parameters(period).gov.dwp.carers_allowance.earnings_limit - weekly_earnings = person("carers_allowance_earnings", period) / WEEKS_IN_YEAR - return weekly_earnings > limit + weekly = person("carers_allowance_earnings", period) / WEEKS_IN_YEAR + # Disregard a fraction of a penny; the tolerance keeps an exact + # number of pence from being knocked down by floating-point error. + weekly_in_pence = np.floor(weekly * 100 + 1e-6) / 100 + return weekly_in_pence > limit diff --git a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py index 4c5e80aa56..f3ce7c1750 100644 --- a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py +++ b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py @@ -27,8 +27,17 @@ class is_carer_for_benefits(Variable): def formula(person, period, parameters): min_hours = parameters(period).gov.dwp.carers_allowance.min_hours meets_hours = person("care_hours", period) >= min_hours + # A reported Carer's Allowance award the person would claim is + # evidence of the caring itself, where no hours are recorded. It still + # counts when the earnings limit (which the Universal Credit carer + # element does not apply) stops the allowance being paid. An award the + # person would not claim counts no more than it did before. + reports_carers_allowance = ( + person("carers_allowance_reported", period) > 0 + ) & person("would_claim_carers_allowance", period) return ( person("receives_carer_benefit", period) | person("is_entitled_to_carer_benefit", period) | meets_hours + | reports_carers_allowance ) From c4aa96c5ef9569e6b6428f9bd432df00c85ae721 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 15:15:07 -0400 Subject: [PATCH 3/4] Give trading profit the full personal allowance; read the Scottish basic rate robustly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second-round review fixes for #2312: - Reg. 14(1) deducts the personal reliefs the claimant is entitled to, with no reduction for allowance the pay uses, so trading profit now takes the full personal allowance. Sharing one allowance deducted too much tax and could make a carer over the limit eligible (Case 16). - Read the Scottish basic rate from the parameter tree's second bracket. The scale at an instant drops the null starter bracket of 2016-17 and 2017-18, so index 1 was the 40% higher rate in those years. - Case 12 carries £5,000 of capital allowances, which reg. 13(6)(a) does not deduct; Case 15 now expects the full allowance. - Wording: the UC carer status description names the reported-award evidence and calls would_claim a modelling convention; the property-test docstring drops Class 2; the 2020 limit cites the 2020-04-06 version. Co-Authored-By: Claude Opus 5.5 --- .../dwp/carers_allowance/earnings_limit.yaml | 2 +- .../dwp/carers_allowance_earnings_limit.yaml | 33 ++++++++++--- ...ers_allowance_earnings_limit_properties.py | 49 +++++++++++++++++-- .../gov/dwp/carers_allowance_earnings.py | 26 ++++++---- .../demographic/is_carer_for_benefits.py | 13 +++-- 5 files changed, 101 insertions(+), 22 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml b/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml index 76beb1c9fc..22c25451e7 100644 --- a/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml +++ b/policyengine_uk/parameters/gov/dwp/carers_allowance/earnings_limit.yaml @@ -34,7 +34,7 @@ values: value: 128 reference: - title: The Social Security Benefits Up-rating Regulations 2020 (SI 2020/266) - href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2021-04-12 + href: https://www.legislation.gov.uk/uksi/1976/409/regulation/8/2020-04-06 2022-04-11: value: 132 reference: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml index 8e28e7a095..cdb8ee2568 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml @@ -165,12 +165,14 @@ input: age: 45 self_employment_income: 15_000 + capital_allowances: 5_000 personal_pension_contributions: 7_700 care_hours: 40 country: ENGLAND output: # 15,000 - 20% x 2,430 (486) - 6% x 2,430 (145.80) - 7,700 / 2 (3,850) - # = 10,518.20, or £202.27 a week. + # = 10,518.20, or £202.27 a week. Capital allowances are not deducted + # (reg. 13(6)(a)), though they cut the actual Class 4 to nil. carers_allowance_earnings: 10_518.20 is_gainfully_employed_for_carers_allowance: false carers_allowance: 86.45 * 52 @@ -201,7 +203,7 @@ is_gainfully_employed_for_carers_allowance: true carers_allowance: 0 -- name: Case 15, pay and trading profit share one personal allowance. +- name: Case 15, pay and trading profit each take the full personal allowance. period: 2026 absolute_error_margin: 0.01 input: @@ -211,8 +213,27 @@ care_hours: 40 country: ENGLAND output: - # Pay uses £10,000 of the allowance, leaving £2,570: trading tax - # 20% x (5,000 - 2,570) = 486. No Class 1 on £10,000; no Class 4 under - # £12,570. 15,000 - 486 = 14,514. - carers_allowance_earnings: 14_514 + # No tax or Class 1 on £10,000 of pay; no notional tax or Class 4 on + # £5,000 of profit under the full £12,570 (reg. 14(1)). £15,000 is + # £288.46 a week. + carers_allowance_earnings: 15_000 is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 + +- name: Case 16, the pay's use of the allowance does not shrink the profit's. + period: 2026 + absolute_error_margin: 0.01 + input: + age: 45 + employment_income: 6_000 + self_employment_income: 8_000 + personal_pension_contributions: 6_600 + care_hours: 40 + country: ENGLAND + output: + # 6,000 + 8,000 - 6,600 / 2 = 10,700, or £205.76 a week: over the limit. + # Sharing one allowance would deduct 20% x (8,000 - 6,570) = 286 and + # give £200.27, under it. + carers_allowance_earnings: 10_700 + is_gainfully_employed_for_carers_allowance: true + carers_allowance: 0 diff --git a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py index e6cbb5901d..2e6c29bb20 100644 --- a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py +++ b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py @@ -3,9 +3,10 @@ SSCBA 1992 s. 70(1)(b) bars a person who is gainfully employed, and reg. 8(1) of the Social Security (Invalid Care Allowance) Regulations 1976 treats a person as gainfully employed when their earnings exceed the limit (£204 a -week from 6 April 2026). Earnings are net of income tax, primary Class 1 (and -Class 2 and 4) contributions and half of pension contributions (Computation of -Earnings Regulations 1996 regs. 10(4) and 13). +week from 6 April 2026). Earnings are net of income tax, primary Class 1 (or, +for trading profit, Class 4) contributions and half of pension contributions +(Computation of Earnings Regulations 1996 regs. 10(4), 13 and 14; reg. 14(2)(a), +which deducted Class 2, was omitted from 6 April 2024). Invariants, for a carer in England in 2026 caring 35 hours or more, with pay, trading profit, employee pension contributions and unearned income drawn @@ -22,6 +23,7 @@ """ import numpy as np +import pytest from hypothesis import HealthCheck, given, settings from hypothesis import strategies as st @@ -119,3 +121,44 @@ def test_unearned_income_is_not_earnings(pay, savings, private_pension): ) < 0.01 ) + + +@pytest.mark.parametrize("year", [2016, 2017, 2018, 2026]) +@pytest.mark.parametrize("country", ["ENGLAND", "SCOTLAND"]) +def test_trading_profit_takes_the_basic_rate_in_every_year(year, country): + """Reg. 14(1): the basic rate, or for a Scottish taxpayer the Scottish + basic rate, which were both 20% in each of these years. Before 2018-19 the + Scottish scale has no starter rate, so reading its second bracket by + position at an instant would pick the higher rate.""" + profit = 30_000 + simulation = Simulation( + situation={ + "people": { + "carer": { + "age": {year: 45}, + "self_employment_income": {year: profit}, + "care_hours": {year: 40}, + } + }, + "benunits": {"family": {"members": ["carer"]}}, + "households": {"home": {"members": ["carer"], "country": {year: country}}}, + } + ) + hmrc = simulation.tax_benefit_system.parameters(year).gov.hmrc + allowance = hmrc.income_tax.allowances.personal_allowance.amount + class_4 = hmrc.national_insurance.class_4 + expected = ( + profit + - 0.20 * (profit - allowance) + - class_4.rates.main + * ( + min(profit, class_4.thresholds.upper_profits_limit) + - class_4.thresholds.lower_profits_limit + ) + ) + assert bool(simulation.calculate("pays_scottish_income_tax", year)[0]) == ( + country == "SCOTLAND" + ) + assert simulation.calculate("carers_allowance_earnings", year)[0] == pytest.approx( + expected, abs=0.01 + ) diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py index 73cee0fbe6..2b962edb28 100644 --- a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py @@ -20,10 +20,12 @@ class carers_allowance_earnings(Variable): "irrelevant here by assumption.\n\n" "Self-employment (regs. 13(4) and 14): trading profit less a notional " "income tax at the basic rate (the Scottish basic rate for a Scottish " - "taxpayer) on the profit above whatever personal allowance the pay " - "leaves unused, and Class 4 contributions at the main rate on the " - "profit between the lower and upper profits limits. Regulation 14 " - "deducts no Class 2 contribution.\n\n" + "taxpayer) on the profit above the full personal allowance, and Class 4 " + "contributions at the main rate on the profit between the lower and " + "upper profits limits. Regulation 14(1) deducts the personal reliefs " + "the claimant is entitled to, with no reduction for any allowance the " + "pay uses, and capital expenditure is not deducted (reg. 13(6)(a)). " + "Regulation 14 deducts no Class 2 contribution.\n\n" "Not modelled: the deduction for care charges (Schedule 3, up to half " "of net earnings) and the Schedule 1 disregards, so this can overstate " "earnings for a carer who pays for care while at work." @@ -65,16 +67,22 @@ def formula(person, period, parameters): primary_class_1 = person("ni_class_1_employee", period) # Self-employment: notional basic-rate tax and main-rate Class 4 - # (reg. 14(1)-(2)), with one set of personal reliefs, used by the pay - # first. + # (reg. 14(1)-(2)). The tax is on the profit as if it were the only + # income, less the full personal allowance. profit = max_(0, person("self_employment_income", period)) - unused_allowance = max_(0, personal_allowance - taxable_pay) + # The basic rate is the first band of the rest-of-UK scale (ITA 2007 + # s. 6(2)). The Scottish basic rate is the second bracket of the + # Scottish scale, after the starter rate; read it from the parameter + # tree, which keeps the null starter bracket of years before 2018-19 in + # place. The scale at an instant drops null brackets, so its index 1 + # was the higher rate in 2016-17 and 2017-18. + scottish_scale = person.simulation.tax_benefit_system.parameters.gov.hmrc.income_tax.rates.scotland.rates basic_rate = where( scottish, - income_tax.rates.scotland.rates.rates[1], + scottish_scale.brackets[1].rate(period.start), income_tax.rates.uk.rates[0], ) - trading_tax = basic_rate * max_(0, profit - unused_allowance) + trading_tax = basic_rate * max_(0, profit - personal_allowance) trading_class_4 = class_4.rates.main * max_( 0, min_(profit, class_4.thresholds.upper_profits_limit) diff --git a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py index f3ce7c1750..f86e87520d 100644 --- a/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py +++ b/policyengine_uk/variables/household/demographic/is_carer_for_benefits.py @@ -20,7 +20,11 @@ class is_carer_for_benefits(Variable): "person cared for receives a qualifying disability benefit (part of " "the Carer's Allowance conditions referenced by regulation 30), so a " "survey hours field counts caring for " - "anyone and a dataset's take-up input carries that simplification." + "anyone and a dataset's take-up input carries that simplification. " + "A reported Carer's Allowance award the person would claim also " + "counts, so a carer whose earnings put them over the Carer's Allowance " + "earnings limit keeps the Universal Credit carer element, which does " + "not apply that limit (regulation 30(1)(a)(i))." ) reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/30" @@ -30,8 +34,11 @@ def formula(person, period, parameters): # A reported Carer's Allowance award the person would claim is # evidence of the caring itself, where no hours are recorded. It still # counts when the earnings limit (which the Universal Credit carer - # element does not apply) stops the allowance being paid. An award the - # person would not claim counts no more than it did before. + # element does not apply) stops the allowance being paid. Requiring + # would_claim is a modelling convention, not the law: regulation 30(2) + # applies whether or not a claim is made. It keeps an award the person + # would not claim counting no more than it did before; in the data, + # would_claim follows reported receipt. reports_carers_allowance = ( person("carers_allowance_reported", period) > 0 ) & person("would_claim_carers_allowance", period) From b934055b110f34f90848297903953bc04fd30bb2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 11 Oct 2026 17:39:27 -0400 Subject: [PATCH 4/4] Deduct Class 2 from trading earnings before April 2024 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third-round review fix for #2312. Until it was omitted from 6 April 2024, reg. 14(2)(a) of the Computation of Earnings Regulations deducted the Class 2 contributions payable on the chargeable profit. The deduction now uses the engine's Class 2 rules (class_2_liable, the year's contribution weeks) on the profit itself, not ni_class_2's base after capital allowances. The flat rate is nil from 2024-25, so 2026 is unchanged. Case 17 (2018): £7,000 of profit and £1,400 of personal pension contributions give £118.20 a week, under the £120 limit; without Class 2 it was £121.15, over it. The year-by-country oracle deducts Class 2 for 2016-2018. Co-Authored-By: Claude Opus 5.5 --- .../dwp/carers_allowance_earnings_limit.yaml | 18 ++++++++++++++++++ ...rers_allowance_earnings_limit_properties.py | 8 +++++++- .../gov/dwp/carers_allowance_earnings.py | 17 ++++++++++++++++- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml index cdb8ee2568..f7ad5724ac 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/carers_allowance_earnings_limit.yaml @@ -237,3 +237,21 @@ carers_allowance_earnings: 10_700 is_gainfully_employed_for_carers_allowance: true carers_allowance: 0 + +- name: Case 17, before April 2024 trading earnings are also net of Class 2. + period: 2018 + absolute_error_margin: 0.01 + input: + age: 45 + self_employment_income: 7_000 + personal_pension_contributions: 1_400 + care_hours: 40 + country: ENGLAND + output: + # 2018-19: no tax or Class 4 under the £11,850 allowance and lower + # profits limit; Class 2 at £2.95 for 52 weeks (reg. 14(2)(a)) = 153.40; + # half the pension 700. 7,000 - 153.40 - 700 = 6,146.60, £118.20 a week, + # under the £120 limit. Without Class 2 it would be £121.15, over it. + carers_allowance_earnings: 6_146.60 + is_gainfully_employed_for_carers_allowance: false + carers_allowance: 64.60 * 52 diff --git a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py index 2e6c29bb20..497d9ad7f9 100644 --- a/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py +++ b/policyengine_uk/tests/test_carers_allowance_earnings_limit_properties.py @@ -28,6 +28,7 @@ from hypothesis import strategies as st from policyengine_uk import Simulation +from policyengine_uk.utils.class_2 import class_2_contribution_weeks LIMIT = 204 RATE = 86.45 @@ -129,7 +130,9 @@ def test_trading_profit_takes_the_basic_rate_in_every_year(year, country): """Reg. 14(1): the basic rate, or for a Scottish taxpayer the Scottish basic rate, which were both 20% in each of these years. Before 2018-19 the Scottish scale has no starter rate, so reading its second bracket by - position at an instant would pick the higher rate.""" + position at an instant would pick the higher rate. Before 2024-25 reg. + 14(2)(a) also deducted Class 2: £30,000 is above the small profits + threshold in each year, so a full year of the flat rate.""" profit = 30_000 simulation = Simulation( situation={ @@ -147,9 +150,12 @@ def test_trading_profit_takes_the_basic_rate_in_every_year(year, country): hmrc = simulation.tax_benefit_system.parameters(year).gov.hmrc allowance = hmrc.income_tax.allowances.personal_allowance.amount class_4 = hmrc.national_insurance.class_4 + class_2 = hmrc.national_insurance.class_2 + class_2_weeks = class_2_contribution_weeks(year) expected = ( profit - 0.20 * (profit - allowance) + - class_2.flat_rate * class_2_weeks - class_4.rates.main * ( min(profit, class_4.thresholds.upper_profits_limit) diff --git a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py index 2b962edb28..d9d7e6f167 100644 --- a/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py +++ b/policyengine_uk/variables/gov/dwp/carers_allowance_earnings.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.class_2 import class_2_contribution_weeks, class_2_liable class carers_allowance_earnings(Variable): @@ -25,7 +26,10 @@ class carers_allowance_earnings(Variable): "upper profits limits. Regulation 14(1) deducts the personal reliefs " "the claimant is entitled to, with no reduction for any allowance the " "pay uses, and capital expenditure is not deducted (reg. 13(6)(a)). " - "Regulation 14 deducts no Class 2 contribution.\n\n" + "Until 6 April 2024 reg. 14(2)(a) also deducted the Class 2 " + "contributions payable on the chargeable profit; that paragraph was " + "omitted with compulsory Class 2, whose flat rate is nil from " + "2024-25.\n\n" "Not modelled: the deduction for care charges (Schedule 3, up to half " "of net earnings) and the Schedule 1 disregards, so this can overstate " "earnings for a carer who pays for care while at work." @@ -88,6 +92,16 @@ def formula(person, period, parameters): min_(profit, class_4.thresholds.upper_profits_limit) - class_4.thresholds.lower_profits_limit, ) + # Reg. 14(2)(a), omitted from 6 April 2024: the Class 2 contributions + # payable on the chargeable profit (SSCBA 1992 s. 11), on the + # profit itself rather than ni_class_2's base after capital + # allowances. The flat rate is nil from 2024-25. + class_2 = p.national_insurance.class_2 + trading_class_2 = ( + class_2_liable(profit, class_2) + * class_2.flat_rate + * class_2_contribution_weeks(period.start.year) + ) half_pension_contributions = 0.5 * person("pension_contributions", period) return max_( @@ -98,5 +112,6 @@ def formula(person, period, parameters): + profit - trading_tax - trading_class_4 + - trading_class_2 - half_pension_contributions, )