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..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,10 +1,9 @@ -description: Disability benefits whose recipients under 35 are excepted from the Universal Credit shared accommodation rate. +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: - - 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..95a8c1101b --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/universal_credit/attendance_allowance_benefits.yaml @@ -0,0 +1,14 @@ +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 + - 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..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 @@ -51,3 +51,34 @@ is_entitled_to_carer_benefit: false is_carer_for_benefits: true uc_non_dep_deduction_exempt: false + +# 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 + 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..811a68be19 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/universal_credit/uc_receives_attendance_allowance.yaml @@ -0,0 +1,83 @@ +# 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: £9,000 stands for any positive amount. +- 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 does not count as 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 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 does not count as attendance allowance. + period: 2026 + input: + iidb_reported: 5_000 + output: + uc_receives_attendance_allowance: false + +- name: Case 8, a reported Armed Forces Compensation Scheme amount does not count as 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..e7394ffa8a --- /dev/null +++ b/policyengine_uk/tests/policy/integration/uc_afip_housing_contribution.yaml @@ -0,0 +1,326 @@ +# 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. Any amount of AFIP in +# payment qualifies: £9,000 stands for any positive amount. + +- 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] + +- 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, DWP excepts a private renter aged 30 with a penny of armed forces independence payment from the shared accommodation 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 new file mode 100644 index 0000000000..035ab68e42 --- /dev/null +++ b/policyengine_uk/tests/test_uc_afip_housing_contribution_properties.py @@ -0,0 +1,424 @@ +"""Property-based tests: armed forces independence payment (AFIP) counts as +attendance allowance in the Universal Credit housing cost contribution rules. + +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 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. +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 + by no more than the element. +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 +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 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 +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) +# 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 +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() + + +# 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", + "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) + + +# Under Sch 4 para 16(1), DWP deducts no contribution "in respect of any +# 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", + "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} + + +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( + 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)) + 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) + + 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 + ) + # 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 be35b6197f..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 @@ -7,18 +7,24 @@ 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, or " - "who receives Carer's Allowance or Carer Support Payment, or who is " - "responsible for a child under the child responsibility age limit. " + "DWP deducts no housing cost contribution for a non-dependant who " + "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 " + "Credit the Secretary of State defines attendance allowance to include " + "armed forces independence payment (see " + "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 " "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 +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) - | (person("attendance_allowance", period) > 0) + # 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 new file mode 100644 index 0000000000..d1d4dbcb42 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/universal_credit/uc_receives_attendance_allowance.py @@ -0,0 +1,38 @@ +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 the Secretary of " + "State defines it in regulation 2 of the Universal Credit Regulations " + "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 " + "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 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 = ( + "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