From d84c026f4705a1d7e1085b4722c893274429a3c4 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:59:29 +0400 Subject: [PATCH 1/5] Limit Housing Benefit childcare to qualifying charges and earnings residuals --- changelog.d/hb-qualified-childcare.fixed.md | 1 + ...t_applicable_income_childcare_element.yaml | 1230 +++++++++++++++-- ...fit_applicable_income_childcare_element.py | 43 +- ...ousing_benefit_childcare_earnings_limit.py | 58 + ...ng_benefit_qualifying_childcare_charges.py | 25 + 5 files changed, 1211 insertions(+), 146 deletions(-) create mode 100644 changelog.d/hb-qualified-childcare.fixed.md create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py create mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py diff --git a/changelog.d/hb-qualified-childcare.fixed.md b/changelog.d/hb-qualified-childcare.fixed.md new file mode 100644 index 0000000000..d5d87c2075 --- /dev/null +++ b/changelog.d/hb-qualified-childcare.fixed.md @@ -0,0 +1 @@ +- Limit Housing Benefit childcare deductions to pre-assessed qualifying charges, the paid-care child-count cap, and remaining earnings plus tax-credit payments. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml index 411dd36183..06bf811f4a 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml @@ -1,242 +1,1212 @@ -- name: One child, capped - period: 2025 - absolute_error_margin: 0.01 +# Qualifying charges are externally pre-assessed; these tests isolate the +# count/cap/residual calculation. Cap inputs reproduce #1925, not another fix. +- name: "No children and no qualifying charges" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + benunits: + family: + members: ["claimant"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 0 + +- name: "Generic expenditure alone is not a qualifying assessment" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true childcare_expenses: 9000 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 0 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 0 + +- name: "One child below weekly cap" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true child0: age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 1 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 1 housing_benefit_applicable_income_childcare_element: 9000 -- name: Two child, capped - period: 2025 - absolute_error_margin: 0.01 +- name: "One child at weekly cap" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9100 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 9100 + +- name: "One child above weekly cap" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 10000 + childcare_expenses: 0 + is_claimant_or_partner: true child0: - age: 8 + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 20000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 9100 + +- name: "Two children below multiple-child cap" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 5000 child1: age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 5000 benunits: - benunit: - members: [claimant, child0, child1] + family: + members: ["claimant","child0","child1"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 2 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 2 housing_benefit_applicable_income_childcare_element: 10000 -- name: No children - period: 2025 - absolute_error_margin: 0.01 +- name: "Two children at multiple-child cap" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 30000 + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 7800 + child1: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 7800 benunits: - benunit: - members: [claimant] + family: + members: ["claimant","child0","child1"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 0 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 0 - housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_applicable_income_childcare_element: 15600 -- name: Non-disabled child aged 15 remains within annual age approximation - period: 2025 - absolute_error_margin: 0.01 +- name: "Two children above multiple-child cap" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9000 + childcare_expenses: 0 + is_claimant_or_partner: true child0: - age: 15 + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 + child1: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0","child1"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 1 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 1 - housing_benefit_applicable_income_childcare_element: 9000 + housing_benefit_applicable_income_childcare_element: 15600 -- name: Non-disabled young person aged 16 no longer counts - period: 2025 - absolute_error_margin: 0.01 +- name: "Three paid-care children do not increase the family cap" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9000 + childcare_expenses: 0 + is_claimant_or_partner: true child0: - age: 16 + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 8000 + child1: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 8000 + child2: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 8000 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0","child1","child2"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 0 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 0 - housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_applicable_income_childcare_element: 15600 -- name: Non-disabled young person aged 17 no longer counts - period: 2025 - absolute_error_margin: 0.01 +- name: "Two children but paid care for only one use one-child cap" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9000 + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 20000 + child1: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 0 + benunits: + family: + members: ["claimant","child0","child1"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 9100 + +- name: "Older child without qualifying care does not increase cap" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 20000 + child1: + age: 17 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 0 + benunits: + family: + members: ["claimant","child0","child1"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 9100 + +- name: "Charges for an excluded young person are not counted" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true child0: age: 17 + is_child_or_young_person_for_legacy_benefits: false + housing_benefit_qualifying_childcare_charges: 9000 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 0 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 0 housing_benefit_applicable_income_childcare_element: 0 -- name: Disabled young person aged 16 counts - period: 2025 - absolute_error_margin: 0.01 +- name: "Negative supplied charges are floored" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9000 + childcare_expenses: 0 + is_claimant_or_partner: true child0: - age: 16 - is_disabled_for_benefits: true + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: -500 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 1 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 1 - housing_benefit_applicable_income_childcare_element: 9000 + housing_benefit_applicable_income_childcare_element: 0 -- name: Blind young person aged 16 counts - period: 2025 - absolute_error_margin: 0.01 +- name: "Zero earnings and no tax credits give no deduction" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9000 + childcare_expenses: 0 + is_claimant_or_partner: true child0: - age: 16 - is_blind: true + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 0 + housing_benefit_special_earnings_disregard: 0 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 1 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 1 - housing_benefit_applicable_income_childcare_element: 9000 + housing_benefit_applicable_income_childcare_element: 0 -- name: Disabled young person aged 17 no longer counts - period: 2025 - absolute_error_margin: 0.01 +- name: "Low earnings after standard disregard" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9000 + childcare_expenses: 0 + is_claimant_or_partner: true child0: - age: 17 - is_disabled_for_benefits: true + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 740 + +- name: "Standard disregard exhausts earnings" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 benunits: - benunit: - members: [claimant, child0] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 260 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # Annual age limits are <16, or <17 if disabled/blind (HB reg 28(6)). - # With 0 relevant children the deduction is min(cost, weekly cap * 52). - num_relevant_children_for_housing_benefit_childcare: 0 housing_benefit_applicable_income_childcare_element: 0 -- name: One relevant child and an older young person use the one-child cap - period: 2025 - absolute_error_margin: 0.01 +- name: "WTC payment meets residual" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 30_000 - child: + childcare_expenses: 0 + is_claimant_or_partner: true + child0: age: 10 - young_person: - age: 17 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 benunits: - benunit: - members: [claimant, child, young_person] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 500 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - num_relevant_children_for_housing_benefit_childcare: 1 - # Reg 27(3) selects the one-child cap: min(30,000, 312.0555942 * 52). - # The existing uprated WTC cap is retained; its difference from the - # statutory HB cap of 175/week is outside this classification change. - housing_benefit_applicable_income_childcare_element: 16_226.8908984 + housing_benefit_applicable_income_childcare_element: 1240 -- name: Under-18 claimant is not their own relevant child - period: 2025 +- name: "CTC payment meets residual" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: - age: 17 - childcare_expenses: 9_000 + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 benunits: - benunit: - members: [claimant] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # 0 dependants implies 0 childcare cap (HB reg 27(3)). - num_relevant_children_for_housing_benefit_childcare: 0 - housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "Both tax credit payments may meet residual under DWP W2.180" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 500 + child_tax_credit: 600 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 1840 -- name: Disabled 16-year-old outside education and training is not a relevant young person - period: 2025 +- name: "Permitted work replaces standard amount before childcare" + period: 2024 + absolute_error_margin: 0.02 input: people: claimant: age: 40 is_parent: true - childcare_expenses: 9_000 - young_person: - age: 16 - is_disabled_for_benefits: true - is_in_non_advanced_education: false - is_in_approved_training: false + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 benunits: - benunit: - members: [claimant, young_person] + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 500 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 500 + +- name: "Accommodation disregard reduces available earnings" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 500 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 240 + +- name: "Accommodation amount is capped at earnings left" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 1000 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - # HB reg 28(6) requires a child of the family, including a qualifying - # young person; the disability age extension does not create that status. - num_relevant_children_for_housing_benefit_childcare: 0 housing_benefit_applicable_income_childcare_element: 0 + +- name: "Special standard amount reduces remaining earnings" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 2000 + housing_benefit_special_earnings_disregard: 1040 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 960 + +- name: "Higher lone-parent amount is retained over permitted work" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 2000 + housing_benefit_special_earnings_disregard: 1300 + housing_benefit_permitted_work_disregard: 500 + is_lone_parent: true + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 700 + +- name: "Additional disregard qualifies at exact childcare-inclusive threshold" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 1000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 2149.2 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 1000 + housing_benefit_applicable_income_disregard: 1149.2 + +- name: "Additional disregard fails just below childcare-inclusive threshold" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 1000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 2149.19 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 1000 + housing_benefit_applicable_income_disregard: 260 + +- name: "Childcare does not recurse through additional disregard with low earnings" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 1000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 740 + housing_benefit_applicable_income_disregard: 260 + +- name: "Explicit one-child cap reform wins" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 9000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 100 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 5200 + +- name: "ENGLAND pension regulations false earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "ENGLAND" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "ENGLAND pension regulations true earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: true + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "ENGLAND" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "SCOTLAND pension regulations false earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "SCOTLAND" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "SCOTLAND pension regulations true earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: true + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "SCOTLAND" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "WALES pension regulations false earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "WALES" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "WALES pension regulations true earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: true + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "WALES" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "NORTHERN_IRELAND pension regulations false earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "NORTHERN_IRELAND" + output: + housing_benefit_applicable_income_childcare_element: 1540 + +- name: "NORTHERN_IRELAND pension regulations true earnings and CTC residual" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + housing_benefit_qualifying_childcare_charges: 2000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 800 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: true + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + households: + household: + members: ["claimant","child0"] + country: "NORTHERN_IRELAND" + output: + housing_benefit_applicable_income_childcare_element: 1540 diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py index aa7dd6bd65..64c182fb67 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py @@ -1,29 +1,40 @@ from policyengine_uk.model_api import * +def hb_capped_childcare_charges(benunit, period, parameters): + """Qualifying paid care only; one-child or multiple-child family cap.""" + person = benunit.members + charges = max_(person("housing_benefit_qualifying_childcare_charges", period), 0) + included = person("is_child_or_young_person_for_legacy_benefits", period) + count = benunit.sum(included & (charges > 0)) + p = parameters(period).gov.dwp.tax_credits.working_tax_credit.elements + cap = select([count == 1, count > 1], [p.childcare_1, p.childcare_2], default=0) + return min_(benunit.sum(charges * included), cap * WEEKS_IN_YEAR) + + class housing_benefit_applicable_income_childcare_element(Variable): value_type = float entity = BenUnit label = "Housing Benefit applicable income childcare element" definition_period = YEAR unit = GBP - reference = "https://www.legislation.gov.uk/uksi/2006/213/regulation/27" + documentation = ( + "Qualifying pre-assessed charges, capped by the number of included " + "children with paid care and by remaining earnings plus WTC/CTC " + "payments. Provider/work/age and continuation conditions are delegated " + "to housing_benefit_qualifying_childcare_charges. Generic " + "childcare_expenses are not treated as qualifying by default. The " + "annual calculation assumes constant household circumstances." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/27", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/30", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/25", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/28", + ) def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.tax_credits.working_tax_credit.elements - relevant_children = benunit( - "num_relevant_children_for_housing_benefit_childcare", period - ) - childcare_amount_1 = (relevant_children == 1) * p.childcare_1 - childcare_amount_2 = (relevant_children > 1) * p.childcare_2 - max_weekly_childcare_amount = childcare_amount_1 + childcare_amount_2 - # Assuming that no children leads to no childcare element - capped_max_childcare_amount = where( - relevant_children == 0, 0, max_weekly_childcare_amount - ) - max_childcare_amount = capped_max_childcare_amount * WEEKS_IN_YEAR - childcare_expenses = add(benunit, period, ["childcare_expenses"]) return min_( - max_childcare_amount, - childcare_expenses, + hb_capped_childcare_charges(benunit, period, parameters), + benunit("housing_benefit_childcare_earnings_limit", period), ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py new file mode 100644 index 0000000000..1a37c7fa5a --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py @@ -0,0 +1,58 @@ +from policyengine_uk.model_api import * + + +def hb_standard_earnings_disregard(benunit, period): + """Selected standard amount, including the permitted-work replacement.""" + net = max_(benunit("housing_benefit_net_earnings", period), 0) + standard = benunit("housing_benefit_special_earnings_disregard", period) + permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) + return where( + permitted > 0, + min_(net, max_(permitted, benunit("is_lone_parent", period) * standard)), + standard, + ) + + +def hb_childcare_earnings_limit(benunit, period, accommodation=None): + """Remaining earnings and tax credits, without an additional-disregard loop.""" + net = max_(benunit("housing_benefit_net_earnings", period), 0) + standard = hb_standard_earnings_disregard(benunit, period) + if accommodation is None: + accommodation = benunit( + "housing_benefit_specified_or_temporary_accommodation_disregard", period + ) + accommodation = min_(max_(accommodation, 0), max_(0, net - standard)) + # DWP BW2 W2.179–W2.182 permits the residual against any WTC/CTC in + # payment, not other income. Both amounts are benefit-unit payments. + credits = max_(benunit("working_tax_credit", period), 0) + max_( + benunit("child_tax_credit", period), 0 + ) + return max_(0, net - standard - accommodation) + credits + + +class housing_benefit_childcare_earnings_limit(Variable): + value_type = float + entity = BenUnit + definition_period = YEAR + unit = GBP + label = "Housing Benefit childcare earnings and tax-credit limit" + documentation = ( + "Net claimant/partner earnings after the selected standard, permitted-work " + "and accommodation disregards, plus WTC/CTC payments. Unrelated income " + "cannot meet remaining childcare charges. The additional earnings " + "disregard is conditional on earnings covering the childcare deduction " + "as well as that disregard: when it applies the full capped childcare " + "charge already fits, so this calculation need not depend on it. This " + "avoids a circular calculation through the childcare deduction." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/27", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/30", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/25", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/28", + "https://assets.publishing.service.gov.uk/media/5a7c7cf7ed915d6969f4538d/hbgm-bw2-assessment-of-income.pdf", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/17", + ) + + def formula(benunit, period, parameters): + return hb_childcare_earnings_limit(benunit, period) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py new file mode 100644 index 0000000000..3a572c94be --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py @@ -0,0 +1,25 @@ +from policyengine_uk.model_api import * + + +class housing_benefit_qualifying_childcare_charges(Variable): + value_type = float + entity = Person + definition_period = YEAR + unit = GBP + default_value = 0 + label = "pre-assessed qualifying Housing Benefit childcare charges" + documentation = ( + "Annual qualifying charges for this child, before the family cap and " + "earnings limit. The supplied amount must already satisfy the relevant " + "provider, paid-care, age, work, leave, incapacity and continuation rules " + "(GB working-age regulation 28, pension-age regulation 31; NI 26/29). " + "It is not generic childcare expenditure. Missing values give no " + "deduction and may understate awards; zero does not establish that no " + "qualifying charges exist. No dataset source has been verified." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/28", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/31", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/26", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/29", + ) From c5e5f88471395f09d1ecd0770cbaee2c938c7807 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Fri, 9 Oct 2026 21:36:49 +0400 Subject: [PATCH 2/5] Retain childcare_expenses and defer Housing Benefit qualifying-care rules --- changelog.d/hb-qualified-childcare.fixed.md | 2 +- ...t_applicable_income_childcare_element.yaml | 195 +++++++++++++----- ...g_benefit_applicable_income_disregard.yaml | 7 +- ...fit_applicable_income_childcare_element.py | 31 +-- ...ousing_benefit_childcare_earnings_limit.py | 2 +- ...ng_benefit_qualifying_childcare_charges.py | 25 --- 6 files changed, 171 insertions(+), 91 deletions(-) delete mode 100644 policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py diff --git a/changelog.d/hb-qualified-childcare.fixed.md b/changelog.d/hb-qualified-childcare.fixed.md index d5d87c2075..cc904f76c9 100644 --- a/changelog.d/hb-qualified-childcare.fixed.md +++ b/changelog.d/hb-qualified-childcare.fixed.md @@ -1 +1 @@ -- Limit Housing Benefit childcare deductions to pre-assessed qualifying charges, the paid-care child-count cap, and remaining earnings plus tax-credit payments. +- Limited Housing Benefit childcare deductions to remaining earnings plus tax-credit payments while retaining childcare_expenses and the existing age-based cap approximation; deferred detailed qualifying-care rules without adding an input. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml index 06bf811f4a..7d4692788e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml @@ -1,5 +1,6 @@ -# Qualifying charges are externally pre-assessed; these tests isolate the -# count/cap/residual calculation. Cap inputs reproduce #1925, not another fix. +# Detailed provider, charge-type and work/leave conditions are deferred. +# Aggregate childcare_expenses and age-eligible child counts retain the +# existing annual approximation. Cap inputs reproduce #1925, not another fix. - name: "No children and no qualifying charges" period: 2024 absolute_error_margin: 0.02 @@ -28,7 +29,7 @@ output: housing_benefit_applicable_income_childcare_element: 0 -- name: "Generic expenditure alone is not a qualifying assessment" +- name: "Existing claimant childcare_expenses are used without a new input" period: 2024 absolute_error_margin: 0.02 input: @@ -41,7 +42,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 0 + childcare_expenses: 0 benunits: family: members: ["claimant","child0"] @@ -58,7 +59,7 @@ gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - housing_benefit_applicable_income_childcare_element: 0 + housing_benefit_applicable_income_childcare_element: 9000 - name: "One child below weekly cap" period: 2024 @@ -73,7 +74,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -105,7 +106,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9100 + childcare_expenses: 9100 benunits: family: members: ["claimant","child0"] @@ -137,7 +138,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 20000 + childcare_expenses: 20000 benunits: family: members: ["claimant","child0"] @@ -169,11 +170,11 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 5000 + childcare_expenses: 5000 child1: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 5000 + childcare_expenses: 5000 benunits: family: members: ["claimant","child0","child1"] @@ -205,11 +206,11 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 7800 + childcare_expenses: 7800 child1: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 7800 + childcare_expenses: 7800 benunits: family: members: ["claimant","child0","child1"] @@ -241,11 +242,11 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 child1: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0","child1"] @@ -277,15 +278,15 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 8000 + childcare_expenses: 8000 child1: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 8000 + childcare_expenses: 8000 child2: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 8000 + childcare_expenses: 8000 benunits: family: members: ["claimant","child0","child1","child2"] @@ -304,7 +305,7 @@ output: housing_benefit_applicable_income_childcare_element: 15600 -- name: "Two children but paid care for only one use one-child cap" +- name: "Age-based cap approximation counts both children when only one has recorded spending" period: 2024 absolute_error_margin: 0.02 input: @@ -317,11 +318,11 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 20000 + childcare_expenses: 20000 child1: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 0 + childcare_expenses: 0 benunits: family: members: ["claimant","child0","child1"] @@ -338,7 +339,9 @@ gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 output: - housing_benefit_applicable_income_childcare_element: 9100 + # Spending attribution is not assessed: two age-eligible children give + # the multiple-child cap even when costs are recorded on only one child. + housing_benefit_applicable_income_childcare_element: 15600 - name: "Older child without qualifying care does not increase cap" period: 2024 @@ -353,11 +356,11 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 20000 + childcare_expenses: 20000 child1: age: 17 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 0 + childcare_expenses: 0 benunits: family: members: ["claimant","child0","child1"] @@ -389,7 +392,7 @@ child0: age: 17 is_child_or_young_person_for_legacy_benefits: false - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -421,7 +424,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: -500 + childcare_expenses: -500 benunits: family: members: ["claimant","child0"] @@ -453,7 +456,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -485,7 +488,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -517,7 +520,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -549,7 +552,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -581,7 +584,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -613,7 +616,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -645,7 +648,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -677,7 +680,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -709,7 +712,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -741,7 +744,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -773,7 +776,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -805,7 +808,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 1000 + childcare_expenses: 1000 benunits: family: members: ["claimant","child0"] @@ -838,7 +841,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 1000 + childcare_expenses: 1000 benunits: family: members: ["claimant","child0"] @@ -871,7 +874,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 1000 + childcare_expenses: 1000 benunits: family: members: ["claimant","child0"] @@ -904,7 +907,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 9000 + childcare_expenses: 9000 benunits: family: members: ["claimant","child0"] @@ -936,7 +939,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -972,7 +975,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1008,7 +1011,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1044,7 +1047,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1080,7 +1083,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1116,7 +1119,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1152,7 +1155,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1175,6 +1178,8 @@ output: housing_benefit_applicable_income_childcare_element: 1540 + + - name: "NORTHERN_IRELAND pension regulations true earnings and CTC residual" period: 2024 absolute_error_margin: 0.02 @@ -1188,7 +1193,7 @@ child0: age: 10 is_child_or_young_person_for_legacy_benefits: true - housing_benefit_qualifying_childcare_charges: 2000 + childcare_expenses: 2000 benunits: family: members: ["claimant","child0"] @@ -1210,3 +1215,97 @@ country: "NORTHERN_IRELAND" output: housing_benefit_applicable_income_childcare_element: 1540 + + +- name: "Recorded spending without age-eligible children gives no deduction" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 5000 + is_claimant_or_partner: true + benunits: + family: + members: ["claimant"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 0 + +- name: "Standard age limit excludes a child aged 16" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 16 + is_child_or_young_person_for_legacy_benefits: true + childcare_expenses: 9000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 0 + +- name: "Disability extends the annual child age limit to include age 16" + period: 2024 + absolute_error_margin: 0.02 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 0 + is_claimant_or_partner: true + child0: + age: 16 + is_disabled_for_benefits: true + is_child_or_young_person_for_legacy_benefits: true + childcare_expenses: 9000 + benunits: + family: + members: ["claimant","child0"] + housing_benefit_net_earnings: 20000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + is_lone_parent: false + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_on_passporting_benefit: false + housing_benefit_pension_age_regulations_apply: false + gov.dwp.tax_credits.working_tax_credit.elements.childcare_1: 175 + gov.dwp.tax_credits.working_tax_credit.elements.childcare_2: 300 + output: + housing_benefit_applicable_income_childcare_element: 9000 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml index 7cf5e90192..a34303ce8e 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.yaml @@ -363,9 +363,10 @@ would_claim_uc: false # not on Universal Credit, which passports the means test members: [parent, child] output: - # Childcare deducted is min(£4,160, one-child maximum) = £4,160. - # £5,200 < £1,300 + £4,160 + £889.20 = £6,349.20, so only £25 a week. - housing_benefit_applicable_income_childcare_element: 4_160 + # Reg 27(1)(c): after the £1,300 standard disregard, only £3,900 of + # earnings remain. With no tax credits, min(£4,160, £3,900) = £3,900. + # £5,200 < £1,300 + £3,900 + £889.20 = £6,089.20, so only £25 a week. + housing_benefit_applicable_income_childcare_element: 3_900 housing_benefit_applicable_income_disregard: 1_300 - name: Lone parent whose earnings cover the disregards and childcare gets the additional disregard diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py index 64c182fb67..1c91664139 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.py @@ -2,14 +2,12 @@ def hb_capped_childcare_charges(benunit, period, parameters): - """Qualifying paid care only; one-child or multiple-child family cap.""" - person = benunit.members - charges = max_(person("housing_benefit_qualifying_childcare_charges", period), 0) - included = person("is_child_or_young_person_for_legacy_benefits", period) - count = benunit.sum(included & (charges > 0)) + """Aggregate childcare spending capped using the existing age-based count.""" + expenses = max_(add(benunit, period, ["childcare_expenses"]), 0) + count = benunit("num_relevant_children_for_housing_benefit_childcare", period) p = parameters(period).gov.dwp.tax_credits.working_tax_credit.elements cap = select([count == 1, count > 1], [p.childcare_1, p.childcare_2], default=0) - return min_(benunit.sum(charges * included), cap * WEEKS_IN_YEAR) + return min_(expenses, cap * WEEKS_IN_YEAR) class housing_benefit_applicable_income_childcare_element(Variable): @@ -19,18 +17,25 @@ class housing_benefit_applicable_income_childcare_element(Variable): definition_period = YEAR unit = GBP documentation = ( - "Qualifying pre-assessed charges, capped by the number of included " - "children with paid care and by remaining earnings plus WTC/CTC " - "payments. Provider/work/age and continuation conditions are delegated " - "to housing_benefit_qualifying_childcare_charges. Generic " - "childcare_expenses are not treated as qualifying by default. The " - "annual calculation assumes constant household circumstances." + "Aggregate childcare_expenses, capped using the existing age-eligible " + "child count and limited by remaining earnings plus WTC/CTC payments. " + "Provider eligibility, excluded charge types, work/leave conditions " + "and incapacity/continuation exceptions are not assessed. The child " + "count approximates children with paid care and can overstate the " + "cap when only one of several eligible children receives paid care. " + "These qualifying-care rules are deferred in " + "https://github.com/PolicyEngine/policyengine-uk/issues/2236. " + "The annual calculation assumes constant household circumstances." ) reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/27", "https://www.legislation.gov.uk/uksi/2006/214/regulation/30", - "https://www.legislation.gov.uk/nisr/2006/405/regulation/25", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/24", "https://www.legislation.gov.uk/nisr/2006/406/regulation/28", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/28", + "https://www.legislation.gov.uk/uksi/2006/214/regulation/31", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/25", + "https://www.legislation.gov.uk/nisr/2006/406/regulation/29", ) def formula(benunit, period, parameters): diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py index 1a37c7fa5a..54ee4c6087 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py @@ -48,7 +48,7 @@ class housing_benefit_childcare_earnings_limit(Variable): reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/27", "https://www.legislation.gov.uk/uksi/2006/214/regulation/30", - "https://www.legislation.gov.uk/nisr/2006/405/regulation/25", + "https://www.legislation.gov.uk/nisr/2006/405/regulation/24", "https://www.legislation.gov.uk/nisr/2006/406/regulation/28", "https://assets.publishing.service.gov.uk/media/5a7c7cf7ed915d6969f4538d/hbgm-bw2-assessment-of-income.pdf", "https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/17", diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py deleted file mode 100644 index 3a572c94be..0000000000 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_qualifying_childcare_charges.py +++ /dev/null @@ -1,25 +0,0 @@ -from policyengine_uk.model_api import * - - -class housing_benefit_qualifying_childcare_charges(Variable): - value_type = float - entity = Person - definition_period = YEAR - unit = GBP - default_value = 0 - label = "pre-assessed qualifying Housing Benefit childcare charges" - documentation = ( - "Annual qualifying charges for this child, before the family cap and " - "earnings limit. The supplied amount must already satisfy the relevant " - "provider, paid-care, age, work, leave, incapacity and continuation rules " - "(GB working-age regulation 28, pension-age regulation 31; NI 26/29). " - "It is not generic childcare expenditure. Missing values give no " - "deduction and may understate awards; zero does not establish that no " - "qualifying charges exist. No dataset source has been verified." - ) - reference = ( - "https://www.legislation.gov.uk/uksi/2006/213/regulation/28", - "https://www.legislation.gov.uk/uksi/2006/214/regulation/31", - "https://www.legislation.gov.uk/nisr/2006/405/regulation/26", - "https://www.legislation.gov.uk/nisr/2006/406/regulation/29", - ) From 50215dfea1f1121a256d4f50284ce62a35e2072f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 14:44:03 -0400 Subject: [PATCH 3/5] Use the shared HB applied standard and cover WTC childcare residuals --- changelog.d/hb-qualified-childcare.fixed.md | 2 +- ...t_applicable_income_childcare_element.yaml | 94 +++++++++++++++++++ ...t_applied_standard_earnings_disregard.yaml | 10 ++ ...fit_applied_standard_earnings_disregard.py | 2 +- ...ousing_benefit_childcare_earnings_limit.py | 20 ++-- 5 files changed, 117 insertions(+), 11 deletions(-) create mode 100644 policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.yaml diff --git a/changelog.d/hb-qualified-childcare.fixed.md b/changelog.d/hb-qualified-childcare.fixed.md index cc904f76c9..54b1056887 100644 --- a/changelog.d/hb-qualified-childcare.fixed.md +++ b/changelog.d/hb-qualified-childcare.fixed.md @@ -1 +1 @@ -- Limited Housing Benefit childcare deductions to remaining earnings plus tax-credit payments while retaining childcare_expenses and the existing age-based cap approximation; deferred detailed qualifying-care rules without adding an input. +Limited Housing Benefit childcare deductions to remaining earnings after the shared applied standard plus tax-credit payments, retaining childcare_expenses and the existing age-based cap approximation; deferred detailed qualifying-care rules without adding an input. Prevented negative overridden net earnings from producing a negative permitted-work applied standard. diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml index 20bdad14bd..86edb6a8d1 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applicable_income_childcare_element.yaml @@ -1563,3 +1563,97 @@ # young person; the disability age extension does not create that status. num_relevant_children_for_housing_benefit_childcare: 0 housing_benefit_applicable_income_childcare_element: 0 + +- name: One relevant child and an older young person have no earnings or tax credits + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_parent: true + childcare_expenses: 30_000 + child: + age: 10 + young_person: + age: 17 + benunits: + benunit: + members: [claimant, child, young_person] + housing_benefit_net_earnings: 0 + housing_benefit_special_earnings_disregard: 0 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + working_tax_credit: 0 + child_tax_credit: 0 + output: + num_relevant_children_for_housing_benefit_childcare: 1 + # Reg 27(3) still selects min(30,000, 175 * 52) = 9,100. + # BW2 W2.179-W2.182: limit = max(0, 0 - 0 - 0) + 0 + 0 = 0; + # deduction = min(9,100, 0) = 0, despite the unchanged relevant-child count. + housing_benefit_childcare_earnings_limit: 0 + housing_benefit_applicable_income_childcare_element: 0 + +- name: Pre-2025 WTC disability award selects the special standard and funds childcare + period: 2024 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_parent: true + is_claimant_or_partner: true + childcare_expenses: 2_000 + sda: 0 + attendance_allowance: 0 + dla_sc: 0 + dla_m: 0 + pip_dl: 0 + pip_m: 0 + armed_forces_independence_payment: 0 + is_blind: false + partner: + age: 70 + is_claimant_or_partner: true + sda: 0 + attendance_allowance: 0 + dla_sc: 0 + dla_m: 0 + pip_dl: 0 + pip_m: 0 + armed_forces_independence_payment: 0 + is_blind: false + child: + age: 10 + is_child_or_young_person_for_legacy_benefits: true + benunits: + family: + members: [claimant, partner, child] + housing_benefit_net_earnings: 2_000 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_pension_age_regulations_apply: true + housing_benefit_on_passporting_benefit: false + meets_housing_benefit_additional_earnings_disregard_conditions: false + disability_premium: 0 + severe_disability_premium: 0 + carer_premium: 0 + # Pre-assessed award in payment and its pre-taper disability element; + # special eligibility and both standard variables remain calculated. + working_tax_credit: 500 + WTC_disabled_element: 3_900 + WTC_severely_disabled_element: 0 + child_tax_credit: 0 + output: + # SI 2006/214 Sch 4 para 5(1)(a)(vi): WTC disability receipt selects + # 20 * 52 = 1,040 (rather than the ordinary couple's 10 * 52 = 520). + # Reg 30(1)(c), (2)-(3), with DWP BW2 W2.179-W2.182: + # residual = max(0, 2,000 - 1,040 - 0) = 960; + # limit = 960 + 500 WTC + 0 CTC = 1,460; + # capped charge = min(2,000, 175 * 52) = 2,000; + # deduction = min(2,000, 1,460) = 1,460. + housing_benefit_special_earnings_disregard_eligible: true + housing_benefit_special_earnings_disregard: 1_040 + housing_benefit_applied_standard_earnings_disregard: 1_040 + housing_benefit_childcare_earnings_limit: 1_460 + housing_benefit_applicable_income_childcare_element: 1_460 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.yaml new file mode 100644 index 0000000000..13a6c3af85 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_applied_standard_earnings_disregard.yaml @@ -0,0 +1,10 @@ +- name: Negative overridden net earnings cannot produce a negative permitted-work standard + period: 2026 + input: + housing_benefit_net_earnings: -1 + housing_benefit_special_earnings_disregard: 0 + housing_benefit_permitted_work_disregard: 1 + is_lone_parent: false + output: + # min(max(-1, 0), max(1, 0 * 0)) = min(0, 1) = 0. + housing_benefit_applied_standard_earnings_disregard: 0 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 index a05e20dd7f..df3258a362 100644 --- 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 @@ -24,7 +24,7 @@ class housing_benefit_applied_standard_earnings_disregard(Variable): ) def formula(benunit, period, parameters): - net_earnings = benunit("housing_benefit_net_earnings", period) + net_earnings = max_(benunit("housing_benefit_net_earnings", period), 0) # Working age Sch 4 paras 3, 4, 7 and 10; pension age Sch 4 paras 2, # 4, 5 and 7. Other full-amount £20 routes are pre-assessed # (housing_benefit_special_earnings_disregard_eligible). diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py index 54ee4c6087..88af643f3c 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py @@ -3,14 +3,7 @@ def hb_standard_earnings_disregard(benunit, period): """Selected standard amount, including the permitted-work replacement.""" - net = max_(benunit("housing_benefit_net_earnings", period), 0) - standard = benunit("housing_benefit_special_earnings_disregard", period) - permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) - return where( - permitted > 0, - min_(net, max_(permitted, benunit("is_lone_parent", period) * standard)), - standard, - ) + return benunit("housing_benefit_applied_standard_earnings_disregard", period) def hb_childcare_earnings_limit(benunit, period, accommodation=None): @@ -43,7 +36,16 @@ class housing_benefit_childcare_earnings_limit(Variable): "disregard is conditional on earnings covering the childcare deduction " "as well as that disregard: when it applies the full capped childcare " "charge already fits, so this calculation need not depend on it. This " - "avoids a circular calculation through the childcare deduction." + "avoids a circular calculation through the childcare deduction. The " + "WTC edge also runs from this limit through " + "housing_benefit_applied_standard_earnings_disregard, " + "housing_benefit_special_earnings_disregard and " + "housing_benefit_special_earnings_disregard_eligible to " + "working_tax_credit and its disability elements. With fixed earnings, " + "WTC entitlement " + "and its components (including the WTC childcare element) do not " + "read the HB childcare deduction, limit or applicable income, so " + "neither this edge nor the direct WTC addition creates a cycle." ) reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/27", From 28f85f2a1202383d052563fbaf64a2e3f300e1c2 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 14:44:04 -0400 Subject: [PATCH 4/5] Test HB childcare residual invariants and monotonicity --- ...st_housing_benefit_childcare_properties.py | 274 ++++++++++++++++++ ...g_benefit_earnings_disregard_properties.py | 7 +- 2 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 policyengine_uk/tests/test_housing_benefit_childcare_properties.py diff --git a/policyengine_uk/tests/test_housing_benefit_childcare_properties.py b/policyengine_uk/tests/test_housing_benefit_childcare_properties.py new file mode 100644 index 0000000000..588340beee --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_childcare_properties.py @@ -0,0 +1,274 @@ +"""Housing Benefit childcare invariants for independently assessed inputs. + +C1: the deduction is nonnegative and no greater than either the qualifying +charge cap or the childcare earnings limit. +C2: increasing private pension or maintenance income cannot increase the +deduction when earnings, tax credits and the other assessed inputs are fixed. +C3: the limit is max(0, net earnings - applied standard - accommodation) plus +WTC and CTC, reading the applied standard from the shared model variable. +C4: increasing net earnings cannot lower the limit, and increasing childcare +charges cannot lower the deduction. + +Selected standards, accommodation and tax credits are pre-assessed inputs so +these properties isolate the earnings residual. Standards, accommodation, +charges and tax credits are nonnegative; net earnings may be negative and +permitted work may be negative to exercise their zero floors. The applied +standard is always calculated by the model, never supplied as an input. +Each population contains independent benefit units with actual adult/child +members; lone-parent status is supplied consistently with those members. +""" + +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 +TOLERANCE = 1e-6 +PROPERTY_SETTINGS = settings( + max_examples=5, + derandomize=True, + deadline=None, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +def _case(**overrides): + case = { + "net_earnings": 0, + "selected_standard": 0, + "permitted_work": 0, + "lone_parent": False, + "children": 1, + "childcare": 1_000, + "accommodation": 0, + "wtc": 0, + "ctc": 0, + "private_pension": 0, + "maintenance": 0, + } + case.update(overrides) + return case + + +CASES = st.fixed_dictionaries( + { + "net_earnings": st.integers(-100, 40_000), + "selected_standard": st.sampled_from([0, 260, 520, 1_040, 1_300]), + "permitted_work": st.sampled_from([-1, 0, 1, 1_040, 1_300, 9_542]), + "lone_parent": st.booleans(), + "children": st.integers(0, 2), + "childcare": st.integers(0, 25_000), + "accommodation": st.integers(0, 10_000), + "wtc": st.integers(0, 10_000), + "ctc": st.integers(0, 10_000), + "private_pension": st.integers(0, 20_000), + "maintenance": st.integers(0, 20_000), + } +) +POPULATIONS = st.lists(CASES, min_size=1, max_size=24) +EDGE_CASES = [ + _case(), + _case( + net_earnings=2_000, + selected_standard=1_300, + permitted_work=1, + lone_parent=True, + childcare=2_000, + ), + _case(net_earnings=-1, selected_standard=260, permitted_work=1), + _case( + selected_standard=1_040, + children=2, + childcare=25_000, + wtc=500, + ctc=1_000, + ), + _case( + net_earnings=10_000, + selected_standard=520, + children=0, + childcare=25_000, + ), + _case( + net_earnings=5_000, + selected_standard=1_040, + lone_parent=True, + childcare=25_000, + accommodation=8_000, + ), + _case( + net_earnings=30_000, + selected_standard=1_040, + children=2, + childcare=25_000, + wtc=1_000, + ), +] + + +def _evaluate(cases): + people = {} + benunits = {} + households = {} + for index, case in enumerate(cases): + # A unit with no children has no lone parent. Otherwise construct one + # adult for a lone parent and two adults for a couple. + lone_parent = case["lone_parent"] and case["children"] > 0 + members = [] + for adult_index in range(1 if lone_parent else 2): + name = f"adult_{index}_{adult_index}" + members.append(name) + people[name] = { + "age": {YEAR: 40}, + "is_claimant_or_partner": {YEAR: True}, + "is_child_or_young_person_for_legacy_benefits": {YEAR: False}, + "childcare_expenses": { + YEAR: case["childcare"] if adult_index == 0 else 0 + }, + "private_pension_income": { + YEAR: case["private_pension"] if adult_index == 0 else 0 + }, + "maintenance_income": { + YEAR: case["maintenance"] if adult_index == 0 else 0 + }, + } + for child_index in range(case["children"]): + name = f"child_{index}_{child_index}" + members.append(name) + people[name] = { + "age": {YEAR: 8}, + "is_claimant_or_partner": {YEAR: False}, + "is_child_or_young_person_for_legacy_benefits": {YEAR: True}, + } + benunits[f"benunit_{index}"] = { + "members": members, + "is_lone_parent": {YEAR: lone_parent}, + "housing_benefit_net_earnings": {YEAR: case["net_earnings"]}, + "housing_benefit_special_earnings_disregard": { + YEAR: case["selected_standard"] + }, + "housing_benefit_permitted_work_disregard": {YEAR: case["permitted_work"]}, + "housing_benefit_specified_or_temporary_accommodation_disregard": { + YEAR: case["accommodation"] + }, + "working_tax_credit": {YEAR: case["wtc"]}, + "child_tax_credit": {YEAR: case["ctc"]}, + } + households[f"household_{index}"] = {"members": members} + simulation = Simulation( + tax_benefit_system=system, + situation={ + "people": people, + "benunits": benunits, + "households": households, + }, + ) + result = { + "limit": simulation.calculate("housing_benefit_childcare_earnings_limit", YEAR), + "deduction": simulation.calculate( + "housing_benefit_applicable_income_childcare_element", YEAR + ), + "standard": simulation.calculate( + "housing_benefit_applied_standard_earnings_disregard", YEAR + ), + "count": simulation.calculate( + "num_relevant_children_for_housing_benefit_childcare", YEAR + ), + } + # The statutory charge ceiling is independent of the earnings formula. + charge_caps = np.where( + result["count"] > 1, + 300 * 52, + np.where(result["count"] == 1, 175 * 52, 0), + ) + result["capped_charge"] = np.minimum( + np.array([case["childcare"] for case in cases]), charge_caps + ) + return result + + +@PROPERTY_SETTINGS +@given(cases=POPULATIONS) +@example(cases=EDGE_CASES) +def test_c1_childcare_deduction_bounds(cases): + result = _evaluate(cases) + assert np.all(result["deduction"] >= -TOLERANCE) + assert np.all( + result["deduction"] + <= np.minimum(result["capped_charge"], result["limit"]) + TOLERANCE + ) + + +@PROPERTY_SETTINGS +@given( + cases=POPULATIONS, + increase=st.integers(1, 20_000), + income_kind=st.sampled_from(["private_pension", "maintenance"]), +) +@example(cases=EDGE_CASES, increase=1_000, income_kind="private_pension") +@example(cases=EDGE_CASES, increase=1_000, income_kind="maintenance") +def test_c2_unrelated_income_cannot_fund_childcare(cases, increase, income_kind): + baseline = _evaluate(cases) + increased = _evaluate( + [ + { + **case, + income_kind: case[income_kind] + increase, + } + for case in cases + ] + ) + assert np.all(increased["deduction"] <= baseline["deduction"] + TOLERANCE) + + +@PROPERTY_SETTINGS +@given(cases=POPULATIONS) +@example(cases=EDGE_CASES) +def test_c3_childcare_earnings_residual_identity(cases): + result = _evaluate(cases) + expected = ( + np.maximum( + 0, + np.array([case["net_earnings"] for case in cases]) + - result["standard"] + - np.array([case["accommodation"] for case in cases]), + ) + + np.array([case["wtc"] for case in cases]) + + np.array([case["ctc"] for case in cases]) + ) + np.testing.assert_allclose(result["limit"], expected, rtol=0, atol=TOLERANCE) + + +@PROPERTY_SETTINGS +@given(cases=POPULATIONS, increase=st.integers(1, 40_000)) +@example(cases=EDGE_CASES, increase=1_000) +def test_c4_net_earnings_cannot_lower_childcare_limit(cases, increase): + baseline = _evaluate(cases) + increased = _evaluate( + [ + { + **case, + "net_earnings": min(40_000, case["net_earnings"] + increase), + } + for case in cases + ] + ) + assert np.all(increased["limit"] + TOLERANCE >= baseline["limit"]) + + +@PROPERTY_SETTINGS +@given(cases=POPULATIONS, increase=st.integers(1, 25_000)) +@example(cases=EDGE_CASES, increase=1_000) +def test_c4_charges_cannot_lower_childcare_deduction(cases, increase): + baseline = _evaluate(cases) + increased = _evaluate( + [ + {**case, "childcare": min(25_000, case["childcare"] + increase)} + for case in cases + ] + ) + assert np.all(increased["deduction"] + TOLERANCE >= baseline["deduction"]) 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 34a3c4a973..62eab6de7b 100644 --- a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py @@ -66,6 +66,7 @@ import numpy as np from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st +from policyengine_core.simulations import Simulation as CoreSimulation from policyengine_uk import Simulation from policyengine_uk.system import system @@ -195,7 +196,11 @@ def situation(units, earnings_bump=0.0, income_support=0.0, age_map=None): def calculate(units, special_eligible=None, **kwargs): - sim = Simulation(situation=situation(units, **kwargs)) + # Fixed-earnings fixtures share the read-only rules, with fresh holders. + # Avoid rebuilding the UK model and cloning a baseline for every draw. + sim = CoreSimulation( + tax_benefit_system=system, situation=situation(units, **kwargs) + ) if special_eligible is not None: sim.set_input( "housing_benefit_special_earnings_disregard_eligible", From a1bcf93587d0cf33a93121994145a8ed646b52dd Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Wed, 28 Oct 2026 16:44:26 -0400 Subject: [PATCH 5/5] Keep the UK constructor in the earnings-disregard properties and name the no-cycle conditions Review r1 (APPROVE WITH NITS at 28f85f2a1), both P3s: - Restore policyengine_uk.Simulation in the existing calculate() helper of test_housing_benefit_earnings_disregard_properties.py, as on main. The core-constructor swap only beat a loaded host's local time cap and removed UK-wrapper coverage; it can be proposed separately. - The childcare limit's no-cycle note now says when earnings are fixed (no baseline, or zero labour supply income and substitution elasticities, the defaults) and that it does not cover reforms turning those responses on. Co-Authored-By: Claude Opus 5.5 --- .../test_housing_benefit_earnings_disregard_properties.py | 7 +------ .../housing_benefit_childcare_earnings_limit.py | 7 ++++++- 2 files changed, 7 insertions(+), 7 deletions(-) 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 62eab6de7b..34a3c4a973 100644 --- a/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py +++ b/policyengine_uk/tests/test_housing_benefit_earnings_disregard_properties.py @@ -66,7 +66,6 @@ import numpy as np from hypothesis import HealthCheck, example, given, settings from hypothesis import strategies as st -from policyengine_core.simulations import Simulation as CoreSimulation from policyengine_uk import Simulation from policyengine_uk.system import system @@ -196,11 +195,7 @@ def situation(units, earnings_bump=0.0, income_support=0.0, age_map=None): def calculate(units, special_eligible=None, **kwargs): - # Fixed-earnings fixtures share the read-only rules, with fresh holders. - # Avoid rebuilding the UK model and cloning a baseline for every draw. - sim = CoreSimulation( - tax_benefit_system=system, situation=situation(units, **kwargs) - ) + sim = Simulation(situation=situation(units, **kwargs)) if special_eligible is not None: sim.set_input( "housing_benefit_special_earnings_disregard_eligible", diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py index 88af643f3c..8fa5661d04 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_childcare_earnings_limit.py @@ -45,7 +45,12 @@ class housing_benefit_childcare_earnings_limit(Variable): "WTC entitlement " "and its components (including the WTC childcare element) do not " "read the HB childcare deduction, limit or applicable income, so " - "neither this edge nor the direct WTC addition creates a cycle." + "neither this edge nor the direct WTC addition creates a cycle. " + "Earnings are fixed when the simulation has no baseline or the " + "labour supply income and substitution elasticities are zero, as " + "they are by default. Labour supply responses can make earnings " + "depend on benefits, so this note does not cover a reform that " + "turns them on." ) reference = ( "https://www.legislation.gov.uk/uksi/2006/213/regulation/27",