diff --git a/changelog.d/hb-accommodation-dates.fixed.md b/changelog.d/hb-accommodation-dates.fixed.md new file mode 100644 index 0000000000..dc4921837d --- /dev/null +++ b/changelog.d/hb-accommodation-dates.fixed.md @@ -0,0 +1 @@ +- Applied Housing Benefit accommodation earnings disregards from their statutory commencement within the first fiscal year, calculating awards before combining date segments. diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/minors.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/minors.yaml index b44d66a347..06d3a3ab8a 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/minors.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/minors.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 97.33 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, couple both under 18 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/older.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/older.yaml index 93d44c2c49..cc5cde75ce 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/older.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/older.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 119.7 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, couple with one member 25 or over diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/younger.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/younger.yaml index 0e156837f2..e17570f62f 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/younger.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/couple/younger.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 61.53 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, couple both under 25, one 18 or over diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/older.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/older.yaml index a038a6223f..584388350a 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/older.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/older.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 77.73 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, lone parent aged 25 or over diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/younger.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/younger.yaml index 4875f03194..4a858af7ec 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/younger.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/lone_parent/younger.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 61.41 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, lone parent under 25 diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/older.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/older.yaml index 63bc144f20..62a3ddf015 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/older.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/older.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 77.73 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, single claimant aged 25 or over diff --git a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/younger.yaml b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/younger.yaml index 10cfa31c01..99591d498e 100644 --- a/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/younger.yaml +++ b/policyengine_uk/parameters/gov/dwp/housing_benefit/means_test/income_disregard/specified_or_temporary_accommodation/single/younger.yaml @@ -3,15 +3,13 @@ values: # Schedule 4 paragraph 18 (NI: Schedule 5 paragraph 18) has effect from 5 # October 2026: SI 2026/753 reg. 2(4), as substituted by SI 2026/978 reg. # 2(2), and NI SR 2026/157 reg. 2(4). There was no such disregard before. - # Neither instrument uprates the sums. The model reads each fiscal year at - # 30 April, so 2026 (2026-27) has no paragraph 18 disregard although it - # applied from 5 October 2026 (183 of the year's 365 days); 2027 onwards - # has the full amounts. fiscal_year_blend is not used: half of an amount - # for the whole year is not the full amount for half the year, because - # the disregard is capped at net earnings and counts in the £17.10 test. + # Neither instrument uprates the sums. Preserve the legal date so awards + # can be calculated separately before/after commencement, including the + # earnings cap and additional-disregard test, then combined over the year. 2006-03-06: 0 2026-10-05: 61.41 metadata: + preserve_calendar_dates: true period: week unit: currency-GBP label: Housing benefit specified or temporary accommodation earnings disregard, single claimant under 25 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.yaml index d6027e8a89..21aad0ceb0 100644 --- a/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.yaml +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.yaml @@ -20,8 +20,9 @@ # 5,061.16; £61.53 → 3,199.56; £119.70 → 6,224.40; £5 → 260; £10 → 520; £25 # → 1,300; £17.10 → 889.20. # -# The model reads each fiscal year at 30 April: 2027 (2027-28) has para 18 in -# full; 2026 (2026-27) reads 30 April 2026, before 5 October, so it has none. +# The accommodation amount retains its legal date: 2026-27 has 182 days +# before 5 October and 183 days afterwards. Completed awards are combined +# after earnings caps, childcare, the income threshold and taper. # Earnings are below the income tax personal allowance and the National # Insurance primary threshold (both £12,570), so net earnings equal gross # earnings, and the hours are below every £17.10 work condition unless a @@ -401,7 +402,7 @@ housing_benefit_specified_or_temporary_accommodation_disregard: 0 housing_benefit_applicable_income_disregard: 260 -- name: In 2026 the model reads 30 April 2026, before para 18 came into force +- name: The 2026 fiscal year includes the 183 days from 5 October period: 2026 absolute_error_margin: 0.01 input: @@ -415,11 +416,9 @@ members: [person] in_specified_or_temporary_accommodation: true output: - # KNOWN LIMITATION, intended: para 18 applies from 5 October 2026, for 183 - # of the 365 days of 2026-27, but the model reads the year's law at 30 - # April, so 2026-27 has no para 18 disregard. - housing_benefit_specified_or_temporary_accommodation_disregard: 0 - housing_benefit_applicable_income_disregard: 260 # £5 × 52 + # Full £77.73 × 52 annual equivalent for 183/365 of the year. + housing_benefit_specified_or_temporary_accommodation_disregard: 2026.51693 + housing_benefit_applicable_income_disregard: 2286.51693 - name: In 2025 there is no para 18 disregard period: 2025 @@ -680,9 +679,9 @@ age: 30 employment_income: 7_800 # £150 a week weekly_hours: 16 - childcare_expenses: 2_080 # £40 a week child: age: 5 + childcare_expenses: 2_080 # £40/week; detailed qualifying-care rules deferred benunits: benunit: would_claim_uc: false # not on Universal Credit, which passports the means test @@ -781,3 +780,470 @@ # 10,400 − 0.65 × (10,140 − 4,968.60) = 10,400 − 3,361.41; 2,627.27 less # than above, which is 0.65 × 4,041.96. housing_benefit_entitlement: 7_038.59 + +# First-year award regressions: GB SI 2026/753 reg 1 and SI 2026/978 reg 2; +# NI SR 2026/157 regs 1/2; HB reg 71 and NI reg 69. Household facts stay +# constant; statutory weekly amounts × 52 apply within each date segment. + +- name: GB and NI working-age awards include the first year while pension-age awards do not + period: 2026 + absolute_error_margin: 0.01 + input: + age: [30,30,30,30,30,30,30,30] + is_claimant_or_partner: [true,true,true,true,true,true,true,true] + is_lone_parent: [false,false,false,false,false,false,false,false] + is_couple: [false,false,false,false,false,false,false,false] + employment_income: [5000,5000,5000,5000,5000,5000,5000,5000] + income_tax: [0,0,0,0,0,0,0,0] + national_insurance: [0,0,0,0,0,0,0,0] + pension_contributions: [0,0,0,0,0,0,0,0] + housing_benefit_net_earnings: [5000,5000,5000,5000,5000,5000,5000,5000] + housing_benefit_special_earnings_disregard: [260,260,260,260,260,260,260,260] + housing_benefit_permitted_work_disregard: [0,0,0,0,0,0,0,0] + housing_benefit_pension_age_regulations_apply: [false,false,false,false,true,true,true,true] + housing_benefit_on_passporting_benefit: [false,false,false,false,false,false,false,false] + housing_benefit_tariff_income: [0,0,0,0,0,0,0,0] + guarantee_credit: [0,0,0,0,0,0,0,0] + income_support: [0,0,0,0,0,0,0,0] + jsa_income: [0,0,0,0,0,0,0,0] + esa_income: [0,0,0,0,0,0,0,0] + tax_credits: [0,0,0,0,0,0,0,0] + housing_benefit_applicable_income_childcare_element: [0,0,0,0,0,0,0,0] + meets_housing_benefit_additional_earnings_disregard_conditions: [false,false,false,false,false,false,false,false] + housing_benefit_applicable_amount: [0,0,0,0,0,0,0,0] + in_specified_or_temporary_accommodation: [true,true,true,true,true,true,true,true] + benunit_rent: [10400,10400,10400,10400,10400,10400,10400,10400] + LHA_eligible: [false,false,false,false,false,false,false,false] + housing_benefit_meals_deduction: [0,0,0,0,0,0,0,0] + housing_benefit_non_dep_deductions: [0,0,0,0,0,0,0,0] + country: ["ENGLAND","WALES","SCOTLAND","NORTHERN_IRELAND","ENGLAND","WALES","SCOTLAND","NORTHERN_IRELAND"] + output: + # Before: 10,400 - 65% × 4,740 = 7,319; after: 10,400 - 65% × 698.04 = 9,946.274. + housing_benefit_entitlement: [8636.236005479452,8636.236005479452,8636.236005479452,8636.236005479452,7319,7319,7319,7319] + +- name: Low earnings cross the income threshold so completed awards must be combined + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 1000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 1000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 500 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + # Before: 10,400 - 65% × (740 - 500) = 10,244; after: full eligible rent. + housing_benefit_applicable_income_disregard: 631.013698630137 + housing_benefit_entitlement: 10322.213698630138 + +- name: The zero-award floor applies before annual integration + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 7000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 7000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 2000 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + # Before: max(0, 2,000 - 65% × 6,740) = 0; after: 2,000 - 65% × 2,698.04 = 246.274. + housing_benefit_entitlement: 123.47436164383558 + +- name: Zero earnings cannot create an accommodation deduction + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 0 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 0 + housing_benefit_special_earnings_disregard: 0 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + # No earner and no net earnings; income is nil and the award cannot exceed eligible rent. + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + housing_benefit_applicable_income_disregard: 0 + housing_benefit_entitlement: 10400 + +- name: ENGLAND childcare_expenses uses the remaining earnings in each date segment + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + child: + age: 5 + is_claimant_or_partner: false + is_child_or_young_person_for_legacy_benefits: true + childcare_expenses: 1000 + benunits: + family: + members: [parent, child] + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 1300 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + households: + household: + members: [parent, child] + country: ENGLAND + output: + # Before: £1,300 standard + £1,000 childcare gives £2,700 income. + # After: accommodation takes all remaining £3,700 earnings, leaving + # no childcare deduction and no assessable income. HB reg 27/71; + # NI regs 24/69: (8,645 × 182 + 10,400 × 183) / 365. + housing_benefit_entitlement: 9524.904109589042 + +- name: NORTHERN_IRELAND childcare_expenses uses the remaining earnings in each date segment + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + child: + age: 5 + is_claimant_or_partner: false + is_child_or_young_person_for_legacy_benefits: true + childcare_expenses: 1000 + benunits: + family: + members: [parent, child] + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 1300 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + working_tax_credit: 0 + child_tax_credit: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + households: + household: + members: [parent, child] + country: NORTHERN_IRELAND + output: + # Before: £1,300 standard + £1,000 childcare gives £2,700 income. + # After: accommodation takes all remaining £3,700 earnings, leaving + # no childcare deduction and no assessable income. HB reg 27/71; + # NI regs 25/69: (8,645 × 182 + 10,400 × 183) / 365. + housing_benefit_entitlement: 9524.904109589042 + + +- name: An explicit annual applicable-income input is not recalculated + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + housing_benefit_applicable_income: 1000 + output: + # Keep supplied income: 10,400 - 65% × 1,000 = 9,750 in both segments. + housing_benefit_entitlement: 9750 + +- name: An explicit annual zero disregard is not overwritten by segmentation + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + housing_benefit_applicable_income_disregard: 0 + output: + # Keep supplied disregard: 10,400 - 65% × 5,000 = 7,150 in both segments. + housing_benefit_entitlement: 7150 + +- name: An explicit annual childcare deduction is retained inside each award calculation + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 1000 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + # Before income: 3,740; after income is floored at zero after the supplied £1,000 childcare deduction. + housing_benefit_entitlement: 9187.830136986302 + +- name: The additional disregard earnings test uses the full accommodation amount + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: true + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + # Before: £260 + £889.20 qualifies. After: £5,000 is below £260 + £4,041.96 + £889.20. + housing_benefit_applicable_income_disregard: 2729.8988493150687 + housing_benefit_entitlement: 8924.434252054794 + +- name: A supplied zero accommodation amount preserves its override + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: false + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + housing_benefit_specified_or_temporary_accommodation_disregard: 0 + output: + # The supplied full-year zero leaves only the standard £260 disregard. + housing_benefit_applicable_income_disregard: 260 + housing_benefit_entitlement: 7319 + +- name: Working-age passporting remains effective across the date boundary + period: 2026 + absolute_error_margin: 0.01 + input: + age: 30 + is_claimant_or_partner: true + is_lone_parent: false + is_couple: false + employment_income: 5000 + income_tax: 0 + national_insurance: 0 + pension_contributions: 0 + housing_benefit_net_earnings: 5000 + housing_benefit_special_earnings_disregard: 260 + housing_benefit_permitted_work_disregard: 0 + housing_benefit_pension_age_regulations_apply: false + housing_benefit_on_passporting_benefit: true + housing_benefit_tariff_income: 0 + guarantee_credit: 0 + income_support: 0 + jsa_income: 0 + esa_income: 0 + tax_credits: 0 + housing_benefit_applicable_income_childcare_element: 0 + meets_housing_benefit_additional_earnings_disregard_conditions: false + housing_benefit_applicable_amount: 0 + in_specified_or_temporary_accommodation: true + benunit_rent: 10400 + LHA_eligible: false + housing_benefit_meals_deduction: 0 + housing_benefit_non_dep_deductions: 0 + output: + # The whole income remains disregarded; no tapered assessment is introduced. + housing_benefit_applicable_income_disregard: 5000 + housing_benefit_entitlement: 10400 diff --git a/policyengine_uk/tests/test_housing_benefit_accommodation_dates.py b/policyengine_uk/tests/test_housing_benefit_accommodation_dates.py new file mode 100644 index 0000000000..1c941b01a7 --- /dev/null +++ b/policyengine_uk/tests/test_housing_benefit_accommodation_dates.py @@ -0,0 +1,222 @@ +"""Statutory dates and reform compatibility for HB accommodation disregards.""" + +from datetime import date + +import pytest + +from policyengine_uk import CountryTaxBenefitSystem, Simulation +from policyengine_uk.model_api import Variable, BenUnit, YEAR + + +PREFIX = ( + "gov.dwp.housing_benefit.means_test.income_disregard." + "specified_or_temporary_accommodation" +) + + +@pytest.fixture(scope="module") +def system(): + return CountryTaxBenefitSystem() + + +def node(parameters): + return parameters.gov.dwp.housing_benefit.means_test.income_disregard.specified_or_temporary_accommodation + + +@pytest.mark.parametrize( + "family,age,amount", + [ + ("single", "younger", 61.41), + ("single", "older", 77.73), + ("lone_parent", "younger", 61.41), + ("lone_parent", "older", 77.73), + ("couple", "minors", 97.33), + ("couple", "younger", 61.53), + ("couple", "older", 119.70), + ], +) +def test_processed_amounts_keep_5_october_commencement(system, family, age, amount): + """GB SI 2026/753 and NI SR 2026/157 commence on 5 October, not January.""" + leaf = node(system.parameters).children[family].children[age] + assert leaf("2026-10-04") == 0 + assert leaf("2026-10-05") == amount + + +def situation(years=(2026,)): + return { + "people": { + "person1": { + "age": {year: 30 for year in years}, + "employment_income": {year: 1_000 for year in years}, + "income_tax": {year: 0 for year in years}, + "national_insurance": {year: 0 for year in years}, + "pension_contributions": {year: 0 for year in years}, + } + }, + "benunits": { + "family": { + "members": ["person1"], + "in_specified_or_temporary_accommodation": { + year: True for year in years + }, + "housing_benefit_net_earnings": {year: 1_000 for year in years}, + "housing_benefit_special_earnings_disregard": { + year: 260 for year in years + }, + "housing_benefit_applicable_amount": {year: 500 for year in years}, + "housing_benefit_on_passporting_benefit": { + year: False for year in years + }, + "housing_benefit_pension_age_regulations_apply": { + year: False for year in years + }, + "housing_benefit_tariff_income": {year: 0 for year in years}, + "housing_benefit_applicable_income_childcare_element": { + year: 0 for year in years + }, + "meets_housing_benefit_additional_earnings_disregard_conditions": { + year: False for year in years + }, + "benunit_rent": {year: 10_400 for year in years}, + "housing_benefit_meals_deduction": {year: 0 for year in years}, + "housing_benefit_non_dep_deductions": {year: 0 for year in years}, + "LHA_eligible": {year: False for year in years}, + "income_support": {year: 0 for year in years}, + "jsa_income": {year: 0 for year in years}, + "esa_income": {year: 0 for year in years}, + "tax_credits": {year: 0 for year in years}, + } + }, + "households": {"household": {"members": ["person1"]}}, + } + + +def test_first_year_combines_completed_awards(): + """Reg 71: average the awards, not the income before its £500 threshold.""" + simulation = Simulation(situation=situation()) + before = 10_400 - 0.65 * (1_000 - 260 - 500) + after = 10_400 + days_before = (date(2026, 10, 5) - date(2026, 4, 6)).days + days_after = (date(2027, 4, 6) - date(2026, 10, 5)).days + expected = (before * days_before + after * days_after) / 365 + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx(expected, abs=0.01) + + +def test_bare_year_zero_reform_has_fiscal_year_boundaries(): + """A 2026 reform must not leak into the following 2027-28 fiscal year.""" + simulation = Simulation( + situation=situation((2025, 2026, 2027)), + reform={f"{PREFIX}.single.older": {"2026": 0}}, + ) + amounts = [ + simulation.calculate( + "housing_benefit_specified_or_temporary_accommodation_disregard", year + )[0] + for year in (2025, 2026, 2027) + ] + assert amounts == pytest.approx([0, 0, 77.73 * 52], abs=0.01) + + +def test_dated_zero_reform_is_not_overwritten(): + simulation = Simulation( + situation=situation(), + reform={f"{PREFIX}.single.older": {"2026-10-05.2027-04-05": 0}}, + ) + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx(10_400 - 0.65 * (1_000 - 260 - 500), abs=0.01) + + +def test_later_supplied_income_keeps_its_value(): + simulation = Simulation(situation=situation()) + simulation.set_input("housing_benefit_applicable_income", 2026, [2_000]) + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx(10_400 - 0.65 * (2_000 - 500), abs=0.01) + + +def test_single_day_zero_reform_changes_only_that_day(): + simulation = Simulation( + situation=situation(), + reform={f"{PREFIX}.single.older": {"2026-10-05": 0}}, + ) + before = 10_400 - 0.65 * (1_000 - 260 - 500) + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx((before * 183 + 10_400 * 182) / 365, abs=0.01) + + +def test_only_supplied_period_overrides_recalculation(): + data = situation((2025, 2026)) + data["benunits"]["family"]["housing_benefit_applicable_income"] = {2025: 2_000} + simulation = Simulation(situation=data) + before = 10_400 - 0.65 * (1_000 - 260 - 500) + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx((before * 182 + 10_400 * 183) / 365, abs=0.01) + + +def test_replaced_income_formula_is_not_bypassed(): + class housing_benefit_applicable_income(Variable): + entity = BenUnit + definition_period = YEAR + value_type = float + + def formula(benunit, period, parameters): + return benunit.filled_array(2_000) + + simulation = Simulation(situation=situation()) + simulation.tax_benefit_system.update_variable(housing_benefit_applicable_income) + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx(10_400 - 0.65 * (2_000 - 500), abs=0.01) + + +def test_neutralized_income_is_not_recalculated(): + simulation = Simulation(situation=situation()) + simulation.tax_benefit_system.neutralize_variable( + "housing_benefit_applicable_income" + ) + assert simulation.calculate("housing_benefit_entitlement", 2026)[0] == 10_400 + + +def test_replaced_disregard_formula_is_not_bypassed(): + class housing_benefit_applicable_income_disregard(Variable): + entity = BenUnit + definition_period = YEAR + value_type = float + + def formula(benunit, period, parameters): + return benunit.filled_array(100) + + simulation = Simulation(situation=situation()) + simulation.tax_benefit_system.update_variable( + housing_benefit_applicable_income_disregard + ) + assert simulation.calculate("housing_benefit_entitlement", 2026)[ + 0 + ] == pytest.approx(10_400 - 0.65 * (1_000 - 100 - 500), abs=0.01) + + +def test_calculation_does_not_reparent_or_edit_parameter_leaves(): + simulation = Simulation(situation=situation()) + schedule = node(simulation.tax_benefit_system.parameters) + parents = { + leaf.name: leaf.parent + for family in ("single", "lone_parent", "couple") + for leaf in schedule.children[family].children.values() + } + values = { + leaf.name: [(value.instant_str, value.value) for value in leaf.values_list] + for family in ("single", "lone_parent", "couple") + for leaf in schedule.children[family].children.values() + } + simulation.calculate("housing_benefit_entitlement", 2026) + for family in ("single", "lone_parent", "couple"): + for leaf in schedule.children[family].children.values(): + assert leaf.parent is parents[leaf.name] + assert [ + (value.instant_str, value.value) for value in leaf.values_list + ] == values[leaf.name] diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py index 8b12285381..b5d5ad0c89 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income_disregard.py @@ -1,4 +1,73 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income.housing_benefit_applicable_income_childcare_element import ( + hb_capped_childcare_charges, + housing_benefit_applicable_income_childcare_element, +) +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income.housing_benefit_childcare_earnings_limit import ( + hb_childcare_earnings_limit, + hb_standard_earnings_disregard, + housing_benefit_childcare_earnings_limit, +) +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income.housing_benefit_specified_or_temporary_accommodation_disregard import ( + hb_accommodation_segments, + hb_uses_annual_override, +) + + +def hb_segment_deductions(benunit, period, parameters, accommodation): + """Apply childcare/earnings limits to one full accommodation amount.""" + net = max_(benunit("housing_benefit_net_earnings", period), 0) + standard = hb_standard_earnings_disregard(benunit, period) + accommodation = min_(max_(accommodation, 0), max_(net - standard, 0)) + childcare_name = "housing_benefit_applicable_income_childcare_element" + if hb_uses_annual_override( + benunit, + period, + childcare_name, + housing_benefit_applicable_income_childcare_element.formula, + ): + childcare = benunit(childcare_name, period) + else: + limit_name = "housing_benefit_childcare_earnings_limit" + if hb_uses_annual_override( + benunit, + period, + limit_name, + housing_benefit_childcare_earnings_limit.formula, + ): + childcare_limit = benunit(limit_name, period) + else: + childcare_limit = hb_childcare_earnings_limit( + benunit, period, accommodation + ) + childcare = min_( + hb_capped_childcare_charges(benunit, period, parameters), childcare_limit + ) + name = "housing_benefit_applicable_income_disregard" + if hb_uses_annual_override( + benunit, period, name, housing_benefit_applicable_income_disregard.formula + ): + return benunit(name, period), childcare + p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard + additional_amount = p.worker * WEEKS_IN_YEAR + # Compare in pence, including equality at a float32 earnings boundary. + covers_additional = np.round(net.astype(float), 2) >= np.round( + (standard + accommodation + childcare + additional_amount).astype(float), 2 + ) + additional = where( + benunit( + "meets_housing_benefit_additional_earnings_disregard_conditions", period + ) + & covers_additional, + additional_amount, + 0, + ) + disregard = where( + benunit("housing_benefit_on_passporting_benefit", period), + net, + standard + accommodation + additional, + ) + return disregard, childcare class housing_benefit_applicable_income_disregard(Variable): @@ -26,6 +95,10 @@ class housing_benefit_applicable_income_disregard(Variable): "(housing_benefit_permitted_work_disregard) replaces the ordinary " "standard amount, retaining a higher lone-parent disregard. It does " "not replace the separate accommodation or additional disregard." + " The annual result combines the capped deductions calculated with " + "the full accommodation amount in each effective-date segment. " + "Entitlement applies its income threshold and taper within those " + "segments rather than using an averaged disregard as a weekly rule." ) definition_period = YEAR unit = GBP @@ -45,67 +118,9 @@ class housing_benefit_applicable_income_disregard(Variable): ) def formula(benunit, period, parameters): - p = parameters(period).gov.dwp.housing_benefit.means_test.income_disregard - net_earnings = benunit("housing_benefit_net_earnings", period) - # Working age Sch 4 paras 4, 7 and 10; pension age Sch 4 paras 2 and 7. - standard = benunit("housing_benefit_special_earnings_disregard", period) - # Working-age para 10A(2) / pension-age para 5A(2): replace the - # ordinary amount, except for a higher lone-parent amount. The input - # already applies the statutory work and partner-allocation limits. - permitted = max_(benunit("housing_benefit_permitted_work_disregard", period), 0) - standard = where( - permitted > 0, - min_( - net_earnings, - max_(permitted, benunit("is_lone_parent", period) * standard), - ), - standard, - ) - # Working age Sch 4 para 18 (NI Sch 5 para 18), from 5 October 2026: - # specified or temporary accommodation. It applies alongside paras 3 - # to 10A, none of which excludes it, and takes the earnings they - # leave. - accommodation = min_( - benunit( - "housing_benefit_specified_or_temporary_accommodation_disregard", - period, - ), - net_earnings - standard, - ) - # Working age Sch 4 para 17; pension age Sch 4 para 9. - additional_amount = p.worker * WEEKS_IN_YEAR - childcare = benunit( - "housing_benefit_applicable_income_childcare_element", period - ) - # "Equal or exceed", compared in pence: net earnings are float32, so - # exactly £1,149.20 is held as £1,149.19995, and an exact comparison - # with the total fails at equality. From 5 October 2026 the total - # includes the para 18 amount (para 17(3)(a), "paragraphs 3 to 10A - # and 18"); it is zero under the pension-age para 9(3)(a). - covers_additional = np.round(net_earnings.astype(float), 2) >= np.round( - (standard + accommodation + childcare + additional_amount).astype(float), - 2, - ) - additional = where( - benunit( - "meets_housing_benefit_additional_earnings_disregard_conditions", period + return sum( + hb_segment_deductions(benunit, period, parameters, accommodation)[0] * share + for accommodation, share in hb_accommodation_segments( + benunit, period, parameters ) - & covers_additional, - additional_amount, - 0, - ) - # Working age Sch 4 para 12 (NI: SR 2006/405 Sch 5 para 12): "Where a - # claimant is on universal credit, income support, an income-based - # jobseeker's allowance or an income-related employment and support - # allowance, his earnings." The working-age Regulations apply at any - # age where the claimant or partner is on one of these benefits (SI - # 2006/213 reg 5(1)(b); SI 2006/214 reg 5(2)), so there is no age - # condition. - on_passporting_benefit = benunit( - "housing_benefit_on_passporting_benefit", period - ) - return where( - on_passporting_benefit, - net_earnings, - standard + accommodation + additional, ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.py index 401d1c6f55..1218ad5f38 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_specified_or_temporary_accommodation_disregard.py @@ -1,4 +1,89 @@ from policyengine_uk.model_api import * +from policyengine_core.parameters import ParameterNode +from policyengine_uk.utils.excise import fiscal_year_segments + + +def hb_uses_annual_override(benunit, period, variable_name, default_formula): + """Keep supplied annual values and replacement formulas out of recalculation.""" + simulation = benunit.simulation + variable = simulation.tax_benefit_system.get_variable(variable_name) + formula = variable.get_formula(period) + if ( + variable.is_neutralized + or formula is None + or formula.__code__ != default_formula.__code__ + ): + return True + # Core records set_input separately from computed caches. Reading those + # records avoids treating a cached formula result as an explicit override. + branch = simulation + visible = {"default"} + while branch is not None: + visible.add(branch.branch_name) + branch = getattr(branch, "parent_branch", None) + holder = benunit.get_holder(variable_name) + return any( + name == variable_name + and input_period == period + and branch_name in visible + and holder._get_array_from_storage(input_period, branch_name) is not None + for name, branch_name, input_period in getattr( + simulation, "_user_input_keys", () + ) + ) + + +def hb_accommodation_segments(benunit, period, parameters): + """Full annual-equivalent accommodation amounts and fiscal-year weights.""" + name = "housing_benefit_specified_or_temporary_accommodation_disregard" + if hb_uses_annual_override( + benunit, + period, + name, + housing_benefit_specified_or_temporary_accommodation_disregard.formula, + ): + yield benunit(name, period), 1 + return + raw = benunit.simulation.tax_benefit_system.parameters + accommodation = raw.gov.dwp.housing_benefit.means_test.income_disregard.specified_or_temporary_accommodation + schedule = ParameterNode("hb_accommodation_schedule", data={}) + for family in ("single", "lone_parent", "couple"): + for age, leaf in accommodation.children[family].children.items(): + # add_child sets the parent: clone leaves, never modify the model + # tree or replace values supplied by a parameter reform. + schedule.add_child(f"{family}_{age}", leaf.clone()) + p = parameters( + period + ).gov.dwp.housing_benefit.means_test.income_disregard.specified_or_temporary_accommodation + person = benunit.members + earner = ( + (person("employment_income", period) > 0) + | (person("self_employment_income", period) != 0) + | (person("employment_benefits", period) > 0) + ) + has_earner = benunit.any(person("is_claimant_or_partner", period) & earner) + eldest = benunit("eldest_claimant_or_partner_age", period) + older = eldest >= p.age_threshold.older + applies = ( + benunit("in_specified_or_temporary_accommodation", period) + & has_earner + & ~benunit("housing_benefit_pension_age_regulations_apply", period) + ) + for rates, share in fiscal_year_segments(schedule, period.start.year): + couple_amount = select( + [older, eldest >= p.age_threshold.younger], + [rates.couple_older, rates.couple_younger], + default=rates.couple_minors, + ) + weekly = select( + [benunit("is_lone_parent", period), benunit("is_couple", period)], + [ + where(older, rates.lone_parent_older, rates.lone_parent_younger), + couple_amount, + ], + default=where(older, rates.single_older, rates.single_younger), + ) + yield where(applies, weekly * WEEKS_IN_YEAR, 0), share class housing_benefit_specified_or_temporary_accommodation_disregard(Variable): @@ -17,8 +102,10 @@ class housing_benefit_specified_or_temporary_accommodation_disregard(Variable): "from the partner's. This is the full amount; " "housing_benefit_applicable_income_disregard adds it to the other " "disregards and caps the total at net earnings. The pension-age " - "Regulations have no such disregard. The model reads each fiscal year " - "at 30 April, so it is zero for 2026-27 and in full from 2027-28." + "Regulations have no such disregard. This annual output combines " + "the full amounts over their effective-date segments. The earnings " + "limits and award are calculated separately within each segment, " + "with annual earnings and household circumstances held constant." ) definition_period = YEAR unit = GBP @@ -31,43 +118,7 @@ class housing_benefit_specified_or_temporary_accommodation_disregard(Variable): ) def formula(benunit, period, parameters): - p = parameters( - period - ).gov.dwp.housing_benefit.means_test.income_disregard.specified_or_temporary_accommodation - person = benunit.members - # Para 18(1)(b): the claimant or partner is an employed earner or a - # self-employed earner. Anyone with earnings is, including statutory - # sick or maternity pay from a continuing employment and a - # self-employment loss. The amount is capped at net earnings in - # housing_benefit_applicable_income_disregard, so a family without - # net earnings gets nothing either way. - earner = ( - (person("employment_income", period) > 0) - | (person("self_employment_income", period) != 0) - | (person("employment_benefits", period) > 0) - ) - has_earner = benunit.any(person("is_claimant_or_partner", period) & earner) - # Para 18(2): the claimant's age, or the elder member's for a couple. - eldest = benunit("eldest_claimant_or_partner_age", period) - older = eldest >= p.age_threshold.older - couple_amount = select( - [older, eldest >= p.age_threshold.younger], - [p.couple.older, p.couple.younger], - default=p.couple.minors, - ) - weekly_amount = select( - [benunit("is_lone_parent", period), benunit("is_couple", period)], - [ - where(older, p.lone_parent.older, p.lone_parent.younger), - couple_amount, - ], - default=where(older, p.single.older, p.single.younger), - ) - # Para 18(1)(a), and only in the working-age Regulations (SI 2006/213; - # NI SR 2006/405): SI 2006/214 and SR 2006/406 are not amended. - applies = ( - benunit("in_specified_or_temporary_accommodation", period) - & has_earner - & ~benunit("housing_benefit_pension_age_regulations_apply", period) + return sum( + amount * share + for amount, share in hb_accommodation_segments(benunit, period, parameters) ) - return where(applies, weekly_amount * WEEKS_IN_YEAR, 0) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py b/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py index 553deed221..84690e73aa 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/entitlement/housing_benefit_entitlement.py @@ -1,4 +1,15 @@ from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income.housing_benefit_applicable_income import ( + hb_income_before_deductions, + housing_benefit_applicable_income, +) +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income.housing_benefit_applicable_income_disregard import ( + hb_segment_deductions, +) +from policyengine_uk.variables.gov.dwp.housing_benefit.applicable_income.housing_benefit_specified_or_temporary_accommodation_disregard import ( + hb_accommodation_segments, + hb_uses_annual_override, +) class housing_benefit_entitlement(Variable): @@ -9,7 +20,14 @@ class housing_benefit_entitlement(Variable): "income over the applicable amount. Where the Local Housing Allowance " "applies, the eligible rent is the maximum rent (LHA), which is the " "lower of the LHA rate and the rent, so the cap applies before the " - "taper." + "taper. When accommodation earnings disregards change within the " + "fiscal year, calculate income floors, childcare/earnings limits, " + "the income threshold, taper and zero-award floor separately in " + "each segment, then combine completed awards. Annual earnings, rent, " + "ages and household status remain constant. Other parameters keep " + "the model's annual convention. Supplied annual income/deductions " + "and replacement formulas retain their normal override semantics. " + "Annual income intermediates are not period-specific assessments." ) entity = BenUnit definition_period = YEAR @@ -34,6 +52,9 @@ class housing_benefit_entitlement(Variable): "https://www.legislation.gov.uk/nisr/2006/406/regulation/14D", "https://www.legislation.gov.uk/nisr/2006/406/regulation/48", "https://www.legislation.gov.uk/nisr/2006/406/regulation/49", + "https://www.legislation.gov.uk/uksi/2026/753/regulation/1", + "https://www.legislation.gov.uk/uksi/2026/978/regulation/2", + "https://www.legislation.gov.uk/nisr/2026/157/regulation/2", ) def formula(benunit, period, parameters): @@ -65,5 +86,33 @@ def formula(benunit, period, parameters): withdrawal_rate = parameters( period ).gov.dwp.housing_benefit.means_test.withdrawal_rate - taper = max_(0, income - applicable_amount) * withdrawal_rate - return max_(0, maximum_housing_benefit - taper) + segments = list(hb_accommodation_segments(benunit, period, parameters)) + if len(segments) == 1 or hb_uses_annual_override( + benunit, + period, + "housing_benefit_applicable_income", + housing_benefit_applicable_income.formula, + ): + taper = max_(0, income - applicable_amount) * withdrawal_rate + return max_(0, maximum_housing_benefit - taper) + income_before_deductions = hb_income_before_deductions( + benunit, period, parameters + ) + retain_annual_income = benunit( + "housing_benefit_pension_age_regulations_apply", period + ) | benunit("housing_benefit_on_passporting_benefit", period) + annual_award = benunit.empty_array() + for accommodation, share in segments: + disregard, childcare = hb_segment_deductions( + benunit, period, parameters, accommodation + ) + segment_income = where( + retain_annual_income, + income, + max_(0, income_before_deductions - disregard - childcare), + ) + taper = max_(0, segment_income - applicable_amount) * withdrawal_rate + annual_award = annual_award + share * max_( + 0, maximum_housing_benefit - taper + ) + return annual_award