diff --git a/changelog.d/afcs-severe-disability.changed.md b/changelog.d/afcs-severe-disability.changed.md new file mode 100644 index 000000000..0c9753835 --- /dev/null +++ b/changelog.d/afcs-severe-disability.changed.md @@ -0,0 +1 @@ +- Documented that `afcs_reported` is FRS benefit code 8 (Armed Forces Compensation Scheme payments and war disablement pensions, not armed forces independence payment) and tested that AFCS payments never confer severe disability. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/is_severely_disabled_for_benefits.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/is_severely_disabled_for_benefits.yaml index 2934198a3..a8c8f2751 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/is_severely_disabled_for_benefits.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/is_severely_disabled_for_benefits.yaml @@ -109,3 +109,123 @@ attendance_allowance: 5_439.19 output: is_severely_disabled_for_benefits: false +# Armed Forces Compensation Scheme payments other than armed forces +# independence payment (lump sums, supplementary awards, guaranteed income +# payments; AFCS Order 2011 art 15(1)) and war disablement pensions are in +# afcs. They meet none of the severe disability paragraphs (CTC reg 8(3)-(6); +# WTC reg 17(2)-(6)), alone or next to an award that is not severe. A DLA care +# award at the middle rate or a PIP daily living award at the standard rate +# still makes a child disabled under CTC reg 8(2), but not severely disabled. + +- name: DLA care component at the middle rate with an AFCS payment is not severe + period: 2025 + input: + age: 10 + dla_sc_category: MIDDLE + afcs_reported: 3_000 + output: + is_severely_disabled_for_benefits: false + +- name: PIP daily living component at the standard rate with an AFCS payment is not severe + period: 2025 + input: + age: 17 + pip_dl_category: STANDARD + afcs_reported: 3_000 + output: + is_severely_disabled_for_benefits: false + +- name: A child with an AFCS payment adds no CTC severely disabled child element + period: 2024 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + child: + age: 8 + afcs_reported: 2_000 + benunits: + benunit: + members: [parent, child] + is_CTC_eligible: true + output: + CTC_severely_disabled_child_element: 0 + +- name: An AFCS payment adds no WTC severe disability element + period: 2024 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 45 + afcs_reported: 12_000 + benunits: + benunit: + members: [adult] + is_WTC_eligible: true + output: + WTC_severely_disabled_element: 0 + +- name: WTC severe disability element for a claimant on armed forces independence payment + period: 2024 + absolute_error_margin: 0.01 + input: + people: + adult: + age: 45 + # AFCS Order 2011 art 24A(3), as amended by SI 2024/347 art 4: 184.30 + # a week in 2024-25; 184.30 * 52 = 9,583.60. + armed_forces_independence_payment: 9_583.60 + benunits: + benunit: + members: [adult] + is_WTC_eligible: true + output: + # WTC Regs 2002 reg 17(4) and Sch 2 para 6: 1,705 a year in 2024-25. + WTC_severely_disabled_element: 1_705 + +# UC Regs 2013 reg 24(2)(b) gives the higher rate of the disabled child +# addition for the DLA care component at the highest rate, the PIP daily living +# component at the enhanced rate (or a Scottish equivalent) or blindness; no +# armed forces payment is listed. Higher rate 2025-26: 495.87 a month (reg 36 +# table); 495.87 * 12 = 5,950.44. + +- name: A qualifying young person with an AFCS payment gets no UC higher disabled child addition + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + is_in_non_advanced_education: true + afcs_reported: 5_000 + benunits: + benunit: + members: [parent, young_person] + output: + is_severely_disabled_for_benefits: [false, false] + uc_individual_severely_disabled_child_element: [0, 0] + +- name: A qualifying young person on the enhanced PIP daily living rate gets the UC higher addition + period: 2025 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + young_person: + age: 17 + is_in_non_advanced_education: true + pip_dl_category: ENHANCED + benunits: + benunit: + members: [parent, young_person] + output: + is_severely_disabled_for_benefits: [false, true] + uc_individual_severely_disabled_child_element: [0, 5_950.44] diff --git a/policyengine_uk/tests/test_severe_disability_afcs_properties.py b/policyengine_uk/tests/test_severe_disability_afcs_properties.py new file mode 100644 index 000000000..0e6b2b32a --- /dev/null +++ b/policyengine_uk/tests/test_severe_disability_afcs_properties.py @@ -0,0 +1,183 @@ +"""Property-based tests for the severe disability test and Armed Forces +Compensation Scheme payments. + +Law: +- Child Tax Credit Regulations 2002 (SI 2002/2007) reg 8(3)-(5): a person is + severely disabled if the DLA care component is payable at the highest rate, + the PIP daily living component at the enhanced rate, or "an armed forces + independence payment is payable in respect of him". +- Working Tax Credit (Entitlement and Maximum Rate) Regulations 2002 reg + 17(2)-(4): the same, plus Attendance Allowance at the higher rate. +- Armed forces independence payment is one of the injury benefits in the Armed + Forces and Reserve Forces (Compensation Scheme) Order 2011 art 15(1), next to + lump sums, supplementary awards and guaranteed income payments. Only it is + named; the afcs variables hold the other payments. + +Invariants, for any generated population of families: + +1. Differential: is_severely_disabled_for_benefits equals the tax credit + condition (CTC reg 8(3)-(5) with WTC reg 17(2)'s higher-rate Attendance + Allowance) recomputed here from the DLA care, PIP daily living and + Attendance Allowance categories and armed forces independence payment. +2. Metamorphic: changing every person's AFCS payment (to zero or another + amount) changes neither the severe disability test nor the amounts that read + it: the UC higher rate disabled child addition, the CTC severely disabled + child element and the WTC severe disability element. The legacy severe + disability premium, which reads its own qualifying-benefit list + (receives_severe_disability_premium_qualifying_benefit) rather than this + flag, is checked the same way as an independent case: its list also names + armed forces independence payment and no other AFCS payment. +3. Monotone: adding armed forces independence payment to a person never turns + the severe disability test off, and turns it on. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +UC_YEAR = 2025 +TAX_CREDIT_YEAR = 2024 +YEARS = (TAX_CREDIT_YEAR, UC_YEAR) +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +DLA_CARE = ["NONE", "LOWER", "MIDDLE", "HIGHER"] +ATTENDANCE_ALLOWANCE = ["NONE", "LOWER", "HIGHER"] +PIP_DAILY_LIVING = ["NONE", "STANDARD", "ENHANCED"] +amounts = st.one_of(st.just(0.0), st.floats(1, 40_000)) + + +@st.composite +def people(draw, ages): + return dict( + age=draw(ages), + dla_sc_category=draw(st.sampled_from(DLA_CARE)), + pip_dl_category=draw(st.sampled_from(PIP_DAILY_LIVING)), + aa_category=draw(st.sampled_from(ATTENDANCE_ALLOWANCE)), + afcs_reported=draw(amounts), + armed_forces_independence_payment=draw( + st.one_of(st.just(0.0), st.just(0.0), st.floats(1, 15_000)) + ), + ) + + +@st.composite +def families(draw): + return dict( + adults=draw(st.lists(people(st.integers(25, 60)), min_size=1, max_size=2)), + # 16 to 18 year olds are in non-advanced education, so qualifying young + # persons for both UC and CTC; younger ones are children. A 19-year-old + # would also need their course to have started before 19, so none is + # drawn. + dependants=draw(st.lists(people(st.integers(0, 18)), max_size=2)), + ) + + +populations = st.lists(families(), min_size=10, max_size=20) + + +def situation(units, afcs=None, afip=None): + """Build a situation; afcs/afip map a person name to a replacement.""" + people_, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + names = [] + for role, members in (("a", unit["adults"]), ("d", unit["dependants"])): + for j, person in enumerate(members): + name = f"{role}{i}_{j}" + values = dict(person) + if afcs is not None: + values["afcs_reported"] = afcs(name, values["afcs_reported"]) + if afip is not None: + values["armed_forces_independence_payment"] = afip( + name, values["armed_forces_independence_payment"] + ) + if role == "a": + values["is_parent"] = bool(unit["dependants"]) + elif values["age"] >= 16: + values["is_in_non_advanced_education"] = True + people_[name] = {k: {y: v for y in YEARS} for k, v in values.items()} + names.append(name) + benunits[f"b{i}"] = { + "members": names, + "is_CTC_eligible": {TAX_CREDIT_YEAR: True}, + "is_WTC_eligible": {TAX_CREDIT_YEAR: True}, + } + households[f"h{i}"] = {"members": names} + return {"people": people_, "benunits": benunits, "households": households} + + +def reference_severely_disabled(units): + """CTC Regs 2002 reg 8(3)-(5) and WTC Regs 2002 reg 17(2)-(4), from the + inputs alone.""" + flags = [] + for unit in units: + for person in unit["adults"] + unit["dependants"]: + flags.append( + person["dla_sc_category"] == "HIGHER" + or person["pip_dl_category"] == "ENHANCED" + or person["aa_category"] == "HIGHER" + or person["armed_forces_independence_payment"] > 0 + ) + return np.array(flags) + + +def outputs(sim): + return dict( + flag=sim.calculate("is_severely_disabled_for_benefits", UC_YEAR), + uc_addition=sim.calculate( + "uc_individual_severely_disabled_child_element", UC_YEAR + ), + ctc_element=sim.calculate( + "CTC_severely_disabled_child_element", TAX_CREDIT_YEAR + ), + wtc_element=sim.calculate("WTC_severely_disabled_element", TAX_CREDIT_YEAR), + premium=sim.calculate("severe_disability_premium", UC_YEAR), + ) + + +@PROPERTY_SETTINGS +@given(populations) +def test_severe_disability_matches_the_tax_credit_condition(units): + sim = Simulation(situation=situation(units)) + flag = np.asarray(sim.calculate("is_severely_disabled_for_benefits", UC_YEAR)) + np.testing.assert_array_equal(flag, reference_severely_disabled(units)) + + +@PROPERTY_SETTINGS +@given(populations, st.data()) +def test_afcs_payments_never_change_severe_disability(units, data): + replacements = {} + + def new_afcs(name, _): + if name not in replacements: + replacements[name] = data.draw(amounts, label=f"afcs {name}") + return replacements[name] + + before = outputs(Simulation(situation=situation(units))) + no_afcs = outputs(Simulation(situation=situation(units, afcs=lambda n, v: 0.0))) + other_afcs = outputs(Simulation(situation=situation(units, afcs=new_afcs))) + for name, value in before.items(): + np.testing.assert_allclose(no_afcs[name], value, atol=1e-6, err_msg=name) + np.testing.assert_allclose(other_afcs[name], value, atol=1e-6, err_msg=name) + + +@PROPERTY_SETTINGS +@given(populations, st.floats(1, 15_000)) +def test_armed_forces_independence_payment_confers_severe_disability(units, amount): + before = np.asarray( + Simulation(situation=situation(units)).calculate( + "is_severely_disabled_for_benefits", UC_YEAR + ) + ) + after = np.asarray( + Simulation(situation=situation(units, afip=lambda n, v: amount)).calculate( + "is_severely_disabled_for_benefits", UC_YEAR + ) + ) + assert after.all() + assert (after >= before).all() diff --git a/policyengine_uk/variables/gov/dwp/afcs_reported.py b/policyengine_uk/variables/gov/dwp/afcs_reported.py index b42daaa61..66625a4cb 100644 --- a/policyengine_uk/variables/gov/dwp/afcs_reported.py +++ b/policyengine_uk/variables/gov/dwp/afcs_reported.py @@ -5,6 +5,14 @@ class afcs_reported(Variable): value_type = float entity = Person label = "Armed Forces Compensation Scheme (reported)" + documentation = ( + "Reported Armed Forces Compensation Scheme and war disablement pension " + "payments. In the Family Resources Survey this is benefit code 8, asked " + "as the Armed Forces Compensation Scheme (formerly War Disablement " + "Pension), including Guaranteed Income Payments. Armed forces " + "independence payment is the separate armed_forces_independence_payment " + "variable." + ) definition_period = YEAR unit = GBP uprating = "gov.economic_assumptions.indices.obr.consumer_price_index" diff --git a/policyengine_uk/variables/gov/dwp/afip.py b/policyengine_uk/variables/gov/dwp/afip.py index 69625ca85..1bea921f1 100644 --- a/policyengine_uk/variables/gov/dwp/afip.py +++ b/policyengine_uk/variables/gov/dwp/afip.py @@ -3,7 +3,14 @@ class armed_forces_independence_payment(Variable): label = "Armed Forces Independence Payment" + documentation = ( + "Armed forces independence payment under article 24A of the Armed " + "Forces and Reserve Forces (Compensation Scheme) Order 2011, payable " + "to people with a guaranteed income payment at a relevant percentage " + "of 50% or more. It is not part of afcs." + ) entity = Person definition_period = YEAR value_type = float unit = GBP + reference = "https://www.legislation.gov.uk/uksi/2011/517/article/24A"