From a66b990b52a468d2182d3154cbcbf8da66982b1a Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Fri, 9 Oct 2026 04:41:51 +0400 Subject: [PATCH 1/4] Add pre-assessed Housing Benefit permitted-work disregard --- changelog.d/hb-permitted-work.added.md | 1 + .../housing_benefit_permitted_work.yaml | 533 ++++++++++++++++++ ...ing_benefit_applicable_income_disregard.py | 23 +- ...ousing_benefit_permitted_work_disregard.py | 29 + 4 files changed, 583 insertions(+), 3 deletions(-) create mode 100644 changelog.d/hb-permitted-work.added.md create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_permitted_work_disregard.py diff --git a/changelog.d/hb-permitted-work.added.md b/changelog.d/hb-permitted-work.added.md new file mode 100644 index 0000000000..1af493ef74 --- /dev/null +++ b/changelog.d/hb-permitted-work.added.md @@ -0,0 +1 @@ +Added a pre-assessed Housing Benefit permitted-work earnings disregard with lone-parent precedence and separate additional-disregard handling. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml new file mode 100644 index 0000000000..9acc44b92a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml @@ -0,0 +1,533 @@ +- name: Permitted work replaces rather than maximises the special standard amount + period: 2024 + input: + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 500 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + is_lone_parent: false + is_couple: false + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 500 + +- name: ENGLAND pension regime false ordinary fallback + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 260 + +- name: ENGLAND pension regime false replacement not stacking + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: ENGLAND pension regime false lone parent precedence + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 1040 + is_lone_parent: true + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 1300 + +- name: ENGLAND pension regime false net earnings cap + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 500 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 500 + +- name: ENGLAND pension regime false additional still applies + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: ENGLAND pension regime false additional earnings boundary + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 2889.19 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: ENGLAND pension regime false additional exact equality + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 2889.2 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: ENGLAND pension regime true ordinary fallback + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 260 + +- name: ENGLAND pension regime true replacement not stacking + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: ENGLAND pension regime true lone parent precedence + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 1040 + is_lone_parent: true + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 1300 + +- name: ENGLAND pension regime true net earnings cap + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 500 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 500 + +- name: ENGLAND pension regime true additional still applies + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: ENGLAND pension regime true additional earnings boundary + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 2889.19 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: ENGLAND pension regime true additional exact equality + period: 2024 + absolute_error_margin: 0.01 + input: + country: ENGLAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 2889.2 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: NORTHERN_IRELAND pension regime false ordinary fallback + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 260 + +- name: NORTHERN_IRELAND pension regime false replacement not stacking + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: NORTHERN_IRELAND pension regime false lone parent precedence + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 1040 + is_lone_parent: true + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 1300 + +- name: NORTHERN_IRELAND pension regime false net earnings cap + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 500 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 500 + +- name: NORTHERN_IRELAND pension regime false additional still applies + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: NORTHERN_IRELAND pension regime false additional earnings boundary + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 2889.19 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: NORTHERN_IRELAND pension regime false additional exact equality + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: false + housing_benefit_net_earnings: 2889.2 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: NORTHERN_IRELAND pension regime true ordinary fallback + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 260 + +- name: NORTHERN_IRELAND pension regime true replacement not stacking + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: NORTHERN_IRELAND pension regime true lone parent precedence + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 1040 + is_lone_parent: true + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 1300 + +- name: NORTHERN_IRELAND pension regime true net earnings cap + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 500 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 500 + +- name: NORTHERN_IRELAND pension regime true additional still applies + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: NORTHERN_IRELAND pension regime true additional earnings boundary + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 2889.19 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2000 + +- name: NORTHERN_IRELAND pension regime true additional exact equality + period: 2024 + absolute_error_margin: 0.01 + input: + country: NORTHERN_IRELAND + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 2889.2 + housing_benefit_permitted_work_disregard: 2000 + is_lone_parent: false + is_couple: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2889.2 + +- name: Permitted work stacks only with separate accommodation amount + period: 2027 + input: + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + housing_benefit_specified_or_temporary_accommodation_disregard: 1000 + housing_benefit_applicable_income_childcare_element: 0 + is_lone_parent: false + is_couple: true + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 3000 + +- name: A reform of the lone parent amount retains precedence + period: 2024 + input: + gov.dwp.housing_benefit.means_test.income_disregard.lone_parent: 50 + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + is_lone_parent: true + is_couple: false + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + output: + housing_benefit_applicable_income_disregard: 2600 + +- name: Passport still disregards all earnings + period: 2024 + input: + housing_benefit_net_earnings: 5000 + housing_benefit_permitted_work_disregard: 2000 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + is_lone_parent: false + is_couple: false + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: true + output: + housing_benefit_applicable_income_disregard: 5000 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py index 7de5c94a3d..8b12285381 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py @@ -22,9 +22,10 @@ class housing_benefit_applicable_income_disregard(Variable): "(SI 2006/213 reg 5(1)(b)). The " "£20 standard disregard uses established qualifying routes or an " "explicit full-amount pre-assessment, without reconstructing partial " - "carer/occupation earnings allocations. The permitted-work " - "disregard (Sch 4 para 10A), which replaces paras 3 to 10 but not " - "para 18, is not yet modelled here." + "carer/occupation earnings allocations. A pre-assessed permitted-work amount " + "(housing_benefit_permitted_work_disregard) replaces the ordinary " + "standard amount, retaining a higher lone-parent disregard. It does " + "not replace the separate accommodation or additional disregard." ) definition_period = YEAR unit = GBP @@ -37,6 +38,10 @@ class housing_benefit_applicable_income_disregard(Variable): "https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12", "https://www.legislation.gov.uk/uksi/2026/978/regulation/2", "https://www.legislation.gov.uk/nisr/2026/157/regulation/2", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/10A", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/4/paragraph/5A", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/10A", + "https://www.legislation.gov.uk/nisr/2006/406/schedule/5/paragraph/5A", ) def formula(benunit, period, parameters): @@ -44,6 +49,18 @@ def formula(benunit, period, parameters): net_earnings = benunit("housing_benefit_net_earnings", period) # Working age Sch 4 paras 4, 7 and 10; pension age Sch 4 paras 2 and 7. standard = benunit("housing_benefit_special_earnings_disregard", period) + # Working-age para 10A(2) / pension-age para 5A(2): replace the + # ordinary amount, except for a higher lone-parent amount. The input + # already applies the statutory work and partner-allocation limits. + permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) + standard = where( + permitted > 0, + min_( + net_earnings, + max_(permitted, benunit("is_lone_parent", period) * standard), + ), + standard, + ) # Working age Sch 4 para 18 (NI Sch 5 para 18), from 5 October 2026: # specified or temporary accommodation. It applies alongside paras 3 # to 10A, none of which excludes it, and takes the earnings they diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_permitted_work_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_permitted_work_disregard.py new file mode 100644 index 0000000000..11e8d7481c --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_permitted_work_disregard.py @@ -0,0 +1,29 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_permitted_work_disregard(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR + unit = GBP + default_value = 0 + label = "Pre-assessed Housing Benefit permitted-work earnings disregard" + documentation = ( + "Annual amount legally disregarded under the permitted-work provision, " + "before any lone-parent precedence and separate additional or " + "accommodation disregard. The external assessor must establish " + "qualifying benefit/credit status, exempt work, its statutory specified " + "amount, and the earnings available for this deduction, including the " + "partner allocation and limit. This is not gross permitted-work income " + "or an unrestricted exemption of all earnings. Enter zero where the " + "provision does not apply. No dataset mapping has been verified; the " + "zero default retains the ordinary-disregard approximation and may " + "understate qualifying awards. The model caps the supplied amount at " + "net earnings but does not reconstruct permitted-work entitlement." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/10A", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/4/paragraph/5A", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/10A", + "https://www.legislation.gov.uk/nisr/2006/406/schedule/5/paragraph/5A", + ) From a67175429937ed802f7817fb9e772eac06ec0217 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 01:09:52 -0400 Subject: [PATCH 2/4] Pin permitted work against the SDA and blindness routes; restate the standard's docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds what #2225's delta review (section 6) said this PR owes now that #2225 has merged: - GB and NI pension-age YAML regressions where the special £20 comes from severe disablement allowance or blindness (derived by housing_benefit_special_earnings_disregard_eligible) and a pre-assessed permitted-work amount is above zero. Each pins the selected standard at £1,040 and the applied disregard at £4,160, not £5,200: SI 2006/214 Sch 4 para 5A(2) and SR 2006/406 Sch 5 para 5A(2) exclude para 5. - A lone parent at the permitted-work maximum, England and NI: the £183.50 reg 45(4) limit (16 x £11.44, rounded up) beats para 4's £25, so £9,542 is disregarded, capped at net earnings. - A permitted-work draw in the selected-standard Hypothesis property. It checks the selected standard ignores permitted work, and the applied disregard is min(net, permitted), or for a lone parent min(net, max(permitted, £1,300)), never stacked. - housing_benefit_special_earnings_disregard's documentation now says it is the amount before permitted work, which housing_benefit_applicable_income_disregard replaces. The formula is unchanged. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_permitted_work.yaml | 218 ++++++++++++++++++ ...g_benefit_earnings_disregard_properties.py | 61 ++++- ...sing_benefit_special_earnings_disregard.py | 10 +- 3 files changed, 280 insertions(+), 9 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml index 9acc44b92a..fc286a9b1c 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_permitted_work.yaml @@ -531,3 +531,221 @@ housing_benefit_on_passporting_benefit: true output: housing_benefit_applicable_income_disregard: 5000 + +# The cases below derive special eligibility from severe disablement +# allowance or blindness (housing_benefit_special_earnings_disregard_eligible) +# instead of supplying it, and add a lone parent at the permitted-work +# maximum. Each holds the accommodation, childcare, additional-disregard and +# passport terms at nil, so the output is the standard amount alone. +# +# Specified amount, 2024-25: ESA Regulations 2008 reg 45(4) (NI: SR 2008/280 +# reg 45(4)) and SI 1995/311 reg 17(4) set the limit at 16 x National Minimum +# Wage, rounded up to the next 50p (reg 45(9A), reg 17(10)). The adult rate +# in gov.hmrc.minimum_wage.non_apprentice is £11.44, and +# gov.dwp.ESA.exempt_work.higher_earnings_limit_minimum_wage_hours is 16: +# 16 x £11.44 = £183.04, rounded up to £183.50 a week. +# +# Couples: A does that exempt work for £60 a week, under 16 hours; B, the +# other member, earns £100 a week from other work and does no exempt work. +# Pension-age para 5A(4) (working-age para 10A(4)): A's £60 is under £183.50, +# so B's earnings are also disregarded up to £183.50 - £60 = £123.50, but at +# most £20. Pre-assessed permitted-work amount: (£60 + £20) x 52 = £4,160. +# Net earnings: (£60 + £100) x 52 = £8,320. +# +# Special amount before permitted work: £20 x 52 = £1,040, in place of the +# couple's £10. Para 5A(2): "paragraphs 1 to 5 and 7 do not apply", so the +# disregard is £4,160, not £4,160 + £1,040 = £5,200 and not £1,040. + +# SI 2006/214 Sch 4. The claimant (A, 70) receives severe disablement +# allowance (para 5A(5)(c)) and does exempt work under SI 1995/311 reg 17(4) +# (para 5A(6)(b)). The SDA qualifies the couple for £20 (para 5(1)(a)(ii)). +- name: ENGLAND pension-age SDA recipient's permitted work replaces the special £20 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + sda: 100 + partner: + age: 68 + is_claimant_or_partner: true + sda: 0 + benunits: + couple: + members: [claimant, partner] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 8320 + housing_benefit_permitted_work_disregard: 4160 + carer_premium: 0 + working_tax_credit: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + is_couple: true + is_lone_parent: false + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_special_earnings_disregard: 1040 + housing_benefit_applicable_income_disregard: 4160 + +# SR 2006/406 Sch 5, the same facts: SDA (para 5A(5)(c)) and the special +# £20 (para 5(1)(a)(ii)). The NI exempt work is under SR 1995/41 reg 17(4) +# (para 5A(6)(b)). Its revised text on legislation.gov.uk still lists +# unapplied amendments, so the £183.50 limit was not checked against that +# text; the £4,160 input is the pre-assessed amount the case supplies. +- name: NORTHERN_IRELAND pension-age SDA recipient's permitted work replaces the special £20 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + sda: 100 + partner: + age: 68 + is_claimant_or_partner: true + sda: 0 + benunits: + couple: + members: [claimant, partner] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 8320 + housing_benefit_permitted_work_disregard: 4160 + carer_premium: 0 + working_tax_credit: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + households: + household: + members: [claimant, partner] + country: NORTHERN_IRELAND + output: + is_couple: true + is_lone_parent: false + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_special_earnings_disregard: 1040 + housing_benefit_applicable_income_disregard: 4160 + +# SI 2006/214 Sch 4. The claimant (B, 70) is certified blind, which +# qualifies the couple for £20 (para 5(1)(b)), and earns £100 a week. The +# partner (A, 63, under State Pension age) receives contributory ESA (para +# 5A(5)(a)) and does exempt work under ESA Regulations 2008 reg 45(4) (para +# 5A(6)(a)). Neither receives SDA, so blindness is the only special route. +- name: ENGLAND pension-age blind claimant with a partner in permitted work has no stacked £20 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + is_blind: true + sda: 0 + partner: + age: 63 + is_claimant_or_partner: true + is_blind: false + sda: 0 + benunits: + couple: + members: [claimant, partner] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 8320 + housing_benefit_permitted_work_disregard: 4160 + carer_premium: 0 + working_tax_credit: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + is_couple: true + is_lone_parent: false + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_special_earnings_disregard: 1040 + housing_benefit_applicable_income_disregard: 4160 + +# SR 2006/406 Sch 5, the same facts: blindness (para 5(1)(b)); the partner's +# contributory ESA (para 5A(5)(a)) and exempt work under SR 2008/280 reg +# 45(4) (para 5A(6)(a)), whose limit is also 16 x National Minimum Wage, +# rounded up under its reg 45(9A). +- name: NORTHERN_IRELAND pension-age blind claimant with a partner in permitted work has no stacked £20 + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + is_blind: true + sda: 0 + partner: + age: 63 + is_claimant_or_partner: true + is_blind: false + sda: 0 + benunits: + couple: + members: [claimant, partner] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 8320 + housing_benefit_permitted_work_disregard: 4160 + carer_premium: 0 + working_tax_credit: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + households: + household: + members: [claimant, partner] + country: NORTHERN_IRELAND + output: + is_couple: true + is_lone_parent: false + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_special_earnings_disregard: 1040 + housing_benefit_applicable_income_disregard: 4160 + +# Lone parent at the permitted-work maximum, working age: SI 2006/213 Sch 4 +# (England) and SR 2006/405 Sch 5 (NI). The lone parent receives +# contributory ESA (para 10A(5)(a)) and earns only from exempt work under +# reg 45(4), at its £183.50 limit: the specified amount is £183.50 x 52 = +# £9,542 (para 10A(7)). Para 10A(2): £183.50 is not less than para 4's £25, +# so para 4 does not apply instead, and the selected £25 x 52 = £1,300 gives +# way to the permitted-work amount whether or not the family is otherwise +# special-eligible. Earning exactly the limit, all £9,542 is disregarded +# (columns 1 and 3). Where net earnings are £9,282 (£178.50 a week), the +# disregard is capped at them (columns 2 and 4). +- name: Lone parent at the permitted-work maximum has the specified amount, capped at net earnings + period: 2024 + absolute_error_margin: 0.01 + input: + country: [ENGLAND, ENGLAND, NORTHERN_IRELAND, NORTHERN_IRELAND] + housing_benefit_pension_age_regulations_apply: [false, false, false, false] + housing_benefit_special_earnings_disregard_eligible: [true, false, true, false] + housing_benefit_net_earnings: [9542, 9282, 9542, 9282] + housing_benefit_permitted_work_disregard: [9542, 9542, 9542, 9542] + is_lone_parent: [true, true, true, true] + is_couple: [false, false, false, false] + housing_benefit_specified_or_temporary_accommodation_disregard: [0, 0, 0, 0] + housing_benefit_applicable_income_childcare_element: [0, 0, 0, 0] + meets_housing_benefit_additional_earnings_disregard_conditions: [false, false, false, false] + housing_benefit_on_passporting_benefit: [false, false, false, false] + output: + housing_benefit_special_earnings_disregard: [1300, 1300, 1300, 1300] + housing_benefit_applicable_income_disregard: [9542, 9282, 9542, 9282] diff --git a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py index ce2b185f9e..34a3c4a973 100644 --- a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py @@ -45,7 +45,13 @@ 8. The selected standard is min(net earnings, 52 times the statutory weekly choice), with £25 lone-parent precedence, then £20 special, then £10 couple, then £5 single. These amounts never stack and the selected - standard stays between zero and net earnings. + standard stays between zero and net earnings. It is the amount before + permitted work, whatever the permitted-work input. With a pre-assessed + permitted-work amount above zero, the standard actually deducted is + min(net earnings, the permitted-work amount), or for a lone parent + min(net earnings, the larger of that amount and £25 x 52): the + permitted-work amount replaces the £20, £10 and £5 (SI 2006/213 Sch 4 + para 10A(2), SI 2006/214 Sch 4 para 5A(2)) and is never added to them. Para 12 is tested on its own: with Income Support, every family has all its net earnings disregarded, at working or pension age. Leaving Income Support can @@ -90,6 +96,14 @@ st.floats(0, 60_000, allow_nan=False), ) LOW_EARNINGS = st.one_of(st.just(0.0), st.floats(0, 12_000, allow_nan=False)) +# A pre-assessed annual permitted-work disregard: none; 52 weeks at the £20 +# exempt-work limit or at 2024-25's £183.50 limit; amounts either side of 52 +# weeks of the lone parent's £25; or any amount in pence up to £20,000. +PERMITTED_WORK = st.one_of( + st.just(0.0), + st.sampled_from([1040.0, 1299.99, 1300.0, 1300.01, 9542.0]), + st.integers(0, 2_000_000).map(lambda pennies: pennies / 100), +) DISREGARD = system.parameters(YEAR).gov.dwp.housing_benefit.means_test.income_disregard MAX_WEEKLY = max(DISREGARD.single, DISREGARD.couple, DISREGARD.lone_parent) MAX_ANNUAL = (MAX_WEEKLY + DISREGARD.worker) * WEEKS @@ -325,6 +339,7 @@ def pension_age_map(age): st.sampled_from(sorted(SHAPES)), st.integers(0, 100_000_000).map(lambda pennies: pennies / 100), st.booleans(), + PERMITTED_WORK, ), min_size=1, max_size=30, @@ -332,17 +347,18 @@ def pension_age_map(age): ) @example( cases=[ - (shape, net, eligible) + (shape, net, eligible, permitted) for shape in SHAPES - for net in (0, 259.99, 260, 500, 520, 1039.99, 1040, 1300, 2000) + for net in (0, 259.99, 260, 500, 520, 1039.99, 1040, 1300, 2000, 9542) for eligible in (False, True) + for permitted in (0, 500, 1040, 1299.99, 1300, 1300.01, 9542) ] ) def test_selected_standard_has_statutory_precedence_and_is_capped_at_net_earnings( cases, ): units = [] - for shape, _, _ in cases: + for shape, _, _, _ in cases: n_adults, n_children = SHAPES[shape] units.append( dict( @@ -370,9 +386,26 @@ def test_selected_standard_has_statutory_precedence_and_is_capped_at_net_earning YEAR, [case[2] for case in cases], ) + sim.set_input( + "housing_benefit_permitted_work_disregard", YEAR, [case[3] for case in cases] + ) + # Hold every other term at nil, so the applied disregard is the standard + # amount after the permitted-work replacement and nothing else. + for variable, value in ( + ("housing_benefit_specified_or_temporary_accommodation_disregard", 0.0), + ("housing_benefit_applicable_income_childcare_element", 0.0), + ("meets_housing_benefit_additional_earnings_disregard_conditions", False), + ("housing_benefit_on_passporting_benefit", False), + ): + sim.set_input(variable, YEAR, np.full(len(cases), value)) selected = sim.calculate("housing_benefit_special_earnings_disregard", YEAR) + applied = sim.calculate("housing_benefit_applicable_income_disregard", YEAR) net_earnings = sim.calculate("housing_benefit_net_earnings", YEAR) - for actual, net, (shape, _, eligible) in zip(selected, net_earnings, cases): + permitted_work = sim.calculate("housing_benefit_permitted_work_disregard", YEAR) + for actual, after, net, permitted, (shape, _, eligible, _) in zip( + selected, applied, net_earnings, permitted_work, cases + ): + case = (shape, net, eligible, permitted) if shape == "lone_parent": weekly = 25 elif eligible: @@ -381,8 +414,22 @@ def test_selected_standard_has_statutory_precedence_and_is_capped_at_net_earning weekly = 10 else: weekly = 5 - assert actual == min(net, WEEKS * weekly), (shape, net, eligible) - assert 0 <= actual <= net, (shape, net, eligible) + # The selected standard is the amount before permitted work, whatever + # the permitted-work input. + assert actual == min(net, WEEKS * weekly), case + assert 0 <= actual <= net, case + if permitted > 0: + # Working-age para 10A(2), pension-age para 5A(2): the specified + # amount replaces paras 3 to 10 (pension age: 1 to 5 and 7), the + # £20 included; a lone parent keeps £25 where that is higher. + floor = WEEKS * 25 if shape == "lone_parent" else 0 + expected = min(net, max(permitted, floor)) + else: + expected = min(net, WEEKS * weekly) + assert abs(after - expected) < 0.01, case + assert -0.01 < after <= net + 0.01, case + # Never stacked: at most the larger of the two amounts, never both. + assert after <= max(permitted, WEEKS * weekly) + 0.01, case @PROPERTY_SETTINGS diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py index 04d731487a..b3529ffc83 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py @@ -11,8 +11,14 @@ class housing_benefit_special_earnings_disregard(Variable): "Standard disregard selected before the additional/accommodation " "amounts: lone-parent precedence, otherwise the special amount for " "a qualifying family, otherwise the ordinary couple/single amount. " - "It is a replacement, not £20 added to £5 or £10. The total standard " - "deduction is capped at the claimant's and partner's net earnings." + "It is a replacement, not £20 added to £5 or £10, and it is capped at " + "the claimant's and partner's net earnings. This is the amount before " + "permitted work: where housing_benefit_permitted_work_disregard is " + "above zero, housing_benefit_applicable_income_disregard deducts that " + "amount in place of this one, keeping this amount only for a lone " + "parent for whom it is higher (SI 2006/213 Sch 4 para 10A(2), SI " + "2006/214 Sch 4 para 5A(2), SR 2006/405 Sch 5 para 10A(2) and SR " + "2006/406 Sch 5 para 5A(2))." ) reference = ( "https://www.legislation.gov.uk/uksi/2006/213/schedule/4", From f06c4ea8236a322d5485cfa6f463991fa04877a2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 08:02:03 -0400 Subject: [PATCH 3/4] Fix savings-credit-only Housing Benefit disregard double counting --- .../hb-savings-credit-disregard.fixed.md | 1 + .../housing_benefit_savings_credit_only.yaml | 191 +++++++++ ...fit_savings_credit_disregard_properties.py | 367 ++++++++++++++++++ ...ing_benefit_applicable_income_disregard.py | 16 +- ...fit_applied_standard_earnings_disregard.py | 42 ++ ...sing_benefit_savings_credit_only_income.py | 49 ++- ...sing_benefit_special_earnings_disregard.py | 2 +- 7 files changed, 628 insertions(+), 40 deletions(-) create mode 100644 changelog.d/hb-savings-credit-disregard.fixed.md create mode 100644 policyengine_uk/tests/test_housing_benefit_savings_credit_disregard_properties.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.py diff --git a/changelog.d/hb-savings-credit-disregard.fixed.md b/changelog.d/hb-savings-credit-disregard.fixed.md new file mode 100644 index 0000000000..fad3f07f29 --- /dev/null +++ b/changelog.d/hb-savings-credit-disregard.fixed.md @@ -0,0 +1 @@ +Fixed double counting of Housing Benefit standard earnings disregards in savings-credit-only income, including permitted work and the special £20 route. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml index 4df1327e70..32be86ff62 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml @@ -436,3 +436,194 @@ # 6,240 - 464.2404 = 5,775.7596. Without (a), 6,240 - 0.65 x (13,570 - # 13,312) = 6,240 - 167.70 = 6,072.30. housing_benefit: 5_775.76 + +# Same couple awarded Savings Credit before 6 April 2016 continuously since; +# entitled to PC/HB on 14 May 2019 and continuously entitled to either since. +# Grandchild normally lives here and they are responsible; no tax-credit award. +# Claimant: gross earnings 125/week, tax 25/week, net 100/week; gross private +# pension 14,732.50/year, tax 432.50/year, net pension 275/week. No Class 1 at80. +# Partner: assessment-phase contributory ESA 90.50/week, actual unsupported +# self-employment 16 hours/week, net earnings 10/week. Not main phase, no other +# special-HB route, and no reg7 ESA assessment exception. ESA reg45(2) applies; +# precisely16hours excludes45(4). HB5A amount=10+min(100,20-10,20)=20/week. +# PC SchVI4 already excludes20/week. AIF=275+100+10+90.50-20=455.50/week. +# Child16hour route adds17.10 HB disregard. PC: minimum332.95+76.79=409.74, +# qualifying income365, SavingsCredit19.04-18.30=0.74/week (PCreg7(4)). +# All AIF/net-earnings inputs are external component assessments. +# Expected:23686+38.48-17.10*52=22835.28. Current head incorrectly subtracts +# (37.10-10)*52=1409.20, producing22315.28. +- name: Protected Savings Credit family offsets the existing PC earnings disregard + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + employment_income: 6500 + private_pension_income: 14732.50 + ni_class_1_employee: 0 + partner: + age: 63 + is_claimant_or_partner: true + self_employment_income: 520 + weekly_hours: 16 + ni_class_1_employee: 0 + esa_contrib: 4706 + grandchild: + age: 10 + is_claimant_or_partner: false + benunits: + couple: + members: [claimant, partner, grandchild] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5720 + housing_benefit_permitted_work_disregard: 1040 + pension_credit_income: 23686 + pension_credit: 38.48 + pension_credit_war_pension_income: 0 + pension_credit_employment_income_tax: 1300 + has_mixed_age_couple_pension_credit_saving: true + would_claim_pc: true + has_tax_credit_award: false + carer_premium: 0 + working_tax_credit: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_maintenance_disregard: 0 + housing_benefit_on_passporting_benefit: false + households: + household: + members: [claimant, partner, grandchild] + country: ENGLAND + output: + housing_benefit_special_earnings_disregard_eligible: false + housing_benefit_special_earnings_disregard: 520 + pension_credit_earnings_disregard: 1040 + housing_benefit_applicable_income_disregard: 1929.20 + housing_benefit_savings_credit_only_income: 22835.28 + +- name: Savings credit carer couple deducts only paragraph 9 beyond the PC assessment + period: 2024 + absolute_error_margin: 0.01 + # Reg 27(4) does not list the Sch 4 para 4 carer standard as an independent + # modification. HB and PC Sch VI para 3 both disregard 20 x 52 = 1,040. + # The supplied PC assessment and savings credit are held fixed here. + # Sch 4 para 9(1): net earnings cover (20 + 17.10) x 52 = 1,929.20, + # so only 17.10 x 52 = 889.20 comes off: 20,434 + 690.32 - 889.20 + # = 20,235.12. The old ordinary-couple subtraction deducts another 520. + input: + people: + claimant: + age: 80 + is_claimant_or_partner: true + employment_income: 6500 + weekly_hours: 30 + ni_class_1_employee: 0 + partner: + age: 78 + is_claimant_or_partner: true + is_entitled_to_carer_benefit: true + benunits: + couple: + members: [claimant, partner] + carer_premium: 2371.20 + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5200 + housing_benefit_permitted_work_disregard: 0 + pension_credit_employment_income_tax: 1300 + pension_credit_income: 20434 + pension_credit: 690.32 + pension_credit_war_pension_income: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_maintenance_disregard: 0 + housing_benefit_on_passporting_benefit: false + households: + household: + members: [claimant, partner] + country: ENGLAND + output: + is_lone_parent: false + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_special_earnings_disregard: 1040 + pension_credit_earnings_disregard: 1040 + housing_benefit_applicable_income_disregard: 1929.20 + housing_benefit_savings_credit_only_income: 20235.12 + +- name: Savings credit lone parents retain paragraph 5A proviso under a PC disregard reform + period: 2024 + absolute_error_margin: 0.01 + # Reform control: PC's higher disregard is 30/week instead of statutory 20. + # At the statutory rate, the old lone-parent error in the residual cancels + # against the standard excess; this reform makes that error observable. + # HB Sch 4 para 5A(2): lower permitted 20 keeps the para 2 amount 25; + # higher permitted 40 replaces it. Net earnings 100/week cap neither. + # Para 9 adds 17.10/week in both cases: totals (25 + 17.10) x 52 = + # 2,189.20 and (40 + 17.10) x 52 = 2,969.20. Reg 27(4)(c)(i),(d): + # standard modifications max(0, 25 - 30) x 52 = 0 and (40 - 30) x 52 + # = 520; para 9 comes off in full. With supplied PC income/payment: + # 18,000 + 100 - 889.20 = 17,210.80; + # 18,000 + 100 - 520 - 889.20 = 16,690.80. + # Old code agrees for lower permitted work but gives 16,430.80 for higher. + input: + gov.dwp.pension_credit.earnings_disregard.higher.amount: 30 + people: + lower_parent: + age: 80 + is_claimant_or_partner: true + employment_income: 5200 + weekly_hours: 16 + ni_class_1_employee: 0 + lower_child: + age: 10 + is_claimant_or_partner: false + higher_parent: + age: 80 + is_claimant_or_partner: true + employment_income: 5200 + weekly_hours: 16 + ni_class_1_employee: 0 + higher_child: + age: 10 + is_claimant_or_partner: false + benunits: + lower: + members: [lower_parent, lower_child] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5200 + housing_benefit_permitted_work_disregard: 1040 + pension_credit_employment_income_tax: 0 + pension_credit_income: 18000 + pension_credit: 100 + pension_credit_war_pension_income: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_maintenance_disregard: 0 + housing_benefit_on_passporting_benefit: false + higher: + members: [higher_parent, higher_child] + housing_benefit_pension_age_regulations_apply: true + housing_benefit_net_earnings: 5200 + housing_benefit_permitted_work_disregard: 2080 + pension_credit_employment_income_tax: 0 + pension_credit_income: 18000 + pension_credit: 100 + pension_credit_war_pension_income: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_maintenance_disregard: 0 + housing_benefit_on_passporting_benefit: false + households: + lower_household: + members: [lower_parent, lower_child] + country: ENGLAND + higher_household: + members: [higher_parent, higher_child] + country: ENGLAND + output: + is_lone_parent: [true, true] + housing_benefit_special_earnings_disregard: [1300, 1300] + pension_credit_earnings_disregard: [1560, 1560] + housing_benefit_applicable_income_disregard: [2189.20, 2969.20] + housing_benefit_savings_credit_only_income: [17210.80, 16690.80] diff --git a/policyengine_uk/tests/test_housing_benefit_savings_credit_disregard_properties.py b/policyengine_uk/tests/test_housing_benefit_savings_credit_disregard_properties.py new file mode 100644 index 0000000000..e549e9b04e --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_savings_credit_disregard_properties.py @@ -0,0 +1,367 @@ +"""Properties of the savings-credit-only Housing Benefit income calculation. + +SI 2006/214 reg 27(4) (NI: SR 2006/406 reg 25(4)) permits the following +earnings modifications to the Pension Service's net income assessment: + +I1. The residual treated as additional earnings disregard is exactly the + Schedule 4 para 9(1) amount included in the applicable disregard: zero or + the annual para 9 amount. It contains no part of the applied standard. +I2. The standard-related modification is zero unless the family is a lone + parent or has permitted work. Otherwise it is the positive excess of + the applied standard over the Pension Credit earnings disregard. +I3. Income is non-negative and equals Pension Credit income plus savings + credit payable, less childcare, maintenance and those earnings + modifications, with a floor at zero. +I4. Increasing either the Pension Credit earnings disregard or Pension + Credit income, with all other inputs fixed, never lowers HB income. +I5. Ordinary families (no permitted work, no lone parent and no special + route) agree with main's ordinary-standard subtraction formula. + +These are fixture-scale pension-age populations, without passporting or +the working-age accommodation disregard. War pension income is fixed at +zero. Earnings, childcare and Pension Credit figures are pre-assessed +inputs: this isolates reg 27 rather than retesting their upstream rules. +Pension Credit earnings disregards may exceed today's statutory amounts as +independent reform/metamorphic controls. In I1 a high disregard disables the +standard-excess modification and a high income prevents clipping, exposing +the actual residual used by the savings-credit formula. This also catches +an omitted lone-parent proviso even when errors in the two modifications +would cancel at the current Pension Credit lone-parent disregard. +""" + +import numpy as np +from hypothesis import HealthCheck, example, given, settings +from hypothesis import strategies as st + +from policyengine_core.simulations import Simulation +from policyengine_uk.system import system + +YEAR = 2026 +WEEKS = 52 +TOLERANCE = 0.01 +HIGH_AMOUNT = 50_000 +SHAPES = { + "single": (1, 0), + "couple": (2, 0), + "lone_parent": (1, 1), + "couple_with_children": (2, 1), +} +P = system.parameters(YEAR).gov.dwp.housing_benefit.means_test.income_disregard +PARA_9 = P.worker * WEEKS +PROPERTY_SETTINGS = settings( + max_examples=5, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +def case(**overrides): + result = dict( + shape="couple", + net=2_000.0, + special=False, + permitted=0.0, + worker=True, + childcare=0.0, + pc_disregard=1_040.0, + pc_income=20_000.0, + savings_credit=500.0, + maintenance=0.0, + ) + result.update(overrides) + return result + + +def components(unit): + """Independent statutory standard and para 9 amounts, in annual pounds.""" + lone_parent = unit["shape"] == "lone_parent" + if lone_parent: + weekly = P.lone_parent + elif unit["special"]: + weekly = P.special + elif SHAPES[unit["shape"]][0] == 2: + weekly = P.couple + else: + weekly = P.single + selected = min(unit["net"], weekly * WEEKS) + permitted = max(0, unit["permitted"]) + # Sch 4 para 5A(2): permitted work replaces the standard, except that + # the lone parent retains para 2 when it is higher; earnings cap both. + standard = ( + min(unit["net"], max(permitted, selected if lone_parent else 0)) + if permitted > 0 + else selected + ) + # Para 9(3)(a): earnings must "equal or exceed" the other disregards, + # deductible childcare and para 9 itself, compared in statutory pence. + covers_additional = round(unit["net"] * 100) >= round( + (standard + unit["childcare"] + PARA_9) * 100 + ) + additional = PARA_9 if unit["worker"] and covers_additional else 0 + excess = ( + max(0, standard - unit["pc_disregard"]) if lone_parent or permitted > 0 else 0 + ) + return standard, additional, excess + + +@st.composite +def families(draw, ordinary=False): + shape = draw( + st.sampled_from( + ["single", "couple", "couple_with_children"] if ordinary else sorted(SHAPES) + ) + ) + return case( + shape=shape, + net=draw(st.integers(0, 2_000_000)) / 100, + special=False if ordinary else draw(st.booleans()), + permitted=( + 0 + if ordinary + else draw( + st.one_of( + st.sampled_from( + [-1, 0, 500, 1_040, 1_299.99, 1_300, 1_300.01, 9_542] + ), + st.integers(-100, 2_000_000).map(lambda pennies: pennies / 100), + ) + ) + ), + worker=draw(st.booleans()), + childcare=(draw(st.integers(0, 800_000)) / 100 if SHAPES[shape][1] else 0), + pc_disregard=draw(st.integers(0, 2_000_000)) / 100, + pc_income=draw(st.integers(0, 5_000_000)) / 100, + savings_credit=draw(st.integers(0, 200_000)) / 100, + maintenance=(draw(st.integers(0, 78_000)) / 100 if SHAPES[shape][1] else 0), + ) + + +# Exercise all family shapes, selected standards, the earnings cap, positive +# permitted work on either side of the lone-parent amount, and para 9 off/on. +EDGE_CASES = [ + case(shape=shape, special=special, net=net, permitted=permitted, worker=worker) + for shape in SHAPES + for special in (False, True) + for net, permitted in ( + (0, 9_542), + (259.99, 0), + (500, 1_040), + (2_000, 0), + (2_000, 1_040), + (2_000, 1_299.99), + (2_000, 1_300), + (2_000, 1_300.01), + (9_542, 20_000), + (20_000, 9_542), + ) + for worker in (False, True) +] +for shape in SHAPES: + for special in (False, True): + for permitted in (0, 1_040, 9_542): + unit = case(shape=shape, special=special, net=20_000, permitted=permitted) + standard = components(unit)[0] + for delta in (-0.01, 0, 0.01): + EDGE_CASES.append(case(**{**unit, "net": standard + PARA_9 + delta})) +EDGE_CASES.extend( + [ + case(shape="lone_parent", permitted=1_040, pc_disregard=1_560), + case(shape="lone_parent", permitted=2_080, pc_disregard=1_560), + case( + shape="couple_with_children", + pc_income=0, + childcare=8_000, + maintenance=780, + ), + ] +) +ORDINARY_CASES = [ + unit + for unit in EDGE_CASES + if unit["shape"] != "lone_parent" and not unit["special"] and unit["permitted"] == 0 +] +POPULATIONS = st.lists(families(), min_size=1, max_size=24) + + +def calculate(units, pc_income=None, pc_disregard=None): + people, benunits, households = {}, {}, {} + for i, unit in enumerate(units): + n_adults, n_children = SHAPES[unit["shape"]] + members = [] + for j in range(n_adults + n_children): + name = f"p{i}_{j}" + people[name] = { + "age": {YEAR: 80 if j < n_adults else 8}, + "is_claimant_or_partner": {YEAR: j < n_adults}, + } + members.append(name) + benunits[f"b{i}"] = {"members": members} + households[f"h{i}"] = {"members": members} + # These pre-assessed fixtures share read-only rules; each new core + # simulation keeps independent holders without UK wrapper data/scenarios. + sim = Simulation( + tax_benefit_system=system, + situation={"people": people, "benunits": benunits, "households": households}, + ) + inputs = { + "housing_benefit_net_earnings": "net", + "housing_benefit_special_earnings_disregard_eligible": "special", + "housing_benefit_permitted_work_disregard": "permitted", + "meets_housing_benefit_additional_earnings_disregard_conditions": "worker", + "housing_benefit_applicable_income_childcare_element": "childcare", + "pension_credit_earnings_disregard": "pc_disregard", + "pension_credit_income": "pc_income", + "pension_credit": "savings_credit", + "housing_benefit_maintenance_disregard": "maintenance", + } + for variable, key in inputs.items(): + values = [unit[key] for unit in units] + if key == "pc_income" and pc_income is not None: + values = np.full(len(units), pc_income) + if key == "pc_disregard" and pc_disregard is not None: + values = np.full(len(units), pc_disregard) + sim.set_input(variable, YEAR, values) + for variable, value in ( + ("housing_benefit_specified_or_temporary_accommodation_disregard", 0), + ("housing_benefit_on_passporting_benefit", False), + ("pension_credit_war_pension_income", 0), + ): + sim.set_input(variable, YEAR, np.full(len(units), value)) + return { + key: np.asarray(sim.calculate(variable, YEAR), dtype=float) + for key, variable in { + "income": "housing_benefit_savings_credit_only_income", + "applicable_disregard": "housing_benefit_applicable_income_disregard", + "standard": "housing_benefit_applied_standard_earnings_disregard", + }.items() + } + + +@PROPERTY_SETTINGS +@given(units=POPULATIONS) +@example(units=EDGE_CASES) +def test_i1_para_9_is_isolated_exactly(units): + values = calculate(units, pc_income=HIGH_AMOUNT, pc_disregard=HIGH_AMOUNT) + for i, unit in enumerate(units): + standard, para_9, _ = components(unit) + assert abs(values["standard"][i] - standard) < TOLERANCE, unit + included_para_9 = values["applicable_disregard"][i] - values["standard"][i] + assert abs(included_para_9 - para_9) < TOLERANCE, unit + # With the standard excess disabled, infer the actual residual used + # by the savings-credit path rather than merely recomputing one. + observed_residual = ( + HIGH_AMOUNT + + unit["savings_credit"] + - unit["childcare"] + - unit["maintenance"] + - values["income"][i] + ) + assert abs(observed_residual - included_para_9) < TOLERANCE, ( + unit, + observed_residual, + included_para_9, + ) + + +@PROPERTY_SETTINGS +@given(units=POPULATIONS) +@example(units=EDGE_CASES) +def test_i2_only_allowed_standard_excess_is_deducted(units): + values = calculate(units, pc_income=HIGH_AMOUNT) + for i, unit in enumerate(units): + _, para_9, excess = components(unit) + observed_excess = ( + HIGH_AMOUNT + + unit["savings_credit"] + - unit["childcare"] + - unit["maintenance"] + - para_9 + - values["income"][i] + ) + assert abs(observed_excess - excess) < TOLERANCE, ( + unit, + observed_excess, + excess, + ) + if unit["shape"] != "lone_parent" and unit["permitted"] <= 0: + assert abs(observed_excess) < TOLERANCE, unit + + +@PROPERTY_SETTINGS +@given(units=POPULATIONS) +@example(units=EDGE_CASES) +def test_i3_income_is_non_negative_and_has_only_listed_modifications(units): + values = calculate(units) + for i, unit in enumerate(units): + _, para_9, excess = components(unit) + expected = max( + 0, + unit["pc_income"] + + unit["savings_credit"] + - unit["childcare"] + - unit["maintenance"] + - para_9 + - excess, + ) + assert values["income"][i] >= 0, unit + assert abs(values["income"][i] - expected) < TOLERANCE, ( + unit, + values["income"][i], + expected, + ) + + +@PROPERTY_SETTINGS +@given(units=POPULATIONS, bump=st.integers(1, 1_000_000).map(lambda p: p / 100)) +@example(units=EDGE_CASES, bump=520) +def test_i4_income_is_non_decreasing_in_pc_earnings_disregard(units, bump): + bumped_units = [ + {**unit, "pc_disregard": unit["pc_disregard"] + bump} for unit in units + ] + # Each family has separate entities, so one vector preserves the same + # before/after controls without constructing the model twice. + income = calculate(units + bumped_units)["income"] + before, after = income[: len(units)], income[len(units) :] + for i, unit in enumerate(units): + assert after[i] >= before[i] - TOLERANCE, (unit, bump, before[i], after[i]) + + +@PROPERTY_SETTINGS +@given(units=POPULATIONS, bump=st.integers(1, 1_000_000).map(lambda p: p / 100)) +@example(units=EDGE_CASES, bump=520) +def test_i4_income_is_non_decreasing_in_pc_income(units, bump): + bumped_units = [{**unit, "pc_income": unit["pc_income"] + bump} for unit in units] + # Each family has separate entities, so one vector preserves the same + # before/after controls without constructing the model twice. + income = calculate(units + bumped_units)["income"] + before, after = income[: len(units)], income[len(units) :] + for i, unit in enumerate(units): + assert after[i] >= before[i] - TOLERANCE, (unit, bump, before[i], after[i]) + + +@PROPERTY_SETTINGS +@given(units=st.lists(families(ordinary=True), min_size=1, max_size=24)) +@example(units=ORDINARY_CASES) +def test_i5_ordinary_families_agree_with_main_formula(units): + values = calculate(units) + for i, unit in enumerate(units): + # origin/main's formula subtracts the ordinary standard from the + # applicable disregard to isolate para 9; its lone-parent term is + # zero for this deliberately restricted population. + ordinary_weekly = P.couple if SHAPES[unit["shape"]][0] == 2 else P.single + ordinary_standard = min(unit["net"], ordinary_weekly * WEEKS) + main_additional = max(0, values["applicable_disregard"][i] - ordinary_standard) + expected = max( + 0, + unit["pc_income"] + + unit["savings_credit"] + - unit["childcare"] + - unit["maintenance"] + - main_additional, + ) + assert abs(values["income"][i] - expected) < TOLERANCE, ( + unit, + values["income"][i], + expected, + ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py index 8b12285381..d88657cc13 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py @@ -47,19 +47,9 @@ class housing_benefit_applicable_income_disregard(Variable): def formula(benunit, period, parameters): p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard net_earnings = benunit("housing_benefit_net_earnings", period) - # Working age Sch 4 paras 4, 7 and 10; pension age Sch 4 paras 2 and 7. - standard = benunit("housing_benefit_special_earnings_disregard", period) - # Working-age para 10A(2) / pension-age para 5A(2): replace the - # ordinary amount, except for a higher lone-parent amount. The input - # already applies the statutory work and partner-allocation limits. - permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) - standard = where( - permitted > 0, - min_( - net_earnings, - max_(permitted, benunit("is_lone_parent", period) * standard), - ), - standard, + # Share the selected/replaced standard with the savings-credit route. + standard = benunit( + "housing_benefit_applied_standard_earnings_disregard", period ) # Working age Sch 4 para 18 (NI Sch 5 para 18), from 5 October 2026: # specified or temporary accommodation. It applies alongside paras 3 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.py new file mode 100644 index 0000000000..76f960ea66 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.py @@ -0,0 +1,42 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_applied_standard_earnings_disregard(Variable): + value_type = float + entity = BenUnit + label = "Housing Benefit applied standard earnings disregard" + documentation = ( + "Standard earnings disregard after any permitted-work replacement, " + "before the separate additional and accommodation amounts. A positive " + "pre-assessed permitted-work amount replaces the selected standard, " + "except that a lone parent keeps a higher selected amount. The result " + "is capped at net earnings. Both the general and savings-credit-only " + "income calculations use this amount so that the additional disregard " + "excludes every part of the applied standard." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/10A", + "https://www.legislation.gov.uk/uksi/2006/214/schedule/4/paragraph/5A", + "https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/10A", + "https://www.legislation.gov.uk/nisr/2006/406/schedule/5/paragraph/5A", + ) + + def formula(benunit, period, parameters): + net_earnings = benunit("housing_benefit_net_earnings", period) + # Working age Sch 4 paras 4, 7 and 10; pension age Sch 4 paras 2 and 7. + standard = benunit("housing_benefit_special_earnings_disregard", period) + # Working-age para 10A(2) / pension-age para 5A(2): replace the + # ordinary amount, except for a higher lone-parent amount. The input + # already applies the statutory work and partner-allocation limits. + permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) + standard = where( + permitted > 0, + min_( + net_earnings, + max_(permitted, benunit("is_lone_parent", period) * standard), + ), + standard, + ) + return standard diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py index 47ccfaa1c6..676d6661d1 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_savings_credit_only_income.py @@ -4,22 +4,23 @@ class housing_benefit_savings_credit_only_income(Variable): value_type = float entity = BenUnit - label = ( - "Housing Benefit income where the Pension Credit award is savings credit only" - ) + label = "Housing Benefit income for a savings-credit-only Pension Credit award" documentation = ( "Income for the Housing Benefit means test of a claimant who, or whose " "partner, has a Pension Credit award of savings credit only. The " "authority takes the Secretary of State's assessment of net income for " "that award and modifies it only for the listed items. Modelled: the " "savings credit payable is added, and childcare charges and the " - "higher Housing Benefit earnings disregards are deducted: a lone " - "parent's 25 pounds over the amount disregarded for Pension Credit, " - "and the additional 17.10 pounds. The Secretary of State's figure is " + "higher Housing Benefit earnings disregards are deducted: any excess " + "of the lone-parent or permitted-work amount over the amount already " + "disregarded for Pension Credit, and the separate additional 17.10 " + "pounds. The Secretary of State's figure is " "Pension Credit income, which is already after the Pension Credit " "earnings disregards (20 pounds for a lone parent, carer, disabled " "or blind earner, otherwise 5 pounds single or 10 pounds couple), so " - "Housing Benefit's own 5 and 10 pound amounts are not deducted again. " + "standard amounts already disregarded for Pension Credit are not " + "deducted again; other special-standard grounds do not independently " + "modify that assessment. " "Childcare charges are deducted as in the general " "route, without the work condition or the cap at earnings. Pension " "Credit counts spousal maintenance in full, so Housing Benefit's 15 " @@ -28,7 +29,7 @@ class housing_benefit_savings_credit_only_income(Variable): "Benefit's own income and tariff income rules do not apply to the " "Secretary of State's figure, which already counts Pension Credit " "deemed income from capital. Not modelled: " - "the exempt work disregard, the income and capital of a " + "the income and capital of a " "partner treated as a household member but not in the Pension Credit " "assessment, and income of a non-dependant treated as the claimant's. " "War pensions (Great Britain: local schemes under section 134(8) of " @@ -74,28 +75,24 @@ def formula(benunit, period, parameters): # Reg 27(4)(c)(i) and (d) (NI: reg 25(4)(c)(i) and (d)): the # Secretary of State's figure is after the SPC Regs 2002 Sch VI # earnings disregards (pension_credit_earnings_disregard), so only the - # higher amounts Housing Benefit allows come off: a lone parent's - # Sch 4 para 2 disregard (25 pounds) over the amount disregarded for - # Pension Credit, and the additional disregard of Sch 4 para 9(1) - # (the para 5A exempt work disregard is not modelled). Housing - # Benefit's other grounds do not replace the Pension Credit ones. This - # route applies only under the pension-age Regulations, where the - # disregard is the standard amount (Sch 4 paras 2 and 7) and the - # additional amount (para 9), with no specified-accommodation amount. - p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard - net_earnings = benunit("housing_benefit_net_earnings", period) + # higher amounts Housing Benefit allows come off: the excess of + # the lone-parent (Sch 4 para 2) or permitted-work (para 5A) amount + # over the existing Pension Credit deduction, plus the separate + # additional disregard (para 9). Read the shared applied standard + # before isolating para 9, so replaced or pre-existing disregards + # are not deducted again. Other special-standard grounds do not + # independently modify the Pension Credit assessment. This route + # has no specified-accommodation disregard. lone_parent = benunit("is_lone_parent", period) - weekly_amount = select( - [lone_parent, benunit("is_couple", period)], - [p.lone_parent, p.couple], - default=p.single, + permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) + standard = benunit( + "housing_benefit_applied_standard_earnings_disregard", period ) - standard = min_(weekly_amount * WEEKS_IN_YEAR, net_earnings) additional = max_( 0, benunit("housing_benefit_applicable_income_disregard", period) - standard ) - higher_lone_parent = where( - lone_parent, + higher_standard = where( + lone_parent | (permitted > 0), max_(0, standard - benunit("pension_credit_earnings_disregard", period)), 0, ) @@ -103,7 +100,7 @@ def formula(benunit, period, parameters): # (SPC Regs 2002 reg 15(5)(d)), so the higher amount Housing Benefit # disregards is all of its Sch 5 para 20 disregard. maintenance = benunit("housing_benefit_maintenance_disregard", period) - disregard = higher_lone_parent + additional + maintenance + disregard = higher_standard + additional + maintenance return max_( 0, pension_credit_income + savings_credit_payable - childcare - disregard, diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py index b3529ffc83..e466097660 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_special_earnings_disregard.py @@ -14,7 +14,7 @@ class housing_benefit_special_earnings_disregard(Variable): "It is a replacement, not £20 added to £5 or £10, and it is capped at " "the claimant's and partner's net earnings. This is the amount before " "permitted work: where housing_benefit_permitted_work_disregard is " - "above zero, housing_benefit_applicable_income_disregard deducts that " + "above zero, housing_benefit_applied_standard_earnings_disregard uses that " "amount in place of this one, keeping this amount only for a lone " "parent for whom it is higher (SI 2006/213 Sch 4 para 10A(2), SI " "2006/214 Sch 4 para 5A(2), SR 2006/405 Sch 5 para 10A(2) and SR " From be21fa43c1c6f1f2921aa9b6e527e090ccbff371 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 09:36:40 -0400 Subject: [PATCH 4/4] Restore spacing in the savings-credit reproducer comment The worked arithmetic in the legal reproducer case had lost its spaces. Also name the pre-fix commit instead of "current head", as the round-2 review asked. Comments only. Co-Authored-By: Claude Opus 5.5 --- .../housing_benefit_savings_credit_only.yaml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml index 32be86ff62..abd2d17830 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/housing_benefit_savings_credit_only.yaml @@ -441,17 +441,18 @@ # entitled to PC/HB on 14 May 2019 and continuously entitled to either since. # Grandchild normally lives here and they are responsible; no tax-credit award. # Claimant: gross earnings 125/week, tax 25/week, net 100/week; gross private -# pension 14,732.50/year, tax 432.50/year, net pension 275/week. No Class 1 at80. +# pension 14,732.50/year, tax 432.50/year, net pension 275/week. No Class 1 at 80. # Partner: assessment-phase contributory ESA 90.50/week, actual unsupported # self-employment 16 hours/week, net earnings 10/week. Not main phase, no other -# special-HB route, and no reg7 ESA assessment exception. ESA reg45(2) applies; -# precisely16hours excludes45(4). HB5A amount=10+min(100,20-10,20)=20/week. -# PC SchVI4 already excludes20/week. AIF=275+100+10+90.50-20=455.50/week. -# Child16hour route adds17.10 HB disregard. PC: minimum332.95+76.79=409.74, -# qualifying income365, SavingsCredit19.04-18.30=0.74/week (PCreg7(4)). +# special-HB route, and no reg 7 ESA assessment exception. ESA reg 45(2) applies; +# precisely 16 hours excludes 45(4). HB para 5A amount +# = 10 + min(100, 20 - 10, 20) = 20/week. PC Sch VI para 4 already excludes +# 20/week. AIF = 275 + 100 + 10 + 90.50 - 20 = 455.50/week. The child 16-hour +# route adds the 17.10 HB disregard. PC: minimum 332.95 + 76.79 = 409.74, +# qualifying income 365, Savings Credit 19.04 - 18.30 = 0.74/week (PC reg 7(4)). # All AIF/net-earnings inputs are external component assessments. -# Expected:23686+38.48-17.10*52=22835.28. Current head incorrectly subtracts -# (37.10-10)*52=1409.20, producing22315.28. +# Expected: 23686 + 38.48 - 17.10 * 52 = 22835.28. Pre-fix a67175429 incorrectly +# subtracts (37.10 - 10) * 52 = 1409.20, producing 22315.28. - name: Protected Savings Credit family offsets the existing PC earnings disregard period: 2024 absolute_error_margin: 0.01