diff --git a/changelog.d/1970.fixed.md b/changelog.d/1970.fixed.md new file mode 100644 index 0000000000..eadc51f308 --- /dev/null +++ b/changelog.d/1970.fixed.md @@ -0,0 +1 @@ +- Separated Universal Credit's higher disabled child condition from tax credits, included certified blindness, and prevented the lower and higher additions from stacking. diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/benefit_amount_tolerance.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/benefit_amount_tolerance.yaml new file mode 100644 index 0000000000..8821f5f5e4 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/elements/child/severely_disabled/benefit_amount_tolerance.yaml @@ -0,0 +1,10 @@ +description: Weekly tolerance for the modeled paid-amount proxy for qualifying DLA care and PIP daily living awards in the Universal Credit higher disabled child condition. This is an existing input approximation, not a statutory entitlement threshold. +values: + 2015-01-01: 10 +metadata: + unit: currency-GBP + period: week + label: UC higher disabled child benefit amount proxy tolerance + reference: + - title: Retained PolicyEngine-UK DLA and PIP paid-amount input convention + href: https://github.com/PolicyEngine/policyengine-uk/blob/b4dbaf57a4ebb1d1f0ba5f7a384795422828db75/policyengine_uk/variables/household/demographic/is_severely_disabled_for_benefits.py diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/uc_individual_severely_disabled_child_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/uc_individual_severely_disabled_child_element.yaml index bd55e902ca..dac7d76bc1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/uc_individual_severely_disabled_child_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/child_element/disability/uc_individual_severely_disabled_child_element.yaml @@ -4,7 +4,7 @@ input: age: 6 is_uc_claimant: false - is_severely_disabled_for_benefits: true + dla_sc_category: HIGHER output: uc_individual_severely_disabled_child_element: 5_942 @@ -14,7 +14,7 @@ input: age: 18 is_in_non_advanced_education: true - is_severely_disabled_for_benefits: true + dla_sc_category: HIGHER output: is_uc_claimant: true uc_individual_severely_disabled_child_element: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml index 6ed4fd7ae8..d41cde1a66 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/claimant_child_element.yaml @@ -115,7 +115,7 @@ is_disabled_for_benefits: true person5: age: 2 - is_severely_disabled_for_benefits: true + dla_sc_category: HIGHER benunits: benunit: members: [person1, person2, person3, person4, person5] diff --git a/policyengine_uk/tests/policy/integration/uc_disabled_child_addition.yaml b/policyengine_uk/tests/policy/integration/uc_disabled_child_addition.yaml new file mode 100644 index 0000000000..c1318369e7 --- /dev/null +++ b/policyengine_uk/tests/policy/integration/uc_disabled_child_addition.yaml @@ -0,0 +1,202 @@ +# Classification: UC Regs 2013 reg 24(2), with the existing statutory-blind +# input. Cash anchors are the unchanged model's 2026 monthly amounts: +# standard 424.90, ordinary child 303.94, lower 164.79, higher 514.71. +# A parent with a post-April-2017 child and no recovered debt therefore gets +# 12 * (424.90 + 303.94 + the single applicable disability addition). +# Only blind/no-award cases assert final UC: the existing generic disability +# flag also defaults LCWRA for child members, a separate model approximation. + +- name: A certified blind dependent receives the higher UC child addition without a disability benefit + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_parent: true + is_benunit_head: true + child: + age: 8 + is_benunit_head: false + is_blind: true + benunits: + family: + members: [parent, child] + uc_has_deduction: false + households: + home: + members: [parent, child] + region: LONDON + output: + uc_individual_disabled_child_element: [0, 0] + uc_individual_severely_disabled_child_element: [0, 6_176.52] + uc_disability_elements: 6_176.52 + universal_credit: 14_922.60 + +- name: Blindness and highest DLA care give one higher addition rather than both tiers + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_parent: true + is_benunit_head: true + child: + age: 8 + is_benunit_head: false + is_blind: true + dla_sc_category: HIGHER + is_disabled_for_benefits: true + benunits: + family: + members: [parent, child] + uc_has_deduction: false + households: + home: + members: [parent, child] + region: LONDON + output: + uc_individual_disabled_child_element: [0, 0] + uc_individual_severely_disabled_child_element: [0, 6_176.52] + +- name: Middle DLA care retains only the lower disabled child addition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_parent: true + is_benunit_head: true + child: + age: 8 + is_benunit_head: false + dla_sc_category: MIDDLE + is_disabled_for_benefits: true + benunits: + family: + members: [parent, child] + uc_has_deduction: false + households: + home: + members: [parent, child] + region: LONDON + output: + uc_individual_disabled_child_element: [0, 1_977.48] + uc_individual_severely_disabled_child_element: [0, 0] + +- name: A nonblind child without a qualifying award receives no disability addition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_parent: true + is_benunit_head: true + child: + age: 8 + is_benunit_head: false + benunits: + family: + members: [parent, child] + uc_has_deduction: false + households: + home: + members: [parent, child] + region: LONDON + output: + uc_individual_disabled_child_element: [0, 0] + uc_individual_severely_disabled_child_element: [0, 0] + universal_credit: 8_746.08 + +- name: An enhanced PIP daily living qualifying young person receives only the higher addition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 45 + is_parent: true + is_benunit_head: true + young_person: + age: 18 + is_benunit_head: false + is_in_non_advanced_education: true + age_started_or_accepted_current_education_or_training: 17 + pip_dl_category: ENHANCED + is_disabled_for_benefits: true + benunits: + family: + members: [parent, young_person] + households: + home: + members: [parent, young_person] + output: + is_uc_claimant: [true, false] + uc_individual_disabled_child_element: [0, 0] + uc_individual_severely_disabled_child_element: [0, 6_176.52] + +- name: A blind young claimant does not receive a child addition for themselves + period: 2026 + absolute_error_margin: 0.01 + input: + age: 18 + is_in_non_advanced_education: true + is_blind: true + is_disabled_for_benefits: true + output: + is_uc_claimant: true + uc_individual_disabled_child_element: 0 + uc_individual_severely_disabled_child_element: 0 + +- name: A blind outside adult does not create a child addition for another family + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_parent: true + is_benunit_head: true + child: + age: 8 + is_benunit_head: false + outside_adult: + age: 30 + is_blind: true + benunits: + family: + members: [parent, child] + other_family: + members: [outside_adult] + households: + home: + members: [parent, child, outside_adult] + output: + uc_individual_disabled_child_element: [0, 0, 0] + uc_individual_severely_disabled_child_element: [0, 0, 0] + +- name: A stored tax credit severe flag alone is not evidence of the UC higher condition + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 35 + is_parent: true + is_benunit_head: true + child: + age: 8 + is_benunit_head: false + is_severely_disabled_for_benefits: true + benunits: + family: + members: [parent, child] + households: + home: + members: [parent, child] + output: + uc_individual_disabled_child_element: [0, 0] + uc_individual_severely_disabled_child_element: [0, 0] diff --git a/policyengine_uk/tests/test_uc_disabled_child_addition_properties.py b/policyengine_uk/tests/test_uc_disabled_child_addition_properties.py new file mode 100644 index 0000000000..86c654f768 --- /dev/null +++ b/policyengine_uk/tests/test_uc_disabled_child_addition_properties.py @@ -0,0 +1,164 @@ +"""UC reg 24(2): one disabled-child tier, with an independent blind limb. + +The exact award categories provide the entitlement classification oracle. +The amount-only checks separately preserve the existing, nonstatutory paid- +amount proxy; they do not establish entitlement from part-year payments. +Generated reported categories test the conditional input contract, not the +underlying age or overlapping-award eligibility of each generated combination. +2026 cash anchors are the unchanged monthly amounts 164.79 and 514.71. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st +from policyengine_core.reforms import Reform + +from policyengine_uk import Simulation +from policyengine_uk.model_api import WEEKS_IN_YEAR + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") +YEAR = 2026 +LOWER = 1_977.48 +HIGHER = 6_176.52 + + +def family(child, reverse=False): + people = { + "parent": { + "age": {YEAR: 35}, + "is_parent": {YEAR: True}, + "is_benunit_head": {YEAR: True}, + }, + "child": { + "age": {YEAR: 8}, + "is_benunit_head": {YEAR: False}, + **{name: {YEAR: value} for name, value in child.items()}, + }, + } + names = list(people) + if reverse: + people = dict(reversed(list(people.items()))) + names.reverse() + return { + "people": people, + "benunits": {"family": {"members": names}}, + "households": {"home": {"members": names}}, + } + + +def child_additions(sim): + ids = list(sim.populations["person"].ids) + child = ids.index("child") + return ( + sim.calculate("uc_individual_disabled_child_element", YEAR)[child], + sim.calculate("uc_individual_severely_disabled_child_element", YEAR)[child], + ) + + +@settings( + max_examples=12, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow], +) +@given( + age=st.integers(0, 15), + dla=st.sampled_from(["NONE", "LOWER", "MIDDLE", "HIGHER"]), + pip=st.sampled_from(["NONE", "STANDARD", "ENHANCED"]), + blind=st.booleans(), + lower_flag=st.booleans(), + reverse=st.booleans(), +) +def test_exact_award_categories_and_blindness_select_one_tier( + age, dla, pip, blind, lower_flag, reverse +): + sim = Simulation( + situation=family( + { + "age": age, + "dla_sc_category": dla, + "pip_dl_category": pip, + "is_blind": blind, + "is_disabled_for_benefits": lower_flag, + }, + reverse=reverse, + ) + ) + higher = blind or dla == "HIGHER" or pip == "ENHANCED" + expected = (LOWER if lower_flag and not higher else 0, HIGHER if higher else 0) + actual = child_additions(sim) + np.testing.assert_allclose(actual, expected, atol=0.01, rtol=0) + assert not (actual[0] > 0 and actual[1] > 0) + # Claimant roles remain excluded, even when entity/member order changes. + parent = list(sim.populations["person"].ids).index("parent") + for variable in ( + "uc_individual_disabled_child_element", + "uc_individual_severely_disabled_child_element", + ): + assert sim.calculate(variable, YEAR)[parent] == 0 + + +@pytest.mark.parametrize( + "awards, higher", + [ + ({"dla_sc": (114.6 - 10) * WEEKS_IN_YEAR - 1}, False), + ({"dla_sc": (114.6 - 10) * WEEKS_IN_YEAR}, True), + ({"pip_dl": (114.6 - 10) * WEEKS_IN_YEAR + 1}, True), + ({"dla_sc_category": "HIGHER", "dla_sc": 0}, False), + ({"pip_dl_category": "ENHANCED", "pip_dl": 0}, False), + ], + ids=[ + "DLA-below-proxy", + "DLA-at-proxy", + "PIP-above-proxy", + "DLA-direct-zero", + "PIP-direct-zero", + ], +) +def test_existing_direct_amount_proxy_and_override_precedence(awards, higher): + # Weekly 114.60 is the existing 2026 high/enhanced rate; 10 is the + # retained modeling tolerance, not a statutory entitlement threshold. + sim = Simulation(situation=family({**awards, "is_disabled_for_benefits": True})) + np.testing.assert_allclose( + child_additions(sim), + (0, HIGHER) if higher else (LOWER, 0), + atol=0.01, + rtol=0, + ) + + +def test_zero_higher_rate_reform_does_not_pay_the_lower_tier_instead(): + reform = Reform.from_dict( + { + "gov.dwp.universal_credit.elements.child.severely_disabled.amount": { + "2026-01-01.2026-12-31": 0 + } + }, + country_id="uk", + ) + sim = Simulation( + situation=family( + {"dla_sc_category": "HIGHER", "is_disabled_for_benefits": True} + ), + reform=reform, + ) + np.testing.assert_array_equal(child_additions(sim), [0, 0]) + + +def test_amount_proxy_tolerance_is_configurable_without_changing_award_rates(): + reform = Reform.from_dict( + { + "gov.dwp.universal_credit.elements.child.severely_disabled.benefit_amount_tolerance": { + "2026-01-01.2026-12-31": 0 + } + }, + country_id="uk", + ) + sim = Simulation( + situation=family( + {"dla_sc": (114.6 - 10) * WEEKS_IN_YEAR, "is_disabled_for_benefits": True} + ), + reform=reform, + ) + np.testing.assert_allclose(child_additions(sim), [LOWER, 0], atol=0.01, rtol=0) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/meets_uc_higher_disabled_child_condition.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/meets_uc_higher_disabled_child_condition.py new file mode 100644 index 0000000000..beaee1644c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/meets_uc_higher_disabled_child_condition.py @@ -0,0 +1,41 @@ +from policyengine_uk.model_api import * + + +class meets_uc_higher_disabled_child_condition(Variable): + value_type = bool + entity = Person + label = "Meets the Universal Credit higher disabled child condition" + definition_period = YEAR + documentation = ( + "The higher UC disabled child condition has an independent certified " + "blindness limb and qualifying DLA care or PIP daily living awards. " + "The model reads DLA and PIP amounts, computed from their categories " + "unless an amount is supplied directly. It retains the existing " + "nonstatutory weekly amount tolerance for those paid-amount proxies; " + "they do not establish underlying entitlement from partial payments. " + "Separate Scottish awards and benefit payable but for a hospital " + "stay are not represented. The child amount separately checks " + "dependent child or qualifying young person status and excludes " + "the UC claimant. The shared tax-credit severe disability flag, " + "attendance allowance and AFIP do not establish this UC condition." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/24", + "https://www.legislation.gov.uk/nisr/2016/216/regulation/25", + ) + + def formula(person, period, parameters): + dwp = parameters(period).gov.dwp + tolerance = ( + dwp.universal_credit.elements.child.severely_disabled.benefit_amount_tolerance + * WEEKS_IN_YEAR + ) + highest_dla_care = ( + person("dla_sc", period) + >= dwp.dla.self_care.higher * WEEKS_IN_YEAR - tolerance + ) + enhanced_pip_daily_living = ( + person("pip_dl", period) + >= dwp.pip.daily_living.enhanced * WEEKS_IN_YEAR - tolerance + ) + return person("is_blind", period) | highest_dla_care | enhanced_pip_daily_living diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/uc_individual_severely_disabled_child_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/uc_individual_severely_disabled_child_element.py index 189e875499..33b5412777 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/uc_individual_severely_disabled_child_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/severe_disability/uc_individual_severely_disabled_child_element.py @@ -7,7 +7,7 @@ class uc_individual_severely_disabled_child_element(Variable): label = "Universal Credit severely disabled child element" definition_period = YEAR unit = GBP - defined_for = "is_severely_disabled_for_benefits" + defined_for = "meets_uc_higher_disabled_child_condition" reference = "https://www.legislation.gov.uk/uksi/2013/376/regulation/24/2" def formula(person, period, parameters): diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/uc_individual_disabled_child_element.py b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/uc_individual_disabled_child_element.py index 578b8a388f..0ad1ebdc85 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/uc_individual_disabled_child_element.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/child_element/disability/uc_individual_disabled_child_element.py @@ -15,4 +15,5 @@ def formula(person, period, parameters): child = person( "is_child_or_qualifying_young_person_for_universal_credit", period ) & ~person("is_uc_claimant", period) - return (child * p.amount) * MONTHS_IN_YEAR + higher_condition = person("meets_uc_higher_disabled_child_condition", period) + return (child * ~higher_condition * p.amount) * MONTHS_IN_YEAR diff --git a/policyengine_uk/variables/household/demographic/is_severely_disabled_for_benefits.py b/policyengine_uk/variables/household/demographic/is_severely_disabled_for_benefits.py index 2a2ca1b08d..241d772e78 100644 --- a/policyengine_uk/variables/household/demographic/is_severely_disabled_for_benefits.py +++ b/policyengine_uk/variables/household/demographic/is_severely_disabled_for_benefits.py @@ -30,10 +30,10 @@ class is_severely_disabled_for_benefits(Variable): "use the stored value instead of this formula " "(policyengine-uk-data#494 aligns its definition with this one). " "The legacy severe disability premium has its own, wider list " - "(receives_severe_disability_premium_qualifying_benefit). The flag " - "also gates the Universal Credit higher disabled child addition, " - "whose condition (UC Regs 2013 reg 24(2)(b)) adds blindness and has " - "no armed forces independence payment limb; that is not yet separated." + "(receives_severe_disability_premium_qualifying_benefit). Universal " + "Credit uses its separate higher disabled child condition " + "(meets_uc_higher_disabled_child_condition), which includes certified " + "blindness and excludes attendance allowance and AFIP." ) definition_period = YEAR reference = (