From f1afbc458f2a68e64b8d12552618065adb009ae7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 12:28:33 -0500 Subject: [PATCH 1/6] Count armed forces independence payment as attendance allowance in UC housing cost contributions UC Regs 2013 reg 2 defines "attendance allowance" to include armed forces independence payment, and Sch 4 para 16(2)(d) exempts a non-dependant who receives it. Add uc_receives_attendance_allowance, which applies the reg 2 definition through gov.dwp.universal_credit.attendance_allowance_benefits, read it in uc_non_dep_deduction_exempt, and name it in the shared accommodation rate exception list in place of its two entries. Co-Authored-By: Claude Opus 5.5 --- changelog.d/2802.fixed.md | 1 + ...ared_accommodation_exception_benefits.yaml | 5 +- .../attendance_allowance_benefits.yaml | 14 + .../uc_non_dep_deduction_exempt.yaml | 30 ++ .../uc_receives_attendance_allowance.yaml | 82 ++++++ .../uc_afip_housing_contribution.yaml | 268 ++++++++++++++++++ ...uc_afip_housing_contribution_properties.py | 209 ++++++++++++++ .../uc_non_dep_deduction_exempt.py | 14 +- .../uc_receives_attendance_allowance.py | 36 +++ 9 files changed, 652 insertions(+), 7 deletions(-) create mode 100644 changelog.d/2802.fixed.md create mode 100644 policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml create mode 100644 policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml create mode 100644 policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py diff --git a/changelog.d/2802.fixed.md b/changelog.d/2802.fixed.md new file mode 100644 index 0000000000..12b0817b74 --- /dev/null +++ b/changelog.d/2802.fixed.md @@ -0,0 +1 @@ +Count armed forces independence payment as attendance allowance in the Universal Credit housing cost contribution exemption, so the model deducts no contribution for a non-dependant who receives it. diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml index 9f9765f986..e9479eae98 100644 --- a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml @@ -1,10 +1,9 @@ -description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. +description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. The list names attendance allowance through uc_receives_attendance_allowance, which applies the Universal Credit definition, so armed forces independence payment counts. values: 2013-04-29: - - attendance_allowance + - uc_receives_attendance_allowance - dla_sc_middle_plus - pip_dl - - armed_forces_independence_payment metadata: unit: list label: Universal Credit shared accommodation rate disability exceptions diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml new file mode 100644 index 0000000000..703b4ca9cc --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml @@ -0,0 +1,14 @@ +description: Benefits that count as attendance allowance under the Universal Credit Regulations, so that a person who receives any of them is in receipt of attendance allowance for Universal Credit. The list covers heads (a) and (f) of the definition. The model holds no separate amount for heads (b), (d) and (e), so it cannot list them. +values: + 2013-04-29: + - attendance_allowance + - armed_forces_independence_payment +metadata: + unit: list + period: year + label: Universal Credit attendance allowance benefits + reference: + - title: Universal Credit Regulations 2013 reg 2 ("attendance allowance") + href: https://www.legislation.gov.uk/uksi/2013/376/regulation/2 + - title: Universal Credit Regulations (Northern Ireland) 2016 reg 2 ("attendance allowance") + href: https://www.legislation.gov.uk/nisr/2016/216/regulation/2 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml index 13d4369453..da9653d1ad 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml @@ -51,3 +51,33 @@ is_entitled_to_carer_benefit: false is_carer_for_benefits: true uc_non_dep_deduction_exempt: false + +# UC Regs 2013 Sch 4 para 16(2)(d) exempts "a person in receipt of attendance +# allowance", which reg 2 defines to include armed forces independence payment. +- name: Exempt - armed forces independence payment + period: 2026 + absolute_error_margin: 0 + input: + pension_credit: 0 + dla_sc_middle_plus: false + pip_dl: 0 + attendance_allowance: 0 + armed_forces_independence_payment: 9_000 + receives_carers_allowance: false + output: + uc_receives_attendance_allowance: true + uc_non_dep_deduction_exempt: true + +- name: Exempt - attendance allowance + period: 2026 + absolute_error_margin: 0 + input: + pension_credit: 0 + dla_sc_middle_plus: false + pip_dl: 0 + aa_category: LOWER + armed_forces_independence_payment: 0 + receives_carers_allowance: false + output: + uc_receives_attendance_allowance: true + uc_non_dep_deduction_exempt: true diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml new file mode 100644 index 0000000000..c08221d535 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml @@ -0,0 +1,82 @@ +# UC Regs 2013 reg 2: "attendance allowance" means (a) an attendance allowance +# under SSCBA 1992 s.64; (b) an increase of disablement pension under s.104 or +# 105; (d) a Personal Injuries (Civilians) Scheme 1983 payment or an analogous +# payment; (e) an attendance addition to a war disablement pension; and (f) +# armed forces independence payment. Head (c) is omitted. Receipt of any +# amount counts, so the AFIP amounts below are illustrative. +- name: Case 1, attendance allowance at the higher rate counts under head (a). + period: 2026 + input: + aa_category: HIGHER + output: + uc_receives_attendance_allowance: true + +- name: Case 2, attendance allowance at the lower rate counts under head (a). + period: 2026 + input: + aa_category: LOWER + output: + uc_receives_attendance_allowance: true + +- name: Case 3, armed forces independence payment counts under head (f). + period: 2026 + input: + armed_forces_independence_payment: 9_000 + output: + attendance_allowance: 0 + uc_receives_attendance_allowance: true + +- name: Case 4, no attendance allowance and no armed forces independence payment. + period: 2026 + input: + aa_category: NONE + armed_forces_independence_payment: 0 + output: + uc_receives_attendance_allowance: false + +- name: Case 5, the DLA care component is not attendance allowance. + period: 2026 + input: + dla_sc_category: HIGHER + output: + dla_sc_middle_plus: true + uc_receives_attendance_allowance: false + +- name: Case 6, the PIP daily living component is not attendance allowance. + period: 2026 + input: + pip_dl_category: ENHANCED + output: + uc_receives_attendance_allowance: false + +- name: Case 7, disablement pension without a constant attendance increase is not attendance allowance. + period: 2026 + input: + iidb_reported: 5_000 + output: + uc_receives_attendance_allowance: false + +- name: Case 8, Armed Forces Compensation Scheme payments and a war disablement pension without an attendance addition are not attendance allowance. + period: 2026 + input: + afcs_reported: 5_000 + output: + uc_receives_attendance_allowance: false + +- name: Case 9, a couple where only the partner receives armed forces independence payment. + period: 2026 + input: + people: + person1: + age: 40 + person2: + age: 40 + armed_forces_independence_payment: 9_000 + benunits: + benunit: + members: [person1, person2] + households: + household: + members: [person1, person2] + output: + uc_receives_attendance_allowance: [false, true] diff --git a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml new file mode 100644 index 0000000000..66c42020ab --- /dev/null +++ b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml @@ -0,0 +1,268 @@ +# UC Regs 2013 Sch 4 para 16(2)(d) exempts a non-dependant "in receipt of +# attendance allowance", and reg 2 defines that term to include armed forces +# independence payment (head (f)). Para 16(2)(e) exempts a non-dependant who +# receives the PIP daily living component. +# 2026-27 amounts, from legislation.gov.uk: +# - Standard allowance, single claimant aged 25 or over (reg 36): 12 x 424.90 = 5,098.80. +# - Housing cost contribution (Sch 4 para 14(1)), one flat amount with no +# earnings bands: 12 x 96.55 = 1,158.60. +# The renter has no earnings or capital and pays the council 6,000 a year, so +# UC = 5,098.80 + 6,000 - contributions. +# The non-dependant's own family does not claim UC. AFIP amounts are +# illustrative: receipt of any amount counts. + +- name: Case 1, a non-dependant with no qualifying benefit bears a contribution. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_non_dep_deduction_exempt: [false, false] + uc_individual_non_dep_deduction: [0, 1_158.6] + uc_non_dep_deductions: [1_158.6, 0] + uc_housing_costs_element: [4_841.4, 0] + universal_credit: [9_940.2, 0] + +- name: Case 2, a non-dependant receiving the PIP daily living component bears none. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + pip_dl_category: STANDARD + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_non_dep_deduction_exempt: [false, true] + uc_individual_non_dep_deduction: [0, 0] + uc_non_dep_deductions: [0, 0] + uc_housing_costs_element: [6_000, 0] + universal_credit: [11_098.8, 0] + +- name: Case 3, a non-dependant receiving armed forces independence payment bears none. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + armed_forces_independence_payment: 9_000 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_receives_attendance_allowance: [false, true] + uc_non_dep_deduction_exempt: [false, true] + uc_individual_non_dep_deduction: [0, 0] + uc_non_dep_deductions: [0, 0] + uc_housing_costs_element: [6_000, 0] + universal_credit: [11_098.8, 0] + +- name: Case 4, a non-dependant with no earnings and armed forces independence payment bears none. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 0 + armed_forces_independence_payment: 9_000 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_non_dep_deduction_exempt: [false, true] + uc_non_dep_deductions: [0, 0] + uc_housing_costs_element: [6_000, 0] + universal_credit: [11_098.8, 0] + +- name: Case 5, a non-dependant earning 100,000 with armed forces independence payment bears none. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 100_000 + armed_forces_independence_payment: 9_000 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_non_dep_deduction_exempt: [false, true] + uc_non_dep_deductions: [0, 0] + uc_housing_costs_element: [6_000, 0] + universal_credit: [11_098.8, 0] + +- name: Case 6, in a non-dependant couple only the partner without armed forces independence payment bears a contribution. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + armed_forces_independence_payment: 9_000 + non_dependant_partner: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant, non_dependant_partner] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant, non_dependant_partner] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_non_dep_deduction_exempt: [false, true, false] + uc_individual_non_dep_deduction: [0, 0, 1_158.6] + uc_non_dep_deductions: [1_158.6, 0] + uc_housing_costs_element: [4_841.4, 0] + universal_credit: [9_940.2, 0] + +- name: Case 7, a non-dependant couple without a qualifying benefit bears two contributions. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + non_dependant_partner: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant, non_dependant_partner] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant, non_dependant_partner] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + # 2 x 1,158.60 = 2,317.20; 6,000 - 2,317.20 = 3,682.80; 5,098.80 + 3,682.80 = 8,781.60. + uc_non_dep_deductions: [2_317.2, 0] + uc_housing_costs_element: [3_682.8, 0] + universal_credit: [8_781.6, 0] diff --git a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py new file mode 100644 index 0000000000..726400ed83 --- /dev/null +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -0,0 +1,209 @@ +"""Property-based tests: armed forces independence payment (AFIP) counts as +attendance allowance in the Universal Credit housing cost contribution rules. + +UC Regs 2013 Sch 4 para 13 deducts a housing cost contribution for each +non-dependant in the renter's extended benefit unit. Para 16(2)(d) exempts a +non-dependant "in receipt of attendance allowance", and reg 2 defines that +term to include armed forces independence payment (head (f)). Reg 66 does not +list AFIP as unearned income, so it does not reduce the renter's award. + +Each generated household has a renter, who claims Universal Credit, and a +non-dependant in a family of their own that does not claim. The same +households are calculated with no AFIP, then with a positive AFIP for the +non-dependant, the renter or both. Invariants: + +1. Raising AFIP from nil to any positive amount never lowers Universal + Credit and never raises the housing cost contributions. +2. A non-dependant who receives AFIP is exempt and bears no contribution. +3. Where only the non-dependant receives AFIP, the renter's housing costs + element rises by no more than the contributions fall (para 14(3) floors + the element at nil), and Universal Credit by no more than the element. +""" + +import numpy as np +import pytest +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") + +YEAR = 2026 +# Fixed slots, so one simulation serves every example. Each slot's tenure and +# region are fixed; the generated inputs change from example to example. +TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA", "RENT_PRIVATELY"] +REGIONS = ["NORTH_WEST", "LONDON", "SCOTLAND", "WALES"] +SLOTS = 12 +PROPERTY_SETTINGS = settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +def situation(): + people, benunits, households = {}, {}, {} + for i in range(SLOTS): + renter, non_dependant = f"renter_{i}", f"non_dependant_{i}" + for name, head in [(renter, True), (non_dependant, False)]: + people[name] = { + "age": {YEAR: 40}, + "is_household_head": {YEAR: head}, + "is_claimant_or_partner": {YEAR: True}, + "employment_income": {YEAR: 0}, + "pip_dl": {YEAR: 0}, + "armed_forces_independence_payment": {YEAR: 0}, + } + benunits[f"renter_family_{i}"] = { + "members": [renter], + "would_claim_uc": {YEAR: True}, + "claims_all_entitled_benefits": {YEAR: False}, + } + benunits[f"non_dependant_family_{i}"] = { + "members": [non_dependant], + "would_claim_uc": {YEAR: False}, + "claims_all_entitled_benefits": {YEAR: False}, + } + households[f"household_{i}"] = { + "members": [renter, non_dependant], + "rent": {YEAR: 6_000}, + "tenure_type": {YEAR: TENURES[i % len(TENURES)]}, + "region": {YEAR: REGIONS[i % len(REGIONS)]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +@pytest.fixture(scope="module") +def simulation(cloned_uk_tax_benefit_system): + return Simulation(situation=situation()) + + +money = st.floats(0, 60_000, allow_nan=False) +positive_afip = st.floats(0, 50_000, exclude_min=True, allow_nan=False) + + +@st.composite +def household(draw): + afip_to_non_dependant = draw(st.booleans()) + return dict( + renter_age=draw(st.integers(18, 64)), + renter_earnings=draw(st.one_of(st.just(0.0), money)), + rent=draw(st.one_of(st.floats(0, 2_000), st.floats(2_000, 20_000))), + non_dependant_age=draw(st.integers(18, 85)), + non_dependant_earnings=draw(st.one_of(st.just(0.0), money)), + non_dependant_pip=draw( + st.one_of(st.just(0.0), st.just(0.0), st.floats(1_000, 6_000)) + ), + afip_to_non_dependant=afip_to_non_dependant, + # At least one person receives AFIP in each household. + afip_to_renter=draw(st.booleans()) or not afip_to_non_dependant, + afip=draw(positive_afip), + ) + + +populations = st.lists(household(), min_size=SLOTS, max_size=SLOTS) + + +def issue_example(): + """The issue's household (PolicyEngine/policyengine-uk#2802): a renter + aged 40 with no earnings paying the council 6,000 a year, and a + non-dependant aged 25 earning 20,000 who receives AFIP.""" + return [ + dict( + renter_age=40, + renter_earnings=0.0, + rent=6_000.0, + non_dependant_age=25, + non_dependant_earnings=20_000.0, + non_dependant_pip=0.0, + afip_to_non_dependant=True, + afip_to_renter=False, + afip=9_000.0, + ) + ] * SLOTS + + +def person_array(households, renter_key, non_dependant_key): + values = [] + for h in households: + values += [h[renter_key], h[non_dependant_key]] + return np.array(values, dtype=float) + + +def calculate(sim, households, afip): + sim.set_input( + "age", YEAR, person_array(households, "renter_age", "non_dependant_age") + ) + sim.set_input( + "employment_income", + YEAR, + person_array(households, "renter_earnings", "non_dependant_earnings"), + ) + pip = [] + for h in households: + pip += [0.0, h["non_dependant_pip"]] + sim.set_input("pip_dl", YEAR, np.array(pip)) + sim.set_input("armed_forces_independence_payment", YEAR, afip) + sim.set_input("rent", YEAR, np.array([h["rent"] for h in households])) + sim.drop_computed_arrays() + return { + name: np.asarray(sim.calculate(name, YEAR)) + for name in [ + "universal_credit", + "uc_non_dep_deductions", + "uc_housing_costs_element", + "uc_individual_non_dep_deduction", + "uc_non_dep_deduction_exempt", + ] + } + + +def afip_amounts(households): + afip = [] + for h in households: + afip += [ + h["afip"] if h["afip_to_renter"] else 0.0, + h["afip"] if h["afip_to_non_dependant"] else 0.0, + ] + return np.array(afip) + + +@PROPERTY_SETTINGS +@given(households=populations) +@example(households=issue_example()) +def test_afip_never_lowers_uc_and_exempts_the_non_dependant(simulation, households): + before = calculate(simulation, households, np.zeros(2 * SLOTS)) + after = calculate(simulation, households, afip_amounts(households)) + renter_family = slice(0, 2 * SLOTS, 2) + non_dependant = slice(1, 2 * SLOTS, 2) + + # Invariant 1. + uc_change = (after["universal_credit"] - before["universal_credit"])[renter_family] + contribution_change = ( + after["uc_non_dep_deductions"] - before["uc_non_dep_deductions"] + )[renter_family] + assert (uc_change >= -0.01).all(), uc_change + assert (contribution_change <= 0.01).all(), contribution_change + + # Invariant 2: Sch 4 para 16(2)(d) with reg 2 head (f). + receives = np.array([h["afip_to_non_dependant"] for h in households]) + assert after["uc_non_dep_deduction_exempt"][non_dependant][receives].all() + np.testing.assert_array_equal( + after["uc_individual_non_dep_deduction"][non_dependant][receives], 0 + ) + + # Invariant 3. + only_non_dependant = receives & ~np.array([h["afip_to_renter"] for h in households]) + element_change = ( + after["uc_housing_costs_element"] - before["uc_housing_costs_element"] + )[renter_family] + assert (element_change[only_non_dependant] >= -0.01).all() + assert ( + element_change[only_non_dependant] + <= -contribution_change[only_non_dependant] + 0.01 + ).all() + assert ( + uc_change[only_non_dependant] <= element_change[only_non_dependant] + 0.01 + ).all() diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py index be35b6197f..3429ba3cad 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py @@ -9,8 +9,10 @@ class uc_non_dep_deduction_exempt(Variable): documentation = ( "No housing costs contribution is deducted for a non-dependant who is " "receiving Pension Credit, the middle or higher rate of the DLA care " - "component, the PIP daily living component or Attendance Allowance, or " - "who receives Carer's Allowance or Carer Support Payment, or who is " + "component, the PIP daily living component or attendance allowance as " + "Universal Credit defines it, which includes armed forces independence " + "payment (see uc_receives_attendance_allowance), or who receives " + "Carer's Allowance or Carer Support Payment, or who is " "responsible for a child under the child responsibility age limit. " "Benefit-unit membership represents the child's normal residence " "with the claimant or partner; the existing Universal Credit child " @@ -18,7 +20,10 @@ class uc_non_dep_deduction_exempt(Variable): "entitlement reduced to nil by overlapping benefits does not qualify " "for the carer exemption." ) - reference = "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/16" + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/schedule/4/paragraph/16", + "https://www.legislation.gov.uk/uksi/2013/376/regulation/2", + ) def formula(person, period, parameters): p = parameters( @@ -35,7 +40,8 @@ def formula(person, period, parameters): person.benunit("uc_non_dep_receives_pension_credit", period) | person("dla_sc_middle_plus", period) | (person("pip_dl", period) > 0) - | (person("attendance_allowance", period) > 0) + # Para 16(2)(d), with "attendance allowance" as reg 2 defines it. + | person("uc_receives_attendance_allowance", period) | person("receives_carer_benefit", period) | responsible_for_young_child ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py new file mode 100644 index 0000000000..a173485a20 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py @@ -0,0 +1,36 @@ +from policyengine_uk.model_api import * + + +class uc_receives_attendance_allowance(Variable): + value_type = bool + entity = Person + label = "In receipt of attendance allowance for Universal Credit" + documentation = ( + "Whether the person receives attendance allowance as regulation 2 of " + "the Universal Credit Regulations 2013 defines it. The definition " + "has five heads: (a) attendance allowance under section 64 of the " + "Social Security Contributions and Benefits Act 1992; (b) an increase " + "of disablement pension under section 104 or 105 of that Act, where " + "constant attendance is needed or for exceptionally severe " + "disablement; (d) a payment under article 14, 15, 16, 43 or 44 of the " + "Personal Injuries (Civilians) Scheme 1983, or any analogous payment; " + "(e) any payment for the need for attendance added to a war " + "disablement pension; and (f) armed forces independence payment. An " + "amending instrument omitted head (c). The model counts heads (a) and " + "(f), from gov.dwp.universal_credit.attendance_allowance_benefits. It " + "holds no separate amount for the other three, so a person who " + "receives only one of them does not count: iidb is a single reported " + "amount with no constant attendance or exceptionally severe " + "disablement increase of its own, afcs holds war disablement pensions " + "without any attendance addition, and no variable holds Personal " + "Injuries (Civilians) Scheme payments." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2013/376/regulation/2", + "https://www.legislation.gov.uk/nisr/2016/216/regulation/2", + ) + + def formula(person, period, parameters): + p = parameters(period).gov.dwp.universal_credit + return add(person, period, p.attendance_allowance_benefits) > 0 From 46e25485f5d876314961674785c75914de3408d7 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 12:45:16 -0500 Subject: [PATCH 2/6] Name who acts in the attendance allowance docs and add the LHA list differential The docs and test comments now say who defines and who deducts. A property test checks that the shared accommodation rate exception list gives the same result as its previous entries. Co-Authored-By: Claude Opus 5.5 --- ...ared_accommodation_exception_benefits.yaml | 2 +- .../attendance_allowance_benefits.yaml | 2 +- .../uc_non_dep_deduction_exempt.yaml | 5 +- .../uc_receives_attendance_allowance.yaml | 13 +- .../uc_afip_housing_contribution.yaml | 13 +- ...uc_afip_housing_contribution_properties.py | 128 ++++++++++++++++-- .../uc_non_dep_deduction_exempt.py | 12 +- .../uc_receives_attendance_allowance.py | 37 ++--- 8 files changed, 164 insertions(+), 48 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml index e9479eae98..8f999baaea 100644 --- a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml @@ -1,4 +1,4 @@ -description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. The list names attendance allowance through uc_receives_attendance_allowance, which applies the Universal Credit definition, so armed forces independence payment counts. +description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. The list names uc_receives_attendance_allowance for attendance allowance, so the model applies the Universal Credit definition and counts armed forces independence payment. values: 2013-04-29: - uc_receives_attendance_allowance diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml index 703b4ca9cc..c501a3c88b 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml @@ -1,4 +1,4 @@ -description: Benefits that count as attendance allowance under the Universal Credit Regulations, so that a person who receives any of them is in receipt of attendance allowance for Universal Credit. The list covers heads (a) and (f) of the definition. The model holds no separate amount for heads (b), (d) and (e), so it cannot list them. +description: Benefits that the Secretary of State counts as attendance allowance for Universal Credit, heads (a) and (f) of the definition in regulation 2 of the Universal Credit Regulations 2013. The model holds no separate amount for heads (b), (d) and (e), so the list leaves them out. values: 2013-04-29: - attendance_allowance diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml index da9653d1ad..fc0b914bfc 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.yaml @@ -52,8 +52,9 @@ is_carer_for_benefits: true uc_non_dep_deduction_exempt: false -# UC Regs 2013 Sch 4 para 16(2)(d) exempts "a person in receipt of attendance -# allowance", which reg 2 defines to include armed forces independence payment. +# Under UC Regs 2013 Sch 4 para 16(2)(d), DWP deducts no contribution for "a +# person in receipt of attendance allowance", and the Secretary of State defines +# that term in reg 2 to include armed forces independence payment. - name: Exempt - armed forces independence payment period: 2026 absolute_error_margin: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml index c08221d535..33e1e10583 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml @@ -1,9 +1,10 @@ -# UC Regs 2013 reg 2: "attendance allowance" means (a) an attendance allowance -# under SSCBA 1992 s.64; (b) an increase of disablement pension under s.104 or -# 105; (d) a Personal Injuries (Civilians) Scheme 1983 payment or an analogous -# payment; (e) an attendance addition to a war disablement pension; and (f) -# armed forces independence payment. Head (c) is omitted. Receipt of any -# amount counts, so the AFIP amounts below are illustrative. +# In UC Regs 2013 reg 2, the Secretary of State defines "attendance allowance" +# as (a) an attendance allowance under SSCBA 1992 s.64; (b) an increase of +# disablement pension under s.104 or 105; (d) a Personal Injuries (Civilians) +# Scheme 1983 payment or an analogous payment; (e) an attendance addition to a +# war disablement pension; and (f) armed forces independence payment. The +# Secretary of State omitted head (c) in 2013 (S.I. 2013/1508). Any amount of +# AFIP in payment qualifies, so the cases use illustrative amounts. - name: Case 1, attendance allowance at the higher rate counts under head (a). period: 2026 input: diff --git a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml index 66c42020ab..15cc29ea90 100644 --- a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml +++ b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml @@ -1,15 +1,16 @@ -# UC Regs 2013 Sch 4 para 16(2)(d) exempts a non-dependant "in receipt of -# attendance allowance", and reg 2 defines that term to include armed forces -# independence payment (head (f)). Para 16(2)(e) exempts a non-dependant who -# receives the PIP daily living component. +# Under UC Regs 2013 Sch 4 para 16(2)(d), DWP deducts no housing cost +# contribution for a non-dependant "in receipt of attendance allowance", and +# the Secretary of State defines that term in reg 2 to include armed forces +# independence payment (head (f)). Under para 16(2)(e), DWP deducts none for a +# non-dependant who receives the PIP daily living component. # 2026-27 amounts, from legislation.gov.uk: # - Standard allowance, single claimant aged 25 or over (reg 36): 12 x 424.90 = 5,098.80. # - Housing cost contribution (Sch 4 para 14(1)), one flat amount with no # earnings bands: 12 x 96.55 = 1,158.60. # The renter has no earnings or capital and pays the council 6,000 a year, so # UC = 5,098.80 + 6,000 - contributions. -# The non-dependant's own family does not claim UC. AFIP amounts are -# illustrative: receipt of any amount counts. +# The non-dependant's own family does not claim UC. Any amount of AFIP in +# payment qualifies, so the cases use illustrative amounts. - name: Case 1, a non-dependant with no qualifying benefit bears a contribution. period: 2026 diff --git a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py index 726400ed83..b0d32839a0 100644 --- a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -1,23 +1,26 @@ """Property-based tests: armed forces independence payment (AFIP) counts as attendance allowance in the Universal Credit housing cost contribution rules. -UC Regs 2013 Sch 4 para 13 deducts a housing cost contribution for each -non-dependant in the renter's extended benefit unit. Para 16(2)(d) exempts a -non-dependant "in receipt of attendance allowance", and reg 2 defines that -term to include armed forces independence payment (head (f)). Reg 66 does not -list AFIP as unearned income, so it does not reduce the renter's award. +Under UC Regs 2013 Sch 4 para 13, DWP deducts a housing cost contribution for +each non-dependant in the renter's extended benefit unit. Under para 16(2)(d) +it deducts none for a non-dependant "in receipt of attendance allowance", and +the Secretary of State defines that term in reg 2 to include AFIP (head (f)). +The Secretary of State names no AFIP head among the descriptions of unearned +income in reg 66, and the model leaves AFIP out of the renter's unearned +income. Each generated household has a renter, who claims Universal Credit, and a -non-dependant in a family of their own that does not claim. The same -households are calculated with no AFIP, then with a positive AFIP for the +non-dependant in a family of their own that does not claim. The tests +calculate the same households with no AFIP, then with a positive AFIP for the non-dependant, the renter or both. Invariants: 1. Raising AFIP from nil to any positive amount never lowers Universal Credit and never raises the housing cost contributions. 2. A non-dependant who receives AFIP is exempt and bears no contribution. 3. Where only the non-dependant receives AFIP, the renter's housing costs - element rises by no more than the contributions fall (para 14(3) floors - the element at nil), and Universal Credit by no more than the element. + element rises by no more than the contributions fall (under para 14(3), + DWP reduces the element to nil and no further), and Universal Credit rises + by no more than the element. """ import numpy as np @@ -207,3 +210,110 @@ def test_afip_never_lowers_uc_and_exempts_the_non_dependant(simulation, househol assert ( uc_change[only_non_dependant] <= element_change[only_non_dependant] + 0.01 ).all() + + +# Sch 4 para 29(5) excepts a renter under 35 who receives the DLA care +# component at the middle or highest rate, attendance allowance or the PIP +# daily living component. The shared accommodation rate exception list used to +# name attendance allowance and AFIP itself; it now names +# uc_receives_attendance_allowance, which applies the reg 2 definition. The +# differential below checks the two lists give the same result. +LHA_EXCEPTIONS = "gov.dwp.LHA.shared_accommodation_exception_benefits" +PREVIOUS_LHA_EXCEPTIONS = [ + "attendance_allowance", + "dla_sc_middle_plus", + "pip_dl", + "armed_forces_independence_payment", +] +RENTER_OUTPUTS = [ + "is_lha_shared_accommodation_rate_specified_renter", + "universal_credit", +] + + +def single_renters(): + people, benunits, households = {}, {}, {} + for i in range(SLOTS): + renter = f"renter_{i}" + people[renter] = { + "age": {YEAR: 30}, + "is_household_head": {YEAR: True}, + "is_claimant_or_partner": {YEAR: True}, + "employment_income": {YEAR: 0}, + "attendance_allowance": {YEAR: 0}, + "dla_sc_middle_plus": {YEAR: False}, + "pip_dl": {YEAR: 0}, + "armed_forces_independence_payment": {YEAR: 0}, + } + benunits[f"renter_family_{i}"] = { + "members": [renter], + "would_claim_uc": {YEAR: True}, + "claims_all_entitled_benefits": {YEAR: False}, + } + households[f"household_{i}"] = { + "members": [renter], + "rent": {YEAR: 9_000}, + "tenure_type": {YEAR: "RENT_PRIVATELY"}, + "region": {YEAR: REGIONS[i % len(REGIONS)]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +@pytest.fixture(scope="module") +def renter_simulation(cloned_uk_tax_benefit_system): + return Simulation(situation=single_renters()) + + +sometimes = st.one_of(st.just(0.0), st.just(0.0), st.floats(1, 9_000)) + + +@st.composite +def renter(draw): + return dict( + age=draw(st.integers(18, 40)), + attendance_allowance=draw(sometimes), + dla_sc_middle_plus=draw(st.booleans()), + pip_dl=draw(sometimes), + armed_forces_independence_payment=draw(sometimes), + ) + + +def renter_outputs(sim): + sim.drop_computed_arrays() + return {name: np.asarray(sim.calculate(name, YEAR)) for name in RENTER_OUTPUTS} + + +@PROPERTY_SETTINGS +@given(renters=st.lists(renter(), min_size=SLOTS, max_size=SLOTS)) +@example( + renters=[ + dict( + age=age, + attendance_allowance=0.0, + dla_sc_middle_plus=False, + pip_dl=0.0, + armed_forces_independence_payment=afip, + ) + for age in (25, 34, 35, 40) + for afip in (0.0, 9_000.0, 0.01) + ] +) +def test_shared_accommodation_exceptions_match_the_previous_list( + renter_simulation, renters +): + sim = renter_simulation + for name in renters[0]: + sim.set_input(name, YEAR, np.array([r[name] for r in renters])) + current = renter_outputs(sim) + parameter = sim.tax_benefit_system.parameters.get_child(LHA_EXCEPTIONS) + listed = parameter(f"{YEAR}-01-01") + try: + parameter.update(period=str(YEAR), value=PREVIOUS_LHA_EXCEPTIONS) + sim.tax_benefit_system.reset_parameter_caches() + previous = renter_outputs(sim) + finally: + parameter.update(period=str(YEAR), value=listed) + sim.tax_benefit_system.reset_parameter_caches() + sim.drop_computed_arrays() + for name in RENTER_OUTPUTS: + np.testing.assert_array_equal(current[name], previous[name], err_msg=name) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py index 3429ba3cad..54598b0f3c 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py @@ -9,10 +9,11 @@ class uc_non_dep_deduction_exempt(Variable): documentation = ( "No housing costs contribution is deducted for a non-dependant who is " "receiving Pension Credit, the middle or higher rate of the DLA care " - "component, the PIP daily living component or attendance allowance as " - "Universal Credit defines it, which includes armed forces independence " - "payment (see uc_receives_attendance_allowance), or who receives " - "Carer's Allowance or Carer Support Payment, or who is " + "component, the PIP daily living component or attendance allowance, " + "which the Secretary of State defines for Universal Credit to include " + "armed forces independence payment (see " + "uc_receives_attendance_allowance), or " + "who receives Carer's Allowance or Carer Support Payment, or who is " "responsible for a child under the child responsibility age limit. " "Benefit-unit membership represents the child's normal residence " "with the claimant or partner; the existing Universal Credit child " @@ -40,7 +41,8 @@ def formula(person, period, parameters): person.benunit("uc_non_dep_receives_pension_credit", period) | person("dla_sc_middle_plus", period) | (person("pip_dl", period) > 0) - # Para 16(2)(d), with "attendance allowance" as reg 2 defines it. + # Para 16(2)(d), reading "attendance allowance" with its reg 2 + # definition. | person("uc_receives_attendance_allowance", period) | person("receives_carer_benefit", period) | responsible_for_young_child diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py index a173485a20..e2b0d85cf3 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py @@ -6,24 +6,25 @@ class uc_receives_attendance_allowance(Variable): entity = Person label = "In receipt of attendance allowance for Universal Credit" documentation = ( - "Whether the person receives attendance allowance as regulation 2 of " - "the Universal Credit Regulations 2013 defines it. The definition " - "has five heads: (a) attendance allowance under section 64 of the " - "Social Security Contributions and Benefits Act 1992; (b) an increase " - "of disablement pension under section 104 or 105 of that Act, where " - "constant attendance is needed or for exceptionally severe " - "disablement; (d) a payment under article 14, 15, 16, 43 or 44 of the " - "Personal Injuries (Civilians) Scheme 1983, or any analogous payment; " - "(e) any payment for the need for attendance added to a war " - "disablement pension; and (f) armed forces independence payment. An " - "amending instrument omitted head (c). The model counts heads (a) and " - "(f), from gov.dwp.universal_credit.attendance_allowance_benefits. It " - "holds no separate amount for the other three, so a person who " - "receives only one of them does not count: iidb is a single reported " - "amount with no constant attendance or exceptionally severe " - "disablement increase of its own, afcs holds war disablement pensions " - "without any attendance addition, and no variable holds Personal " - "Injuries (Civilians) Scheme payments." + "Whether the person receives attendance allowance as the Secretary of " + "State defines it in regulation 2 of the Universal Credit Regulations " + "2013. The definition lists five heads: (a) attendance allowance under " + "section 64 of the Social Security Contributions and Benefits Act " + "1992; (b) an increase of disablement pension under section 104 or 105 " + "of that Act, where constant attendance is needed or for exceptionally " + "severe disablement; (d) a payment under article 14, 15, 16, 43 or 44 " + "of the Personal Injuries (Civilians) Scheme 1983, or any analogous " + "payment; (e) any payment for the need for attendance added to a war " + "disablement pension; and (f) armed forces independence payment. The " + "Secretary of State omitted head (c) in 2013 (S.I. 2013/1508). The " + "model counts heads (a) and (f), from " + "gov.dwp.universal_credit.attendance_allowance_benefits. It holds no " + "separate amount for the other three, so it does not count a person " + "who receives only one of them: iidb holds one reported amount with no " + "constant attendance or exceptionally severe disablement increase of " + "its own, afcs holds war disablement pensions without any attendance " + "addition, and no variable holds Personal Injuries (Civilians) Scheme " + "payments." ) definition_period = YEAR reference = ( From 4ffa8a77439e0900b4e9f4b01393a79762f7fe92 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 15:46:35 -0500 Subject: [PATCH 3/6] Start the AFIP property amounts at a penny The model stores amounts as 32-bit floats, so a generated AFIP of 5e-324 reached it as zero. Co-Authored-By: Claude Opus 5.5 --- .../tests/test_uc_afip_housing_contribution_properties.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py index b0d32839a0..74719bcc81 100644 --- a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -84,7 +84,9 @@ def simulation(cloned_uk_tax_benefit_system): money = st.floats(0, 60_000, allow_nan=False) -positive_afip = st.floats(0, 50_000, exclude_min=True, allow_nan=False) +# The model holds amounts as 32-bit floats, which store an amount below about +# 1e-45 as zero, so positive amounts start at a penny. +positive_afip = st.floats(0.01, 50_000, allow_nan=False) @st.composite From 926099dfa1561ab80db8e6577dad2e594f68d7e3 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sun, 1 Nov 2026 16:15:48 -0500 Subject: [PATCH 4/6] Tighten the attendance allowance docs and test comments Co-Authored-By: Claude Opus 5.5 --- ...est_uc_afip_housing_contribution_properties.py | 15 ++++++++------- .../uc_receives_attendance_allowance.py | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py index 74719bcc81..a1ae066810 100644 --- a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -16,7 +16,8 @@ 1. Raising AFIP from nil to any positive amount never lowers Universal Credit and never raises the housing cost contributions. -2. A non-dependant who receives AFIP is exempt and bears no contribution. +2. The model exempts a non-dependant who receives AFIP and charges no + contribution for them. 3. Where only the non-dependant receives AFIP, the renter's housing costs element rises by no more than the contributions fall (under para 14(3), DWP reduces the element to nil and no further), and Universal Credit rises @@ -214,12 +215,12 @@ def test_afip_never_lowers_uc_and_exempts_the_non_dependant(simulation, househol ).all() -# Sch 4 para 29(5) excepts a renter under 35 who receives the DLA care -# component at the middle or highest rate, attendance allowance or the PIP -# daily living component. The shared accommodation rate exception list used to -# name attendance allowance and AFIP itself; it now names -# uc_receives_attendance_allowance, which applies the reg 2 definition. The -# differential below checks the two lists give the same result. +# Under Sch 4 para 29(5), DWP excepts from the shared accommodation rate a +# renter under 35 who receives the DLA care component at the middle or highest +# rate, attendance allowance or the PIP daily living component. The exception +# list names uc_receives_attendance_allowance, which applies the reg 2 +# definition. The differential below checks that the list gives the same result +# as one naming attendance allowance and AFIP directly. LHA_EXCEPTIONS = "gov.dwp.LHA.shared_accommodation_exception_benefits" PREVIOUS_LHA_EXCEPTIONS = [ "attendance_allowance", diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py index e2b0d85cf3..277d1c252a 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py @@ -11,8 +11,8 @@ class uc_receives_attendance_allowance(Variable): "2013. The definition lists five heads: (a) attendance allowance under " "section 64 of the Social Security Contributions and Benefits Act " "1992; (b) an increase of disablement pension under section 104 or 105 " - "of that Act, where constant attendance is needed or for exceptionally " - "severe disablement; (d) a payment under article 14, 15, 16, 43 or 44 " + "of that Act, for constant attendance or exceptionally severe " + "disablement; (d) a payment under article 14, 15, 16, 43 or 44 " "of the Personal Injuries (Civilians) Scheme 1983, or any analogous " "payment; (e) any payment for the need for attendance added to a war " "disablement pension; and (f) armed forces independence payment. The " From de6a8476274910cdfafb2a359c5a2528b185f115 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 01:31:55 -0500 Subject: [PATCH 5/6] Apply the review findings on the attendance allowance docs and tests Describe what afcs holds, name who acts in the docs, and pin a penny of armed forces independence payment in two fixed cases. A new property checks that one non-dependant's payment moves only their own exemption. Co-Authored-By: Claude Opus 5.5 --- ...ared_accommodation_exception_benefits.yaml | 2 +- .../attendance_allowance_benefits.yaml | 2 +- .../uc_receives_attendance_allowance.yaml | 10 +- .../uc_afip_housing_contribution.yaml | 59 +++++++++++- ...uc_afip_housing_contribution_properties.py | 92 ++++++++++++++++++- .../uc_non_dep_deduction_exempt.py | 17 ++-- .../uc_receives_attendance_allowance.py | 19 ++-- 7 files changed, 173 insertions(+), 28 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml index 8f999baaea..64fd56ccfc 100644 --- a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml @@ -1,4 +1,4 @@ -description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. The list names uc_receives_attendance_allowance for attendance allowance, so the model applies the Universal Credit definition and counts armed forces independence payment. +description: Disability benefits whose receipt makes a renter under 35 an excepted person, so DWP does not apply the Universal Credit shared accommodation rate. For attendance allowance the model reads uc_receives_attendance_allowance, which counts armed forces independence payment. The model has no variables for the three Scottish heads of paragraph 29(5). values: 2013-04-29: - uc_receives_attendance_allowance diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml index c501a3c88b..9061829ce1 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml @@ -1,4 +1,4 @@ -description: Benefits that the Secretary of State counts as attendance allowance for Universal Credit, heads (a) and (f) of the definition in regulation 2 of the Universal Credit Regulations 2013. The model holds no separate amount for heads (b), (d) and (e), so the list leaves them out. +description: Benefits that the Secretary of State counts as attendance allowance for Universal Credit, heads (a) and (f) of the definition in regulation 2 of the Universal Credit Regulations 2013. The model holds no separate amount for heads (b), (d) and (e), so it leaves them out. The Northern Ireland regulations letter the same payments (a) to (e), with armed forces independence payment at (e). values: 2013-04-29: - attendance_allowance diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml index 33e1e10583..811a68be19 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml @@ -4,7 +4,7 @@ # Scheme 1983 payment or an analogous payment; (e) an attendance addition to a # war disablement pension; and (f) armed forces independence payment. The # Secretary of State omitted head (c) in 2013 (S.I. 2013/1508). Any amount of -# AFIP in payment qualifies, so the cases use illustrative amounts. +# AFIP in payment qualifies: £9,000 stands for any positive amount. - name: Case 1, attendance allowance at the higher rate counts under head (a). period: 2026 input: @@ -35,7 +35,7 @@ output: uc_receives_attendance_allowance: false -- name: Case 5, the DLA care component is not attendance allowance. +- name: Case 5, the DLA care component does not count as attendance allowance. period: 2026 input: dla_sc_category: HIGHER @@ -43,21 +43,21 @@ dla_sc_middle_plus: true uc_receives_attendance_allowance: false -- name: Case 6, the PIP daily living component is not attendance allowance. +- name: Case 6, the PIP daily living component does not count as attendance allowance. period: 2026 input: pip_dl_category: ENHANCED output: uc_receives_attendance_allowance: false -- name: Case 7, disablement pension without a constant attendance increase is not attendance allowance. +- name: Case 7, disablement pension without a constant attendance increase does not count as attendance allowance. period: 2026 input: iidb_reported: 5_000 output: uc_receives_attendance_allowance: false -- name: Case 8, Armed Forces Compensation Scheme payments and a war disablement pension without an attendance addition are not attendance allowance. +- name: Case 8, a reported Armed Forces Compensation Scheme amount does not count as attendance allowance. period: 2026 input: afcs_reported: 5_000 diff --git a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml index 15cc29ea90..0d807f9fbd 100644 --- a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml +++ b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml @@ -10,7 +10,7 @@ # The renter has no earnings or capital and pays the council 6,000 a year, so # UC = 5,098.80 + 6,000 - contributions. # The non-dependant's own family does not claim UC. Any amount of AFIP in -# payment qualifies, so the cases use illustrative amounts. +# payment qualifies: £9,000 stands for any positive amount. - name: Case 1, a non-dependant with no qualifying benefit bears a contribution. period: 2026 @@ -267,3 +267,60 @@ uc_non_dep_deductions: [2_317.2, 0] uc_housing_costs_element: [3_682.8, 0] universal_credit: [8_781.6, 0] + +- name: Case 8, a non-dependant with a penny of armed forces independence payment bears none. + period: 2026 + absolute_error_margin: 0.02 + input: + people: + renter: + age: 40 + is_household_head: true + is_claimant_or_partner: true + non_dependant: + age: 25 + is_claimant_or_partner: true + employment_income: 20_000 + armed_forces_independence_payment: 0.01 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + non_dependant_family: + members: [non_dependant] + would_claim_uc: false + claims_all_entitled_benefits: false + households: + household: + members: [renter, non_dependant] + rent: 6_000 + tenure_type: RENT_FROM_COUNCIL + region: NORTH_WEST + output: + uc_receives_attendance_allowance: [false, true] + uc_non_dep_deduction_exempt: [false, true] + uc_non_dep_deductions: [0, 0] + universal_credit: [11_098.8, 0] + +- name: Case 9, a private renter aged 30 with a penny of armed forces independence payment keeps the full LHA rate. + period: 2026 + input: + people: + renter: + age: 30 + is_household_head: true + armed_forces_independence_payment: 0.01 + benunits: + renter_family: + members: [renter] + would_claim_uc: true + claims_all_entitled_benefits: false + households: + household: + members: [renter] + rent: 9_000 + tenure_type: RENT_PRIVATELY + region: NORTH_WEST + output: + is_lha_shared_accommodation_rate_specified_renter: false diff --git a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py index a1ae066810..1226ccb330 100644 --- a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -22,6 +22,8 @@ element rises by no more than the contributions fall (under para 14(3), DWP reduces the element to nil and no further), and Universal Credit rises by no more than the element. +4. One non-dependant's AFIP changes only that person's own attendance + allowance result, exemption and contribution. """ import numpy as np @@ -34,8 +36,8 @@ pytestmark = pytest.mark.usefixtures("cloned_uk_tax_benefit_system") YEAR = 2026 -# Fixed slots, so one simulation serves every example. Each slot's tenure and -# region are fixed; the generated inputs change from example to example. +# Fixed slots, so one simulation serves every example. Each slot keeps its tenure +# and region; the generated inputs change from example to example. TENURES = ["RENT_FROM_COUNCIL", "RENT_FROM_HA", "RENT_PRIVATELY"] REGIONS = ["NORTH_WEST", "LONDON", "SCOTLAND", "WALES"] SLOTS = 12 @@ -320,3 +322,89 @@ def test_shared_accommodation_exceptions_match_the_previous_list( sim.drop_computed_arrays() for name in RENTER_OUTPUTS: np.testing.assert_array_equal(current[name], previous[name], err_msg=name) + + +# Under Sch 4 para 16(1), DWP deducts no contribution "in respect of any +# non-dependant" to whom para 16(2) applies, so one person's AFIP opens the +# exemption for that person alone. Each slot holds a renter and a non-dependant +# couple in one household, which separates the person from the benefit unit and +# from the household. +PERSON_OUTPUTS = [ + "uc_receives_attendance_allowance", + "uc_non_dep_deduction_exempt", + "uc_individual_non_dep_deduction", +] +PEOPLE_PER_SLOT = 3 + + +def non_dependant_couples(): + people, benunits, households = {}, {}, {} + for i in range(SLOTS): + renter, first, second = f"renter_{i}", f"first_{i}", f"second_{i}" + for name in (renter, first, second): + people[name] = { + "age": {YEAR: 40 if name == renter else 25}, + "is_household_head": {YEAR: name == renter}, + "is_claimant_or_partner": {YEAR: True}, + "employment_income": {YEAR: 0}, + "armed_forces_independence_payment": {YEAR: 0}, + } + benunits[f"renter_family_{i}"] = { + "members": [renter], + "would_claim_uc": {YEAR: True}, + "claims_all_entitled_benefits": {YEAR: False}, + } + benunits[f"non_dependant_family_{i}"] = { + "members": [first, second], + "would_claim_uc": {YEAR: False}, + "claims_all_entitled_benefits": {YEAR: False}, + } + households[f"household_{i}"] = { + "members": [renter, first, second], + "rent": {YEAR: 6_000}, + "tenure_type": {YEAR: TENURES[i % len(TENURES)]}, + "region": {YEAR: REGIONS[i % len(REGIONS)]}, + } + return {"people": people, "benunits": benunits, "households": households} + + +@pytest.fixture(scope="module") +def couple_simulation(cloned_uk_tax_benefit_system): + return Simulation(situation=non_dependant_couples()) + + +def person_outputs(sim, afip): + sim.set_input("armed_forces_independence_payment", YEAR, afip) + sim.drop_computed_arrays() + return {name: np.asarray(sim.calculate(name, YEAR)) for name in PERSON_OUTPUTS} + + +@PROPERTY_SETTINGS +@given( + earnings=st.lists( + st.one_of(st.just(0.0), money), + min_size=PEOPLE_PER_SLOT * SLOTS, + max_size=PEOPLE_PER_SLOT * SLOTS, + ), + # 1 is the couple's first member and 2 the second; the renter takes no AFIP. + recipients=st.lists(st.integers(1, 2), min_size=SLOTS, max_size=SLOTS), + afip=positive_afip, +) +def test_one_persons_afip_moves_only_their_own_exemption( + couple_simulation, earnings, recipients, afip +): + sim = couple_simulation + sim.set_input("employment_income", YEAR, np.array(earnings)) + before = person_outputs(sim, np.zeros(PEOPLE_PER_SLOT * SLOTS)) + amounts = np.zeros(PEOPLE_PER_SLOT * SLOTS) + amounts[PEOPLE_PER_SLOT * np.arange(SLOTS) + np.array(recipients)] = afip + after = person_outputs(sim, amounts) + + received = amounts > 0 + assert after["uc_receives_attendance_allowance"][received].all() + assert after["uc_non_dep_deduction_exempt"][received].all() + np.testing.assert_array_equal(after["uc_individual_non_dep_deduction"][received], 0) + for name in PERSON_OUTPUTS: + np.testing.assert_array_equal( + after[name][~received], before[name][~received], err_msg=name + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py index 54598b0f3c..6c041655d4 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py @@ -7,14 +7,14 @@ class uc_non_dep_deduction_exempt(Variable): label = "Exempt from the Universal Credit non-dependent housing costs contributions deduction" definition_period = YEAR documentation = ( - "No housing costs contribution is deducted for a non-dependant who is " - "receiving Pension Credit, the middle or higher rate of the DLA care " - "component, the PIP daily living component or attendance allowance, " - "which the Secretary of State defines for Universal Credit to include " + "DWP deducts no housing cost contribution for a non-dependant who " + "receives Pension Credit, the middle or higher rate of the DLA care " + "component, the PIP daily living component, attendance allowance, " + "Carer's Allowance or Carer Support Payment, or who is responsible " + "for a child under the child responsibility age limit. For Universal " + "Credit the Secretary of State defines attendance allowance to include " "armed forces independence payment (see " - "uc_receives_attendance_allowance), or " - "who receives Carer's Allowance or Carer Support Payment, or who is " - "responsible for a child under the child responsibility age limit. " + "uc_receives_attendance_allowance). " "Benefit-unit membership represents the child's normal residence " "with the claimant or partner; the existing Universal Credit child " "age measure excludes children looked after by a local authority. Underlying " @@ -41,8 +41,7 @@ def formula(person, period, parameters): person.benunit("uc_non_dep_receives_pension_credit", period) | person("dla_sc_middle_plus", period) | (person("pip_dl", period) > 0) - # Para 16(2)(d), reading "attendance allowance" with its reg 2 - # definition. + # Para 16(2)(d) uses "attendance allowance" as reg 2 defines it. | person("uc_receives_attendance_allowance", period) | person("receives_carer_benefit", period) | responsible_for_young_child diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py index 277d1c252a..d1d4dbcb42 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py @@ -8,11 +8,11 @@ class uc_receives_attendance_allowance(Variable): documentation = ( "Whether the person receives attendance allowance as the Secretary of " "State defines it in regulation 2 of the Universal Credit Regulations " - "2013. The definition lists five heads: (a) attendance allowance under " - "section 64 of the Social Security Contributions and Benefits Act " - "1992; (b) an increase of disablement pension under section 104 or 105 " - "of that Act, for constant attendance or exceptionally severe " - "disablement; (d) a payment under article 14, 15, 16, 43 or 44 " + "2013. There the Secretary of State lists five heads: (a) attendance " + "allowance under section 64 of the Social Security Contributions and " + "Benefits Act 1992; (b) an increase of disablement pension under section " + "104 or 105 of that Act, for constant attendance or exceptionally " + "severe disablement; (d) a payment under article 14, 15, 16, 43 or 44 " "of the Personal Injuries (Civilians) Scheme 1983, or any analogous " "payment; (e) any payment for the need for attendance added to a war " "disablement pension; and (f) armed forces independence payment. The " @@ -20,11 +20,12 @@ class uc_receives_attendance_allowance(Variable): "model counts heads (a) and (f), from " "gov.dwp.universal_credit.attendance_allowance_benefits. It holds no " "separate amount for the other three, so it does not count a person " - "who receives only one of them: iidb holds one reported amount with no " + "who receives only one of them. iidb holds one reported amount with no " "constant attendance or exceptionally severe disablement increase of " - "its own, afcs holds war disablement pensions without any attendance " - "addition, and no variable holds Personal Injuries (Civilians) Scheme " - "payments." + "its own. afcs holds one reported amount for war disablement pensions " + "and Armed Forces Compensation Scheme payments, with no separate " + "attendance addition. No variable holds Personal Injuries (Civilians) " + "Scheme payments." ) definition_period = YEAR reference = ( From 9079310d6a6e3baa5f5f987fc1bc9ec550e156ca Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Mon, 2 Nov 2026 03:56:13 -0500 Subject: [PATCH 6/6] Pin the renter's total and tighten the attendance allowance wording The per-person property now checks that the renter's total falls by the recipient's contribution and no more, and the docstring limits invariant 4 to where AFIP starts no Pension Credit. Co-Authored-By: Claude Opus 5.5 --- ...ared_accommodation_exception_benefits.yaml | 2 +- .../attendance_allowance_benefits.yaml | 2 +- .../uc_afip_housing_contribution.yaml | 2 +- ...uc_afip_housing_contribution_properties.py | 34 +++++++++++++------ .../uc_non_dep_deduction_exempt.py | 2 +- 5 files changed, 28 insertions(+), 14 deletions(-) diff --git a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml index 64fd56ccfc..5816e4c75f 100644 --- a/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/LHA/shared_accommodation_exception_benefits.yaml @@ -1,4 +1,4 @@ -description: Disability benefits whose receipt makes a renter under 35 an excepted person, so DWP does not apply the Universal Credit shared accommodation rate. For attendance allowance the model reads uc_receives_attendance_allowance, which counts armed forces independence payment. The model has no variables for the three Scottish heads of paragraph 29(5). +description: Disability benefits for which DWP treats a renter under 35 as an excepted person and does not apply the Universal Credit shared accommodation rate. For attendance allowance the model reads uc_receives_attendance_allowance, which counts armed forces independence payment. The model has no variables for the three Scottish heads of paragraph 29(5). values: 2013-04-29: - uc_receives_attendance_allowance diff --git a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml index 9061829ce1..95a8c1101b 100644 --- a/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml @@ -1,4 +1,4 @@ -description: Benefits that the Secretary of State counts as attendance allowance for Universal Credit, heads (a) and (f) of the definition in regulation 2 of the Universal Credit Regulations 2013. The model holds no separate amount for heads (b), (d) and (e), so it leaves them out. The Northern Ireland regulations letter the same payments (a) to (e), with armed forces independence payment at (e). +description: Benefits that the Secretary of State counts as attendance allowance for Universal Credit, heads (a) and (f) of the definition in regulation 2 of the Universal Credit Regulations 2013. The model holds no separate amount for heads (b), (d) and (e), so it leaves them out. In the Northern Ireland regulations the Secretary of State letters the same payments (a) to (e), with armed forces independence payment at (e). values: 2013-04-29: - attendance_allowance diff --git a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml index 0d807f9fbd..e7394ffa8a 100644 --- a/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml +++ b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml @@ -303,7 +303,7 @@ uc_non_dep_deductions: [0, 0] universal_credit: [11_098.8, 0] -- name: Case 9, a private renter aged 30 with a penny of armed forces independence payment keeps the full LHA rate. +- name: Case 9, DWP excepts a private renter aged 30 with a penny of armed forces independence payment from the shared accommodation rate. period: 2026 input: people: diff --git a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py index 1226ccb330..035ab68e42 100644 --- a/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -9,10 +9,11 @@ income in reg 66, and the model leaves AFIP out of the renter's unearned income. -Each generated household has a renter, who claims Universal Credit, and a -non-dependant in a family of their own that does not claim. The tests -calculate the same households with no AFIP, then with a positive AFIP for the -non-dependant, the renter or both. Invariants: +Each generated household has a renter, who claims Universal Credit, and one +non-dependant (a non-dependant couple in the last test) in a family of their +own that does not claim. The tests calculate each household with no AFIP and +then with a positive AFIP: for the non-dependant, the renter or both, or in +the last test for one member of the couple. Invariants: 1. Raising AFIP from nil to any positive amount never lowers Universal Credit and never raises the housing cost contributions. @@ -22,8 +23,9 @@ element rises by no more than the contributions fall (under para 14(3), DWP reduces the element to nil and no further), and Universal Credit rises by no more than the element. -4. One non-dependant's AFIP changes only that person's own attendance - allowance result, exemption and contribution. +4. One non-dependant's AFIP changes no other person's attendance allowance + result, exemption or contribution, unless it starts Pension Credit for + their benefit unit (para 16(2)(b)). """ import numpy as np @@ -325,10 +327,10 @@ def test_shared_accommodation_exceptions_match_the_previous_list( # Under Sch 4 para 16(1), DWP deducts no contribution "in respect of any -# non-dependant" to whom para 16(2) applies, so one person's AFIP opens the -# exemption for that person alone. Each slot holds a renter and a non-dependant -# couple in one household, which separates the person from the benefit unit and -# from the household. +# non-dependant" to whom para 16(2) applies, so DWP exempts only the person who +# receives AFIP. Each slot puts a renter and a non-dependant couple in one +# household, so an exemption widened to the benefit unit or the household would +# reach the recipient's partner. PERSON_OUTPUTS = [ "uc_receives_attendance_allowance", "uc_non_dep_deduction_exempt", @@ -379,6 +381,11 @@ def person_outputs(sim, afip): return {name: np.asarray(sim.calculate(name, YEAR)) for name in PERSON_OUTPUTS} +def renter_totals(sim): + # Benefit units alternate: the renter's family, then the couple's. + return np.asarray(sim.calculate("uc_non_dep_deductions", YEAR))[::2] + + @PROPERTY_SETTINGS @given( earnings=st.lists( @@ -396,6 +403,7 @@ def test_one_persons_afip_moves_only_their_own_exemption( sim = couple_simulation sim.set_input("employment_income", YEAR, np.array(earnings)) before = person_outputs(sim, np.zeros(PEOPLE_PER_SLOT * SLOTS)) + total_before = renter_totals(sim) amounts = np.zeros(PEOPLE_PER_SLOT * SLOTS) amounts[PEOPLE_PER_SLOT * np.arange(SLOTS) + np.array(recipients)] = afip after = person_outputs(sim, amounts) @@ -408,3 +416,9 @@ def test_one_persons_afip_moves_only_their_own_exemption( np.testing.assert_array_equal( after[name][~received], before[name][~received], err_msg=name ) + # The renter's total falls by the recipient's contribution and no more. + np.testing.assert_allclose( + total_before - renter_totals(sim), + before["uc_individual_non_dep_deduction"][received], + atol=0.01, + ) diff --git a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py index 6c041655d4..ec6430b768 100644 --- a/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py +++ b/policyengine_uk/variables/gov/dwp/universal_credit/housing_costs_element/non_dep_deduction/uc_non_dep_deduction_exempt.py @@ -8,7 +8,7 @@ class uc_non_dep_deduction_exempt(Variable): definition_period = YEAR documentation = ( "DWP deducts no housing cost contribution for a non-dependant who " - "receives Pension Credit, the middle or higher rate of the DLA care " + "receives Pension Credit, the middle or highest rate of the DLA care " "component, the PIP daily living component, attendance allowance, " "Carer's Allowance or Carer Support Payment, or who is responsible " "for a child under the child responsibility age limit. For Universal "