From ab4022bf3d4b26d0e7379b82b9fca7f3e1e67f96 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Thu, 1 Oct 2026 15:31:28 -0400 Subject: [PATCH 1/3] Apply the Pension Credit earnings disregards (SPC Regs 2002 Sch VI) 20 pounds a week for a lone parent (para 1), a carer satisfying Sch I para 4 (para 3), or a claimant or partner on a listed disability benefit or certified blind (para 4(1)); otherwise 5 pounds single and 10 pounds couple (para 5); at most 20 pounds (para 4A), never more than the earnings. Refs #1954. Co-Authored-By: Claude Opus 5.5 --- changelog.d/pc-earnings-disregard.fixed.md | 1 + .../earnings_disregard/higher/amount.yaml | 10 + .../disability_benefit_unit_benefits.yaml | 19 ++ .../higher/disability_benefits.yaml | 44 +++ .../earnings_disregard/standard.yaml | 20 ++ .../pension_credit_earnings_disregard.yaml | 250 ++++++++++++++++++ .../pension_credit/pension_credit_income.yaml | 6 +- ...on_credit_earnings_disregard_properties.py | 136 ++++++++++ .../pension_credit_earnings_disregard.py | 55 ++++ .../pension_credit/pension_credit_income.py | 4 +- 10 files changed, 543 insertions(+), 2 deletions(-) create mode 100644 changelog.d/pc-earnings-disregard.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml create mode 100644 policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml create mode 100644 policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py diff --git a/changelog.d/pc-earnings-disregard.fixed.md b/changelog.d/pc-earnings-disregard.fixed.md new file mode 100644 index 0000000000..8a95e17dbe --- /dev/null +++ b/changelog.d/pc-earnings-disregard.fixed.md @@ -0,0 +1 @@ +Apply the Pension Credit earnings disregards of SPC Regs 2002 Schedule VI: 20 pounds a week for lone parents, carers and claimants or partners on a listed disability benefit or certified blind, otherwise 5 pounds (single) or 10 pounds (couple), capped at earnings. diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml new file mode 100644 index 0000000000..bc51f73542 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/amount.yaml @@ -0,0 +1,10 @@ +description: Weekly earnings disregarded from Pension Credit income for a lone parent, a carer, or a claimant or partner on a listed disability benefit or certified blind. It is the most that can be disregarded however many conditions are met. +values: + 2003-10-06: 20 +metadata: + unit: currency-GBP + period: week + label: Pension Credit higher earnings disregard + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI paras. 1, 3, 4 and 4A + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml new file mode 100644 index 0000000000..8e31a28658 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefit_unit_benefits.yaml @@ -0,0 +1,19 @@ +description: > + Benefit-unit awards whose receipt qualifies the benefit unit for the higher + Pension Credit earnings disregard (Sch. VI para. 4(1)(a)(vii), employment + and support allowance, from 27 October 2008). The model holds income-related + employment and support allowance on the benefit unit, as the claimant's + award; contributory employment and support allowance is a person-level + benefit in `disability_benefits`. +metadata: + label: Pension Credit higher earnings disregard benefit-unit disability benefits + unit: list + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI para. 4(1)(a)(vii) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/4 + - title: The Employment and Support Allowance (Consequential Provisions) (No. 2) Regulations 2008 reg. 4(10)(a)(ii) + href: https://www.legislation.gov.uk/uksi/2008/1554/regulation/4 +values: + 2003-10-06: [] + 2008-10-27: + - esa_income diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml new file mode 100644 index 0000000000..30a23dc061 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml @@ -0,0 +1,44 @@ +description: > + Person-level benefits whose receipt by the claimant or partner qualifies the + benefit unit for the higher Pension Credit earnings disregard (Sch. VI para. + 4(1)(a)). Income-related employment and support allowance, a benefit-unit + award, is in `disability_benefit_unit_benefits`. + `incapacity_benefit` stands for long-term incapacity benefit (para. + 4(1)(a)(i)); the model does not separate the short-term rates. Not modelled: + the war pensions mobility supplement (v), the working tax credit disability + elements (vi), pension age disability payment (iiia, from 21 October 2024), + Scottish adult disability living allowance (iva, from 21 March 2025) and + adult disability payment (viiia, from 21 March 2022), which have no separate + variables. +metadata: + label: Pension Credit higher earnings disregard disability benefits + unit: list + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI para. 4(1)(a) + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/4 + - title: The Employment and Support Allowance (Consequential Provisions) (No. 2) Regulations 2008 reg. 4(10)(a)(ii) (ESA, from 27 October 2008) + href: https://www.legislation.gov.uk/uksi/2008/1554/regulation/4 + - title: The Personal Independence Payment (Supplementary Provisions and Consequential Amendments) Regulations 2013 Sch. para. 27(8)(b) (PIP, from 8 April 2013) + href: https://www.legislation.gov.uk/uksi/2013/388/schedule/paragraph/27 + - title: The Armed Forces and Reserve Forces Compensation Scheme (Consequential Provisions Subordinate Legislation) Order 2013 Sch. para. 23(8)(b) (AFIP, from 8 April 2013) + href: https://www.legislation.gov.uk/uksi/2013/591/schedule/paragraph/23 +values: + 2003-10-06: + - incapacity_benefit + - sda + - attendance_allowance + - dla + 2008-10-27: + - incapacity_benefit + - sda + - attendance_allowance + - dla + - esa_contrib + 2013-04-08: + - incapacity_benefit + - sda + - attendance_allowance + - dla + - esa_contrib + - pip + - armed_forces_independence_payment diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml new file mode 100644 index 0000000000..80ffae79fe --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/standard.yaml @@ -0,0 +1,20 @@ +description: Weekly earnings disregarded from Pension Credit income where neither the claimant nor a partner qualifies for the higher disregard. +SINGLE: + values: + 2003-10-06: 5 + metadata: + label: Pension Credit standard earnings disregard (single) +COUPLE: + values: + 2003-10-06: 10 + metadata: + label: Pension Credit standard earnings disregard (couple) +metadata: + unit: currency-GBP + period: week + propagate_metadata_to_children: true + reference: + - title: The State Pension Credit Regulations 2002 Sch. VI para. 5 + href: https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI/paragraph/5 + - title: The State Pension Credit Regulations 2002 reg. 17(9) + href: https://www.legislation.gov.uk/uksi/2002/1792/regulation/17 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml new file mode 100644 index 0000000000..2f09ea9468 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -0,0 +1,250 @@ +# Expected values are hand-computed from SPC Regs 2002 Sch VI (paras 1, 3, 4, +# 4A and 5): weekly amounts times 52 weeks, capped at the earnings. + +- name: A single claimant with earnings has 5 pounds a week disregarded (para 5(a)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 + +- name: A couple with earnings has 10 pounds a week disregarded (para 5(b)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + partner: + age: 70 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + pension_credit_earnings_disregard: 10 * 52 + +- name: A claimant on attendance allowance has 20 pounds a week disregarded (para 4(1)(a)(iii)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + attendance_allowance: 3000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A partner's personal independence payment qualifies the couple (para 4(1)(a)(viii)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + partner: + age: 70 + pip_dl: 3000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A carer satisfying Sch I para 4 has 20 pounds a week disregarded (para 3). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + is_carer_for_benefits: true + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A lone parent has 20 pounds a week disregarded (para 1). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + child: + age: 10 + benunits: + benunit: + members: [claimant, child] + households: + household: + members: [claimant, child] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: A claimant certified blind has 20 pounds a week disregarded (para 4(1)(b)). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + is_blind: true + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: 20 pounds is the most disregarded when both partners qualify (para 4A). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + attendance_allowance: 3000 + partner: + age: 70 + employment_income: 2000 + dla_sc: 3000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: The disregard never exceeds the earnings. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 100 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 100 + +- name: Self-employed earnings get the same disregard. + period: 2024 + input: + people: + claimant: + age: 70 + self_employment_income: 2000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 + +- name: Without earnings there is nothing to disregard. + period: 2024 + input: + people: + claimant: + age: 70 + attendance_allowance: 3000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 0 + +- name: A dependant child's disability benefit does not qualify a couple for the higher disregard. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + partner: + age: 70 + child: + age: 10 + dla_sc: 3000 + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + pension_credit_earnings_disregard: 10 * 52 + +- name: Income-related ESA, a benefit-unit award, qualifies for the higher disregard. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + benunits: + benunit: + members: [claimant] + esa_income: 1000 + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 + +- name: The disregard comes off Pension Credit income and raises the guarantee credit. + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2000 + state_pension: 9000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # minimum guarantee 218.15 a week in 2024-25 (SI 2024/242) + pension_credit_income: 9_000 + 2_000 - 5 * 52 + guarantee_credit: 218.15 * 52 - (9_000 + 2_000 - 5 * 52) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml index b6cf5c9493..3c1b7febc4 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml @@ -20,7 +20,11 @@ savings: 11_000 output: pension_credit_deemed_income: 104 - pension_credit_income: 343 + # 1 + 2 + 4 + 8 + 32 + 64 + 128 + 104 = 343, less the Sch VI para 5 + # disregard: 5 pounds a week for a single claimant, capped at the 129 of + # earnings. + pension_credit_earnings_disregard: 129 + pension_credit_income: 214 - name: Interest and dividends are not counted directly in Pension Credit income. period: 2021 diff --git a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py new file mode 100644 index 0000000000..2aaa136839 --- /dev/null +++ b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py @@ -0,0 +1,136 @@ +"""Properties of the Pension Credit earnings disregards (SPC Regs 2002 Sch VI). + +Invariants, over generated pension-age benefit units: + +1. Statute: the disregard equals min(earnings, 52 x w), where w is 20 if the + unit has a lone parent, a carer satisfying Sch I para 4, or a claimant or + partner on a listed disability benefit or certified blind, and otherwise 5 + for a single claimant and 10 for a couple. The expected value is computed + here from the inputs, not read from the model. +2. Bounds: 0 <= disregard <= min(earnings, 20 x 52). +3. Differential: Pension Credit income is the pre-change income (sources less + tax, NI and half of pension contributions, floored at 0, recomputed from + the model's own components) less the disregard, floored at 0. +4. Monotonicity: the guarantee credit never falls when the disregard applies, + i.e. it is at least the guarantee credit without it. +5. The dated benefit lists follow the amending instruments: ESA from 27 + October 2008, PIP and AFIP from 8 April 2013. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.model_api import WEEKS_IN_YEAR + +YEAR = 2024 +SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +DISABILITY = [ + "attendance_allowance", + "dla_sc", + "pip_dl", + "incapacity_benefit", + "esa_contrib", +] + + +@st.composite +def units(draw): + couple = draw(st.booleans()) + child = draw(st.booleans()) and not couple + people = {} + names = ["claimant"] + (["partner"] if couple else []) + for n in names: + person = { + "age": draw(st.sampled_from([67, 72, 85])), + "employment_income": draw(st.sampled_from([0, 50, 400, 3_000])), + "self_employment_income": draw(st.sampled_from([0, 0, 1_500])), + "state_pension": draw(st.integers(min_value=0, max_value=12_000)), + "private_pension_income": draw(st.sampled_from([0, 2_000])), + "is_blind": draw(st.sampled_from([False, False, False, True])), + "is_carer_for_benefits": draw(st.sampled_from([False, False, False, True])), + } + benefit = draw(st.sampled_from([None, None] + DISABILITY)) + if benefit: + person[benefit] = 2_000 + people[n] = person + if child: + people["child"] = {"age": 10, "dla_sc": draw(st.sampled_from([0, 2_000]))} + return people + + +def simulate(people): + members = list(people) + dated = { + name: {k: {str(YEAR): v} for k, v in person.items()} + for name, person in people.items() + } + return Simulation( + situation={ + "people": dated, + "benunits": {"b": {"members": members}}, + "households": {"h": {"members": members}}, + } + ) + + +@SETTINGS +@given(units()) +def test_statute_bounds_differential_and_monotonicity(people): + sim = simulate(people) + calc = lambda v: float(sim.calculate(v, YEAR)[0]) + adults = [p for n, p in people.items() if n != "child"] + earnings = sum(p["employment_income"] + p["self_employment_income"] for p in adults) + couple = len(adults) == 2 + disabled = any( + p.get("is_blind") or any(p.get(b, 0) > 0 for b in DISABILITY) for p in adults + ) + carer = calc("carer_minimum_guarantee_addition") > 0 + lone_parent = "child" in people + weekly = 20 if (disabled or carer or lone_parent) else (10 if couple else 5) + expected = min(earnings, weekly * WEEKS_IN_YEAR) + disregard = calc("pension_credit_earnings_disregard") + assert np.isclose(disregard, expected, atol=0.01) + assert 0 <= disregard <= min(earnings, 20 * WEEKS_IN_YEAR) + 0.01 + + sources = sim.tax_benefit_system.parameters( + f"{YEAR}-06-01" + ).gov.dwp.pension_credit.guarantee_credit.income + total = sum(float(sim.calculate(s, YEAR, map_to="benunit")[0]) for s in sources) + deductions = ( + float(sim.calculate("income_tax", YEAR, map_to="benunit")[0]) + + float(sim.calculate("national_insurance", YEAR, map_to="benunit")[0]) + + 0.5 * float(sim.calculate("pension_contributions", YEAR, map_to="benunit")[0]) + ) + before = max(0.0, total - deductions) + assert np.isclose( + calc("pension_credit_income"), max(0.0, before - disregard), atol=0.01 + ) + gc_without = max(0.0, calc("minimum_guarantee") - before) + assert calc("guarantee_credit") >= gc_without - 0.01 + + +def test_benefit_lists_follow_amending_instruments(): + from policyengine_uk.system import system + + def lists(date): + p = system.parameters(date).gov.dwp.pension_credit.earnings_disregard.higher + return set(p.disability_benefits), set(p.disability_benefit_unit_benefits) + + person, unit = lists("2008-10-26") + assert "esa_contrib" not in person and "esa_income" not in unit + person, unit = lists("2008-10-27") + assert "esa_contrib" in person and "esa_income" in unit + person, _ = lists("2013-04-07") + assert "pip" not in person and "armed_forces_independence_payment" not in person + person, _ = lists("2013-04-08") + assert {"pip", "armed_forces_independence_payment"} <= person + for date in ["2008-10-26", "2013-04-08", "2026-04-06"]: + person, _ = lists(date) + assert {"incapacity_benefit", "sda", "attendance_allowance", "dla"} <= person diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py new file mode 100644 index 0000000000..5b93defa35 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py @@ -0,0 +1,55 @@ +from policyengine_uk.model_api import * + + +class pension_credit_earnings_disregard(Variable): + label = "Pension Credit earnings disregard" + documentation = ( + "Earnings disregarded from Pension Credit income under Schedule VI to " + "the State Pension Credit Regulations 2002: 20 pounds a week for a " + "lone parent (para. 1), where the claimant or partner is a carer " + "satisfying Sch. I para. 4 (para. 3), or where the claimant or partner " + "receives a listed disability benefit or is certified blind " + "(para. 4(1)); otherwise 5 pounds a week for a single claimant and 10 " + "pounds for a couple (para. 5). 20 pounds is the most disregarded " + "however many conditions are met (para. 4A), and the disregard never " + "exceeds the earnings. Not modelled: the employment-specific 20 pound " + "disregards of paras. 2 to 2B (part-time firefighters, auxiliary " + "coastguards, lifeboat crew, reserve forces) and the transitional " + "protections of para. 4(2) to (4)." + ) + entity = BenUnit + definition_period = YEAR + value_type = float + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + ) + + def formula(benunit, period, parameters): + p = parameters(period).gov.dwp.pension_credit.earnings_disregard + person = benunit.members + claimant_or_partner = person("is_uc_claimant", period) + on_disability_benefit = ( + add(person, period, p.higher.disability_benefits) > 0 + ) | person("is_blind", period) + unit_disability_benefits = p.higher.disability_benefit_unit_benefits + unit_on_disability_benefit = ( + add(benunit, period, unit_disability_benefits) > 0 + if len(unit_disability_benefits) > 0 + else False + ) + disabled = ( + benunit.any(claimant_or_partner & on_disability_benefit) + | unit_on_disability_benefit + ) + carer = benunit("carer_minimum_guarantee_addition", period) > 0 + lone_parent = benunit("is_lone_parent", period) + relation_type = benunit("relation_type", period) + weekly = where( + lone_parent | carer | disabled, + p.higher.amount, + p.standard[relation_type], + ) + earnings = max_(0, benunit("pension_credit_earnings", period)) + return min_(weekly * WEEKS_IN_YEAR, earnings) diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py index 1160800f3a..7a9ec44199 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_income.py @@ -46,4 +46,6 @@ def formula(benunit, period, parameters): ) pen_con_deduction_rate = pc.income.pension_contributions_deduction deductions = tax + pension_contributions * pen_con_deduction_rate - return max_(0, total - deductions) + # Schedule VI earnings disregards (reg. 17(9)). + earnings_disregard = benunit("pension_credit_earnings_disregard", period) + return max_(0, total - deductions - earnings_disregard) From 3d4f12b24f6ed0d421ed940551e66d424d15db51 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Fri, 2 Oct 2026 10:06:22 -0400 Subject: [PATCH 2/3] Cap the Pension Credit earnings disregard at net earnings (review of #2019) Reg 17(9) disregards the Sch VI sums in calculating the claimant's earnings, which are net of income tax and NI (reg 17(10)) and of half of pension contributions (reg 17A(4A)). Capping at gross earnings let a taxpayer's first pounds of earnings raise Pension Credit. Income tax on earnings is the lesser of the person's income tax and the basic rate on their earnings. Also: the carer condition reads the claimant's or partner's is_carer_for_benefits (Sch I para 4), not the carer addition, which counts dependants; is_claimant_or_partner after #1896; property tests compute net earnings independently, include dependants' DLA and caring, and add that earning more never raises Pension Credit; parameter description and test comment fixes. Co-Authored-By: Claude Opus 5.5 --- .../higher/disability_benefits.yaml | 14 ++- .../claimant_partner_income_and_carers.yaml | 8 +- .../pension_credit_earnings_disregard.yaml | 65 +++++++++++ .../pension_credit/pension_credit_income.yaml | 6 +- ...on_credit_earnings_disregard_properties.py | 109 +++++++++++++----- .../pension_credit_earnings_disregard.py | 42 +++++-- 6 files changed, 196 insertions(+), 48 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml index 30a23dc061..e2ea6f3328 100644 --- a/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/pension_credit/earnings_disregard/higher/disability_benefits.yaml @@ -5,11 +5,13 @@ description: > award, is in `disability_benefit_unit_benefits`. `incapacity_benefit` stands for long-term incapacity benefit (para. 4(1)(a)(i)); the model does not separate the short-term rates. Not modelled: - the war pensions mobility supplement (v), the working tax credit disability - elements (vi), pension age disability payment (iiia, from 21 October 2024), - Scottish adult disability living allowance (iva, from 21 March 2025) and - adult disability payment (viiia, from 21 March 2022), which have no separate - variables. + the war pensions mobility supplement (v), which has no variable; pension age + disability payment (iiia, from 21 October 2024), Scottish adult disability + living allowance (iva, from 21 March 2025) and adult disability payment + (viiia, from 21 March 2022), which have no separate variables; and the + working tax credit disability elements (vi), left out because in the model + they follow from DLA or PIP, which are listed, and working tax credit ended + in April 2025. metadata: label: Pension Credit higher earnings disregard disability benefits unit: list @@ -20,7 +22,7 @@ metadata: href: https://www.legislation.gov.uk/uksi/2008/1554/regulation/4 - title: The Personal Independence Payment (Supplementary Provisions and Consequential Amendments) Regulations 2013 Sch. para. 27(8)(b) (PIP, from 8 April 2013) href: https://www.legislation.gov.uk/uksi/2013/388/schedule/paragraph/27 - - title: The Armed Forces and Reserve Forces Compensation Scheme (Consequential Provisions Subordinate Legislation) Order 2013 Sch. para. 23(8)(b) (AFIP, from 8 April 2013) + - title: "The Armed Forces and Reserve Forces Compensation Scheme (Consequential Provisions: Subordinate Legislation) Order 2013 Sch. para. 23(8)(b) (AFIP, from 8 April 2013)" href: https://www.legislation.gov.uk/uksi/2013/591/schedule/paragraph/23 values: 2003-10-06: diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml index dbf4bb5dbf..991253993f 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/claimant_partner_income_and_carers.yaml @@ -160,9 +160,13 @@ is_claimant_or_partner: [true, true] # SPCA 2002 s.5: gross = 5,000 + 1,000 + 100 + 2,000 + 500 + 200 # + 104 = 8,904; deductions = 100 + 50 + 25 + 50% * 300 = 325. - pension_credit_income: 8_579 + # Sch VI para 3 (both carers): 20 pounds a week, capped at net earnings + # (claimant 1,000 - 100 tax - 100 half contributions = 800; partner + # 500 - 25 NI - 50 tax - 50 = 375; 1,175), so 1,040 is disregarded: + # 8,904 - 325 - 1,040. + pension_credit_income: 7_539 # Reg.9: remove the claimant's 100 JSA and partner's 200 ESA. - savings_credit_income: 8_279 + savings_credit_income: 7_239 # Sch.I para.4 and the 2026 carer addition: 2 * 48.15 * 52 = 5,007.60. carer_minimum_guarantee_addition: 5_007.60 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml index 2f09ea9468..1a042c7dba 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -248,3 +248,68 @@ # minimum guarantee 218.15 a week in 2024-25 (SI 2024/242) pension_credit_income: 9_000 + 2_000 - 5 * 52 guarantee_credit: 218.15 * 52 - (9_000 + 2_000 - 5 * 52) + +- name: A taxpayer's disregard is capped at net earnings, so earnings below the disregard leave Pension Credit income unchanged (reg 17(9), 17(10)). + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 3_000 + attendance_allowance: 3_000 + employment_income: 1_040 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # Income tax (16,040 - 12,570) x 20% = 694. Tax on the earnings is the + # lesser of 694 and 20% x 1,040 = 208, so net earnings are 832; the 20 + # pound (attendance allowance) disregard of 1,040 is capped at 832. + pension_credit_earnings_disregard: 832 + # 16,040 - 694 - 832, the same as with no earnings (next case). + pension_credit_income: 14_514 + +- name: The same claimant with no earnings has the same Pension Credit income. + period: 2024 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 3_000 + attendance_allowance: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + # 15,000 - (15,000 - 12,570) x 20% + pension_credit_earnings_disregard: 0 + pension_credit_income: 14_514 + +- name: A dependant's caring does not qualify a couple for the higher disregard (Sch I para 4 is the claimant's or partner's). + period: 2024 + input: + people: + claimant: + age: 70 + employment_income: 2_000 + partner: + age: 70 + child: + age: 17 + is_carer_for_benefits: true + benunits: + benunit: + members: [claimant, partner, child] + households: + household: + members: [claimant, partner, child] + output: + pension_credit_earnings_disregard: 10 * 52 diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml index 3c1b7febc4..b33aeac542 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_income.yaml @@ -20,9 +20,9 @@ savings: 11_000 output: pension_credit_deemed_income: 104 - # 1 + 2 + 4 + 8 + 32 + 64 + 128 + 104 = 343, less the Sch VI para 5 - # disregard: 5 pounds a week for a single claimant, capped at the 129 of - # earnings. + # 1 + 2 + 4 + 8 + 32 + 64 + 128 + 104 = 343, less the Sch VI disregard: + # the 20 pound para 4(1)(a)(vii) amount (contributory ESA), capped at the + # 129 of net earnings (no tax or NI on them). pension_credit_earnings_disregard: 129 pension_credit_income: 214 diff --git a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py index 2aaa136839..5c1e475c86 100644 --- a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py @@ -1,19 +1,24 @@ """Properties of the Pension Credit earnings disregards (SPC Regs 2002 Sch VI). -Invariants, over generated pension-age benefit units: - -1. Statute: the disregard equals min(earnings, 52 x w), where w is 20 if the - unit has a lone parent, a carer satisfying Sch I para 4, or a claimant or - partner on a listed disability benefit or certified blind, and otherwise 5 - for a single claimant and 10 for a couple. The expected value is computed - here from the inputs, not read from the model. -2. Bounds: 0 <= disregard <= min(earnings, 20 x 52). +Invariants, over generated benefit units (England, so the basic rate is 20%): + +1. Statute: the disregard equals min(net earnings, 52 x w). Net earnings are + the claimant's and partner's gross earnings less earnings NI, half of + pension contributions and income tax on the earnings (the lesser of their + income tax and 20% of their earnings), recomputed here from the inputs and + the model's tax and NI. w is 20 if the unit is a lone-parent family, or a + claimant or partner is a carer (an input here), receives a listed + disability benefit or is blind; otherwise 5 single, 10 couple. A dependent + child's DLA or caring never qualifies. +2. Bounds: 0 <= disregard <= min(gross earnings, 20 x 52). 3. Differential: Pension Credit income is the pre-change income (sources less - tax, NI and half of pension contributions, floored at 0, recomputed from - the model's own components) less the disregard, floored at 0. -4. Monotonicity: the guarantee credit never falls when the disregard applies, - i.e. it is at least the guarantee credit without it. -5. The dated benefit lists follow the amending instruments: ESA from 27 + tax, NI and half of pension contributions, floored at 0) less the + disregard, floored at 0. +4. Monotonicity: the guarantee credit is never below its value without the + disregard. +5. Earning more never raises Pension Credit entitlement (reg 17(9) applies the + disregard to net earnings). +6. The dated benefit lists follow the amending instruments: ESA from 27 October 2008, PIP and AFIP from 8 April 2013. """ @@ -43,16 +48,16 @@ @st.composite def units(draw): couple = draw(st.booleans()) - child = draw(st.booleans()) and not couple + child = draw(st.booleans()) people = {} names = ["claimant"] + (["partner"] if couple else []) for n in names: person = { "age": draw(st.sampled_from([67, 72, 85])), - "employment_income": draw(st.sampled_from([0, 50, 400, 3_000])), + "employment_income": draw(st.sampled_from([0, 50, 400, 1_040, 3_000])), "self_employment_income": draw(st.sampled_from([0, 0, 1_500])), "state_pension": draw(st.integers(min_value=0, max_value=12_000)), - "private_pension_income": draw(st.sampled_from([0, 2_000])), + "private_pension_income": draw(st.sampled_from([0, 2_000, 6_000])), "is_blind": draw(st.sampled_from([False, False, False, True])), "is_carer_for_benefits": draw(st.sampled_from([False, False, False, True])), } @@ -61,7 +66,12 @@ def units(draw): person[benefit] = 2_000 people[n] = person if child: - people["child"] = {"age": 10, "dla_sc": draw(st.sampled_from([0, 2_000]))} + # A dependent child's DLA or caring never qualifies the unit. + people["child"] = { + "age": 10, + "dla_sc": draw(st.sampled_from([0, 2_000])), + "is_carer_for_benefits": draw(st.booleans()), + } return people @@ -80,24 +90,51 @@ def simulate(people): ) +def unit_value(sim, variable): + return float(sim.calculate(variable, YEAR)[0]) + + +def person_values(sim, variable): + return np.asarray(sim.calculate(variable, YEAR, map_to="person"), dtype=float) + + @SETTINGS @given(units()) def test_statute_bounds_differential_and_monotonicity(people): sim = simulate(people) - calc = lambda v: float(sim.calculate(v, YEAR)[0]) - adults = [p for n, p in people.items() if n != "child"] - earnings = sum(p["employment_income"] + p["self_employment_income"] for p in adults) - couple = len(adults) == 2 + names = list(people) + adults = [n != "child" for n in names] + gross = np.array( + [ + people[n]["employment_income"] + people[n].get("self_employment_income", 0) + if n != "child" + else 0 + for n in names + ], + dtype=float, + ) + ni = sum( + person_values(sim, v) + for v in ["ni_class_1_employee", "ni_class_2", "ni_class_4"] + ) + tax_on_earnings = np.minimum(person_values(sim, "income_tax"), 0.2 * gross) + contributions = 0.5 * person_values(sim, "pension_contributions") + net = np.maximum(0, gross - ni - tax_on_earnings - contributions) + net_earnings = float(net[adults].sum()) + + adult_people = [people[n] for n in names if n != "child"] + couple = len(adult_people) == 2 disabled = any( - p.get("is_blind") or any(p.get(b, 0) > 0 for b in DISABILITY) for p in adults + p.get("is_blind") or any(p.get(b, 0) > 0 for b in DISABILITY) + for p in adult_people ) - carer = calc("carer_minimum_guarantee_addition") > 0 - lone_parent = "child" in people + carer = any(p.get("is_carer_for_benefits") for p in adult_people) + lone_parent = "child" in people and not couple weekly = 20 if (disabled or carer or lone_parent) else (10 if couple else 5) - expected = min(earnings, weekly * WEEKS_IN_YEAR) - disregard = calc("pension_credit_earnings_disregard") + expected = min(net_earnings, weekly * WEEKS_IN_YEAR) + disregard = unit_value(sim, "pension_credit_earnings_disregard") assert np.isclose(disregard, expected, atol=0.01) - assert 0 <= disregard <= min(earnings, 20 * WEEKS_IN_YEAR) + 0.01 + assert 0 <= disregard <= min(gross.sum(), 20 * WEEKS_IN_YEAR) + 0.01 sources = sim.tax_benefit_system.parameters( f"{YEAR}-06-01" @@ -110,10 +147,22 @@ def test_statute_bounds_differential_and_monotonicity(people): ) before = max(0.0, total - deductions) assert np.isclose( - calc("pension_credit_income"), max(0.0, before - disregard), atol=0.01 + unit_value(sim, "pension_credit_income"), + max(0.0, before - disregard), + atol=0.01, ) - gc_without = max(0.0, calc("minimum_guarantee") - before) - assert calc("guarantee_credit") >= gc_without - 0.01 + gc_without = max(0.0, unit_value(sim, "minimum_guarantee") - before) + assert unit_value(sim, "guarantee_credit") >= gc_without - 0.01 + + +@SETTINGS +@given(units(), st.sampled_from([1, 50, 260, 1_040, 5_000])) +def test_earning_more_never_raises_pension_credit(people, extra): + more = {name: dict(person) for name, person in people.items()} + more["claimant"]["employment_income"] += extra + before = unit_value(simulate(people), "pension_credit_entitlement") + after = unit_value(simulate(more), "pension_credit_entitlement") + assert after <= before + 0.01 def test_benefit_lists_follow_amending_instruments(): diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py index 5b93defa35..549bd43a6a 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py @@ -11,8 +11,14 @@ class pension_credit_earnings_disregard(Variable): "receives a listed disability benefit or is certified blind " "(para. 4(1)); otherwise 5 pounds a week for a single claimant and 10 " "pounds for a couple (para. 5). 20 pounds is the most disregarded " - "however many conditions are met (para. 4A), and the disregard never " - "exceeds the earnings. Not modelled: the employment-specific 20 pound " + "however many conditions are met (para. 4A). Reg. 17(9) disregards the " + "sums 'in calculating the claimant's earnings', which are net of income " + "tax and National Insurance (reg. 17(10)) and of half of pension " + "contributions (reg. 17A(4A)), so the disregard never exceeds the " + "claimant's and partner's net earnings. Income tax on earnings is taken " + "as the lesser of the person's income tax and the basic rate on their " + "earnings, the tax deducted where the personal allowance goes against " + "pension income first. Not modelled: the employment-specific 20 pound " "disregards of paras. 2 to 2B (part-time firefighters, auxiliary " "coastguards, lifeboat crew, reserve forces) and the transitional " "protections of para. 4(2) to (4)." @@ -24,12 +30,15 @@ class pension_credit_earnings_disregard(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2002/1792/schedule/VI", "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17", + "https://www.legislation.gov.uk/uksi/2002/1792/regulation/17A", ) def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.pension_credit.earnings_disregard + pc = parameters(period).gov.dwp.pension_credit + p = pc.earnings_disregard person = benunit.members - claimant_or_partner = person("is_uc_claimant", period) + claimant_or_partner = person("is_claimant_or_partner", period) + on_disability_benefit = ( add(person, period, p.higher.disability_benefits) > 0 ) | person("is_blind", period) @@ -43,7 +52,10 @@ def formula(benunit, period, parameters): benunit.any(claimant_or_partner & on_disability_benefit) | unit_on_disability_benefit ) - carer = benunit("carer_minimum_guarantee_addition", period) > 0 + # Sch. I para. 4: a claimant or partner entitled to carer's allowance. + carer = benunit.any( + claimant_or_partner & person("is_carer_for_benefits", period) + ) lone_parent = benunit("is_lone_parent", period) relation_type = benunit("relation_type", period) weekly = where( @@ -51,5 +63,21 @@ def formula(benunit, period, parameters): p.higher.amount, p.standard[relation_type], ) - earnings = max_(0, benunit("pension_credit_earnings", period)) - return min_(weekly * WEEKS_IN_YEAR, earnings) + + # Net earnings of the claimant and partner (regs. 17(10), 17A(4A)). + gross = max_(0, add(person, period, pc.guarantee_credit.earnings_sources)) + national_insurance = add( + person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] + ) + basic_rate = parameters(period).gov.hmrc.income_tax.rates.uk.rates[0] + income_tax_on_earnings = min_(person("income_tax", period), basic_rate * gross) + pension_contributions = ( + person("pension_contributions", period) + * pc.income.pension_contributions_deduction + ) + net = max_( + 0, + gross - national_insurance - income_tax_on_earnings - pension_contributions, + ) + net_earnings = benunit.sum(net * claimant_or_partner) + return min_(weekly * WEEKS_IN_YEAR, net_earnings) From 816a4e1ea156f138b054364ce3822e07a3d27a9d Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 4 Oct 2026 06:34:20 -0400 Subject: [PATCH 3/3] Tax earnings as the top slice on the person's schedule; carer entitlement (review of #2019) - Income tax on earnings is now the tax on them as the top slice of taxable non-savings income on the person's own Scottish or rest-of-UK schedule, capped at their income tax. The 20% cap let earnings crossing Scottish bands raise Pension Credit (reviewer's couple: +2.89). - The para 3 carer condition reads is_entitled_to_carer_benefit, the carer addition's own Sch I para 4 test; caring hours alone no longer qualify (reviewer's case overpaid 780 a year). - Tests: Scottish couple pair (income flat with and without earnings), hours-only vs entitled carer cases; the property test adds Scottish households, carer entitlement, care hours and pension contributions, and measures tax on earnings independently by rerunning without them. Co-Authored-By: Claude Opus 5.5 --- .../pension_credit_earnings_disregard.yaml | 96 ++++++++++++++++++- ...on_credit_earnings_disregard_properties.py | 62 ++++++++---- .../pension_credit_earnings_disregard.py | 30 ++++-- 3 files changed, 160 insertions(+), 28 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml index 1a042c7dba..709c69578e 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/pension_credit_earnings_disregard.yaml @@ -78,7 +78,7 @@ claimant: age: 70 employment_income: 2000 - is_carer_for_benefits: true + is_entitled_to_carer_benefit: true benunits: benunit: members: [claimant] @@ -304,7 +304,7 @@ age: 70 child: age: 17 - is_carer_for_benefits: true + is_entitled_to_carer_benefit: true benunits: benunit: members: [claimant, partner, child] @@ -313,3 +313,95 @@ members: [claimant, partner, child] output: pension_credit_earnings_disregard: 10 * 52 + +- name: A Scottish taxpayer's tax on earnings is their top slice on the Scottish schedule, so Pension Credit income does not fall with earnings. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 16_000 + attendance_allowance: 5_000 + employment_income: 1_040 + partner: + age: 70 + attendance_allowance: 5_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: SCOTLAND + output: + # The 1,040 of earnings add 210.89 of income tax (they cross into the + # Scottish intermediate band), so net earnings are 829.11 and the 20 pound + # disregard of 1,040 is capped there; income is as with no earnings (next). + income_tax: [3_016.62, 0] + pension_credit_earnings_disregard: 829.11 + pension_credit_income: 25_194.27 + +- name: The same Scottish couple with no earnings has the same Pension Credit income. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + state_pension: 12_000 + private_pension_income: 16_000 + attendance_allowance: 5_000 + partner: + age: 70 + attendance_allowance: 5_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + country: SCOTLAND + output: + income_tax: [2_805.73, 0] + pension_credit_earnings_disregard: 0 + pension_credit_income: 25_194.27 + +- name: Caring hours without entitlement to carer's allowance do not qualify for the 20 pound disregard (Sch VI para 3 needs Sch I para 4). + period: 2025 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + employment_income: 2_000 + care_hours: 40 + is_entitled_to_carer_benefit: false + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 5 * 52 + +- name: Entitlement to carer's allowance qualifies for the 20 pound disregard. + period: 2025 + input: + people: + claimant: + age: 70 + state_pension: 9_000 + employment_income: 2_000 + care_hours: 40 + is_entitled_to_carer_benefit: true + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + pension_credit_earnings_disregard: 20 * 52 diff --git a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py index 5c1e475c86..c60d3de0f2 100644 --- a/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_pension_credit_earnings_disregard_properties.py @@ -1,13 +1,15 @@ """Properties of the Pension Credit earnings disregards (SPC Regs 2002 Sch VI). -Invariants, over generated benefit units (England, so the basic rate is 20%): +Invariants, over generated benefit units in England and Scotland: 1. Statute: the disregard equals min(net earnings, 52 x w). Net earnings are the claimant's and partner's gross earnings less earnings NI, half of - pension contributions and income tax on the earnings (the lesser of their - income tax and 20% of their earnings), recomputed here from the inputs and - the model's tax and NI. w is 20 if the unit is a lone-parent family, or a - claimant or partner is a carer (an input here), receives a listed + pension contributions and income tax on the earnings. The tax on a + person's earnings is measured independently, as the fall in their income + tax when the simulation is rerun without their earnings (the earnings as + the top slice, on their own Scottish or rest-of-UK schedule). w is 20 if + the unit is a lone-parent family, or a claimant or partner is entitled to + carer's allowance (an input here), receives a listed disability benefit or is blind; otherwise 5 single, 10 couple. A dependent child's DLA or caring never qualifies. 2. Bounds: 0 <= disregard <= min(gross earnings, 20 x 52). @@ -47,6 +49,7 @@ @st.composite def units(draw): + country = draw(st.sampled_from(["ENGLAND", "SCOTLAND"])) couple = draw(st.booleans()) child = draw(st.booleans()) people = {} @@ -59,7 +62,11 @@ def units(draw): "state_pension": draw(st.integers(min_value=0, max_value=12_000)), "private_pension_income": draw(st.sampled_from([0, 2_000, 6_000])), "is_blind": draw(st.sampled_from([False, False, False, True])), - "is_carer_for_benefits": draw(st.sampled_from([False, False, False, True])), + "is_entitled_to_carer_benefit": draw( + st.sampled_from([False, False, False, True]) + ), + "care_hours": draw(st.sampled_from([0, 0, 40])), + "personal_pension_contributions": draw(st.sampled_from([0, 0, 300])), } benefit = draw(st.sampled_from([None, None] + DISABILITY)) if benefit: @@ -70,12 +77,13 @@ def units(draw): people["child"] = { "age": 10, "dla_sc": draw(st.sampled_from([0, 2_000])), - "is_carer_for_benefits": draw(st.booleans()), + "is_entitled_to_carer_benefit": draw(st.booleans()), } - return people + return {"people": people, "country": country} -def simulate(people): +def simulate(case): + people = case["people"] members = list(people) dated = { name: {k: {str(YEAR): v} for k, v in person.items()} @@ -85,11 +93,20 @@ def simulate(people): situation={ "people": dated, "benunits": {"b": {"members": members}}, - "households": {"h": {"members": members}}, + "households": { + "h": {"members": members, "country": {str(YEAR): case["country"]}} + }, } ) +def without_earnings(case, name): + people = {n: dict(p) for n, p in case["people"].items()} + people[name]["employment_income"] = 0 + people[name]["self_employment_income"] = 0 + return {"people": people, "country": case["country"]} + + def unit_value(sim, variable): return float(sim.calculate(variable, YEAR)[0]) @@ -100,8 +117,9 @@ def person_values(sim, variable): @SETTINGS @given(units()) -def test_statute_bounds_differential_and_monotonicity(people): - sim = simulate(people) +def test_statute_bounds_differential_and_monotonicity(case): + people = case["people"] + sim = simulate(case) names = list(people) adults = [n != "child" for n in names] gross = np.array( @@ -117,7 +135,12 @@ def test_statute_bounds_differential_and_monotonicity(people): person_values(sim, v) for v in ["ni_class_1_employee", "ni_class_2", "ni_class_4"] ) - tax_on_earnings = np.minimum(person_values(sim, "income_tax"), 0.2 * gross) + income_tax = person_values(sim, "income_tax") + tax_on_earnings = np.zeros(len(names)) + for i, name in enumerate(names): + if gross[i] > 0: + rerun = person_values(simulate(without_earnings(case, name)), "income_tax") + tax_on_earnings[i] = max(0.0, income_tax[i] - rerun[i]) contributions = 0.5 * person_values(sim, "pension_contributions") net = np.maximum(0, gross - ni - tax_on_earnings - contributions) net_earnings = float(net[adults].sum()) @@ -128,7 +151,7 @@ def test_statute_bounds_differential_and_monotonicity(people): p.get("is_blind") or any(p.get(b, 0) > 0 for b in DISABILITY) for p in adult_people ) - carer = any(p.get("is_carer_for_benefits") for p in adult_people) + carer = any(p.get("is_entitled_to_carer_benefit") for p in adult_people) lone_parent = "child" in people and not couple weekly = 20 if (disabled or carer or lone_parent) else (10 if couple else 5) expected = min(net_earnings, weekly * WEEKS_IN_YEAR) @@ -157,11 +180,14 @@ def test_statute_bounds_differential_and_monotonicity(people): @SETTINGS @given(units(), st.sampled_from([1, 50, 260, 1_040, 5_000])) -def test_earning_more_never_raises_pension_credit(people, extra): - more = {name: dict(person) for name, person in people.items()} +def test_earning_more_never_raises_pension_credit(case, extra): + more = {name: dict(person) for name, person in case["people"].items()} more["claimant"]["employment_income"] += extra - before = unit_value(simulate(people), "pension_credit_entitlement") - after = unit_value(simulate(more), "pension_credit_entitlement") + before = unit_value(simulate(case), "pension_credit_entitlement") + after = unit_value( + simulate({"people": more, "country": case["country"]}), + "pension_credit_entitlement", + ) assert after <= before + 0.01 diff --git a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py index 549bd43a6a..2e39753ac5 100644 --- a/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py +++ b/policyengine_uk/variables/gov/dwp/pension_credit/pension_credit_earnings_disregard.py @@ -15,10 +15,13 @@ class pension_credit_earnings_disregard(Variable): "sums 'in calculating the claimant's earnings', which are net of income " "tax and National Insurance (reg. 17(10)) and of half of pension " "contributions (reg. 17A(4A)), so the disregard never exceeds the " - "claimant's and partner's net earnings. Income tax on earnings is taken " - "as the lesser of the person's income tax and the basic rate on their " - "earnings, the tax deducted where the personal allowance goes against " - "pension income first. Not modelled: the employment-specific 20 pound " + "claimant's and partner's net earnings. Income tax on earnings is the " + "tax on them as the top slice of the person's taxable non-savings " + "income, on the person's own rate schedule (Scottish or rest of UK), " + "capped at their income tax: the tax deducted from the earnings where " + "the personal allowance goes against pension income first (DMG " + "86038-86039), so earning more never raises Pension Credit. Not " + "modelled: the employment-specific 20 pound " "disregards of paras. 2 to 2B (part-time firefighters, auxiliary " "coastguards, lifeboat crew, reserve forces) and the transitional " "protections of para. 4(2) to (4)." @@ -52,9 +55,10 @@ def formula(benunit, period, parameters): benunit.any(claimant_or_partner & on_disability_benefit) | unit_on_disability_benefit ) - # Sch. I para. 4: a claimant or partner entitled to carer's allowance. + # Sch. I para. 4: a claimant or partner entitled to carer's allowance + # (the carer addition's own test), not caring hours alone. carer = benunit.any( - claimant_or_partner & person("is_carer_for_benefits", period) + claimant_or_partner & person("is_entitled_to_carer_benefit", period) ) lone_parent = benunit("is_lone_parent", period) relation_type = benunit("relation_type", period) @@ -69,8 +73,18 @@ def formula(benunit, period, parameters): national_insurance = add( person, period, ["ni_class_1_employee", "ni_class_2", "ni_class_4"] ) - basic_rate = parameters(period).gov.hmrc.income_tax.rates.uk.rates[0] - income_tax_on_earnings = min_(person("income_tax", period), basic_rate * gross) + # Tax on the earnings as the top slice of taxable non-savings income. + rates = parameters(period).gov.hmrc.income_tax.rates + taxable = person("earned_taxable_income", period) + below_earnings = max_(0, taxable - min_(gross, taxable)) + scottish = person("pays_scottish_income_tax", period) + top_slice_tax = where( + scottish, + rates.scotland.rates.calc(taxable) + - rates.scotland.rates.calc(below_earnings), + rates.uk.calc(taxable) - rates.uk.calc(below_earnings), + ) + income_tax_on_earnings = min_(person("income_tax", period), top_slice_tax) pension_contributions = ( person("pension_contributions", period) * pc.income.pension_contributions_deduction