diff --git a/changelog.d/1758.fixed.md b/changelog.d/1758.fixed.md new file mode 100644 index 0000000000..d0b886a5fc --- /dev/null +++ b/changelog.d/1758.fixed.md @@ -0,0 +1 @@ +Apply the £10,000 capital tariff threshold to Income Support, income-based JSA and income-related ESA claimants who live permanently in prescribed accommodation. Callers mark them with the new legacy_prescribed_accommodation_capital_exception input, which defaults to false. diff --git a/policyengine_uk/parameters/gov/dwp/ESA/income/capital/residential_threshold.yaml b/policyengine_uk/parameters/gov/dwp/ESA/income/capital/residential_threshold.yaml new file mode 100644 index 0000000000..0fa7d05819 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/ESA/income/capital/residential_threshold.yaml @@ -0,0 +1,13 @@ +description: Capital tariff threshold for income-related Employment and Support Allowance claimants who live permanently in prescribed accommodation. +metadata: + label: Income-related ESA prescribed accommodation capital tariff threshold + unit: currency-GBP + period: year +values: + 2008-10-27: + value: 10000 + reference: + - title: The Employment and Support Allowance Regulations 2008, regulation 118(2)-(4) + href: https://www.legislation.gov.uk/uksi/2008/794/regulation/118 + - title: The Employment and Support Allowance Regulations (Northern Ireland) 2008, regulation 118(2)-(4) + href: https://www.legislation.gov.uk/nisr/2008/280/regulation/118 diff --git a/policyengine_uk/parameters/gov/dwp/JSA/income/capital/residential_threshold.yaml b/policyengine_uk/parameters/gov/dwp/JSA/income/capital/residential_threshold.yaml new file mode 100644 index 0000000000..8b56ba2b37 --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/JSA/income/capital/residential_threshold.yaml @@ -0,0 +1,15 @@ +description: Capital tariff threshold for income-based Jobseeker's Allowance claimants who live permanently in prescribed accommodation. +metadata: + label: Income-based JSA prescribed accommodation capital tariff threshold + unit: currency-GBP + period: year +values: + 1996-10-07: + value: 10000 + reference: + - title: The Jobseeker's Allowance Regulations 1996, regulation 116(1A)-(1C) + href: https://www.legislation.gov.uk/uksi/1996/207/regulation/116 + - title: The Jobseeker's Allowance (Amendment) Regulations 1996, regulation 12(b) + href: https://www.legislation.gov.uk/uksi/1996/1516/regulation/12 + - title: The Jobseeker's Allowance (Amendment) Regulations (Northern Ireland) 1996, regulation 10 + href: https://www.legislation.gov.uk/nisr/1996/356/regulation/10/made diff --git a/policyengine_uk/parameters/gov/dwp/income_support/means_test/capital/residential_threshold.yaml b/policyengine_uk/parameters/gov/dwp/income_support/means_test/capital/residential_threshold.yaml new file mode 100644 index 0000000000..4316fb672d --- /dev/null +++ b/policyengine_uk/parameters/gov/dwp/income_support/means_test/capital/residential_threshold.yaml @@ -0,0 +1,15 @@ +description: Capital tariff threshold for Income Support claimants who live permanently in prescribed accommodation. +metadata: + label: Income Support prescribed accommodation capital tariff threshold + unit: currency-GBP + period: year +values: + 1996-04-08: + value: 10000 + reference: + - title: The Income Support (General) Regulations 1987, regulation 53(1A)-(1C) + href: https://www.legislation.gov.uk/uksi/1987/1967/regulation/53 + - title: The Income-related Benefits Schemes (Miscellaneous Amendments) Regulations 1996, regulation 12(1)(b) + href: https://www.legislation.gov.uk/uksi/1996/462/regulation/12 + - title: The Income-Related Benefits (Miscellaneous Amendments) Regulations (Northern Ireland) 1996, regulation 11(1)(b) + href: https://www.legislation.gov.uk/nisr/1996/93/regulation/11/made diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_residential_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_residential_capital.yaml new file mode 100644 index 0000000000..d74d1c0679 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/esa_income/esa_income_residential_capital.yaml @@ -0,0 +1,185 @@ +# Income-related ESA tariff income from capital: ESA Regs 2008 reg 118 (NI: SR +# 2008/280 reg 118). DWP and the Department for Communities count capital +# above the lower threshold as £1 a week for each complete £250 (reg +# 118(1)-(2)) and £1 for any part of £250 left over (reg 118(5)), over 52 +# weeks. They use a lower threshold of £6,000 (reg 118(1)), or £10,000 where +# the claimant lives permanently in prescribed accommodation (reg 118(2)-(4)), +# and stop counting capital at £16,000. The model pays no income-related ESA +# above the £16,000 capital limit (reg 110). At £16,001 the model continues +# the arithmetic, which the regulation does not prescribe, and the award case +# checks for a nil award at the configured limit. + +- name: Income-related ESA ordinary capital threshold and partial steps + period: 2025 + absolute_error_margin: 0.01 + input: + esa_income_assessable_capital: [6000, 6001, 10000, 10001, 10251, 16000, 16001] + legacy_prescribed_accommodation_capital_exception: [false, false, false, false, false, false, false] + output: + # Excess over £6,000: 0, 1, 4,000, 4,001, 4,251, 10,000, 10,001. + # Steps of £250 or part: 0, 1, 16, 17, 18, 40, 41; each £1 x 52. + esa_income_tariff_income: [0, 52, 832, 884, 936, 2080, 2132] + +- name: Income-related ESA prescribed accommodation capital threshold and partial steps + period: 2025 + absolute_error_margin: 0.01 + input: + esa_income_assessable_capital: [6000, 6001, 10000, 10001, 10251, 16000, 16001] + legacy_prescribed_accommodation_capital_exception: [true, true, true, true, true, true, true] + output: + # Excess over £10,000: 0, 0, 0, 1, 251, 6,000, 6,001. + # Steps of £250 or part: 0, 0, 0, 1, 2, 24, 25; each £1 x 52. + esa_income_tariff_income: [0, 0, 0, 52, 104, 1248, 1300] + +- name: Omitted prescribed accommodation status retains the ordinary ESA threshold + period: 2025 + absolute_error_margin: 0.01 + input: + esa_income_assessable_capital: 10001 + output: + legacy_prescribed_accommodation_capital_exception: false + esa_income_tariff_income: 884 + +- name: Housing Benefit's residential exception does not set the ESA threshold + period: 2025 + absolute_error_margin: 0.01 + input: + esa_income_assessable_capital: 10001 + housing_benefit_assessable_capital: 10001 + housing_benefit_residential_capital_exception: true + housing_benefit_pension_age_regulations_apply: false + output: + legacy_prescribed_accommodation_capital_exception: false + # HB Regs 2006 reg 52(3): £10,001 exceeds £10,000 by £1, one step. + housing_benefit_tariff_income: 52 + # ESA Regs 2008 reg 118(1): £4,001 over £6,000, 17 steps. + esa_income_tariff_income: 884 + +- name: The ESA prescribed accommodation exception does not set Housing Benefit's threshold + period: 2025 + absolute_error_margin: 0.01 + input: + esa_income_assessable_capital: 10001 + legacy_prescribed_accommodation_capital_exception: true + housing_benefit_assessable_capital: 10001 + housing_benefit_pension_age_regulations_apply: false + output: + housing_benefit_residential_capital_exception: false + # HB Regs 2006 reg 52(1): £4,001 over £6,000, 17 steps. + housing_benefit_tariff_income: 884 + # ESA Regs 2008 reg 118(2): £1 over £10,000, one step. + esa_income_tariff_income: 52 + +- name: Income-related ESA in prescribed accommodation is paid at the upper capital limit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 3_000 + benunits: + benunit: + members: claimant + would_claim_uc: false + esa_income_assessable_capital: 16_000 + legacy_prescribed_accommodation_capital_exception: true + households: + household: + members: claimant + output: + esa_income_eligible: true + # Reg 118(2): £6,000 over £10,000 is 24 steps, £24 x 52 = £1,248. + esa_income_tariff_income: 1_248 + # £3,000 reported award less £1,248 tariff income. + esa_income: 1_752 + +- name: Income-related ESA outside prescribed accommodation is paid at the upper capital limit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + esa_income_reported: 3_000 + benunits: + benunit: + members: claimant + would_claim_uc: false + esa_income_assessable_capital: 16_000 + legacy_prescribed_accommodation_capital_exception: false + households: + household: + members: claimant + output: + esa_income_eligible: true + # Reg 118(1): £10,000 over £6,000 is 40 steps, £40 x 52 = £2,080. + esa_income_tariff_income: 2_080 + # £3,000 reported award less £2,080 tariff income. + esa_income: 920 + +- name: Income-related ESA is not paid above the capital limit with or without the exception + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant_1: + age: 40 + esa_income_reported: 3_000 + claimant_2: + age: 40 + esa_income_reported: 3_000 + benunits: + benunit_1: + members: claimant_1 + would_claim_uc: false + esa_income_assessable_capital: 16_001 + legacy_prescribed_accommodation_capital_exception: true + benunit_2: + members: claimant_2 + would_claim_uc: false + esa_income_assessable_capital: 16_001 + legacy_prescribed_accommodation_capital_exception: false + households: + household_1: + members: claimant_1 + household_2: + members: claimant_2 + output: + # Reg 110: no award where capital exceeds £16,000. + esa_income_eligible: [false, false] + esa_income: [0, 0] + +- name: Income-related ESA keeps a fractional ordinary threshold when the exception input is omitted + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.ESA.income.capital.tariff_income.threshold: 6_000.02 + esa_income_assessable_capital: [6_000.02, 6_000.03, 6_250.02] + output: + # Reg 118(1) with the threshold reformed to £6,000.02: excess 0, 0.01 + # and 250; steps of £250 or part 0, 1 and 1; each £1 x 52. + esa_income_tariff_income: [0, 52, 52] + +- name: Income-related ESA keeps a fractional ordinary threshold when the exception input is false + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.ESA.income.capital.tariff_income.threshold: 6_000.02 + esa_income_assessable_capital: [6_000.02, 6_000.03, 6_250.02] + legacy_prescribed_accommodation_capital_exception: [false, false, false] + output: + # Reg 118(1): excess over £6,000.02 of 0, 0.01 and 250; 0, 1 and 1 steps. + esa_income_tariff_income: [0, 52, 52] + +- name: Income-related ESA applies a fractional prescribed accommodation threshold + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.ESA.income.capital.residential_threshold: 10_000.07 + esa_income_assessable_capital: [10_000.07, 10_000.08, 10_250.07] + legacy_prescribed_accommodation_capital_exception: [true, true, true] + output: + # Reg 118(2)-(4) with the threshold reformed to £10,000.07: excess 0, + # 0.01 and 250; 0, 1 and 1 steps; each £1 x 52. + esa_income_tariff_income: [0, 52, 52] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_residential_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_residential_capital.yaml new file mode 100644 index 0000000000..501e64df3a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/income_support/income_support_residential_capital.yaml @@ -0,0 +1,177 @@ +# Income Support tariff income from capital: IS Regs 1987 reg 53 (NI: SR +# 1987/459 reg 53). DWP and the Department for Communities count capital above +# the lower threshold as £1 a week for each complete £250 (reg 53(1)-(1A)) and +# £1 for any part of £250 left over (reg 53(2)), over 52 weeks. They use a +# lower threshold of £6,000 (reg 53(1)), or £10,000 where the claimant lives +# permanently in prescribed accommodation (reg 53(1A)-(1C)), and stop counting +# capital at £16,000. The model pays no Income Support above the £16,000 +# capital limit (reg 45). At £16,001 the model continues the arithmetic, which +# the regulation does not prescribe, and the award case checks for a nil award +# at the configured limit. + +- name: Income Support ordinary capital threshold and partial steps + period: 2025 + absolute_error_margin: 0.01 + input: + income_support_assessable_capital: [6000, 6001, 10000, 10001, 10251, 16000, 16001] + legacy_prescribed_accommodation_capital_exception: [false, false, false, false, false, false, false] + output: + # Excess over £6,000: 0, 1, 4,000, 4,001, 4,251, 10,000, 10,001. + # Steps of £250 or part: 0, 1, 16, 17, 18, 40, 41; each £1 x 52. + income_support_tariff_income: [0, 52, 832, 884, 936, 2080, 2132] + +- name: Income Support prescribed accommodation capital threshold and partial steps + period: 2025 + absolute_error_margin: 0.01 + input: + income_support_assessable_capital: [6000, 6001, 10000, 10001, 10251, 16000, 16001] + legacy_prescribed_accommodation_capital_exception: [true, true, true, true, true, true, true] + output: + # Excess over £10,000: 0, 0, 0, 1, 251, 6,000, 6,001. + # Steps of £250 or part: 0, 0, 0, 1, 2, 24, 25; each £1 x 52. + income_support_tariff_income: [0, 0, 0, 52, 104, 1248, 1300] + +- name: Omitted prescribed accommodation status retains the ordinary Income Support threshold + period: 2025 + absolute_error_margin: 0.01 + input: + income_support_assessable_capital: 10001 + output: + legacy_prescribed_accommodation_capital_exception: false + income_support_tariff_income: 884 + +- name: Housing Benefit's residential exception does not set the Income Support threshold + period: 2025 + absolute_error_margin: 0.01 + input: + income_support_assessable_capital: 10001 + housing_benefit_residential_capital_exception: true + output: + legacy_prescribed_accommodation_capital_exception: false + # Reg 53(1): £4,001 over £6,000, 17 steps. + income_support_tariff_income: 884 + +- name: Income Support in prescribed accommodation is paid at the upper capital limit + period: 2021 + absolute_error_margin: 0.01 + input: + people: + p1: + age: 26 + income_support_reported: true + c1: + age: 4 + benunits: + b1: + members: [p1, c1] + would_claim_IS: true + income_support_assessable_capital: 16_000 + legacy_prescribed_accommodation_capital_exception: true + households: + household: + members: [p1, c1] + output: + income_support_eligible: true + # Reg 53(1A): £6,000 over £10,000 is 24 steps, £24 x 52 = £1,248. + income_support_tariff_income: 1_248 + income_support_applicable_income: 1_248 + # £74.70 x 52 = £3,884.40 (2021-22 lone parent aged 18 or over), less + # £1,248 tariff income. + income_support: 2_636.40 + +- name: Income Support outside prescribed accommodation is paid at the upper capital limit + period: 2021 + absolute_error_margin: 0.01 + input: + people: + p1: + age: 26 + income_support_reported: true + c1: + age: 4 + benunits: + b1: + members: [p1, c1] + would_claim_IS: true + income_support_assessable_capital: 16_000 + legacy_prescribed_accommodation_capital_exception: false + households: + household: + members: [p1, c1] + output: + income_support_eligible: true + # Reg 53(1): £10,000 over £6,000 is 40 steps, £40 x 52 = £2,080. + income_support_tariff_income: 2_080 + income_support_applicable_income: 2_080 + # £3,884.40 less £2,080 tariff income. + income_support: 1_804.40 + +- name: Income Support is not paid above the capital limit with or without the exception + period: 2021 + absolute_error_margin: 0.01 + input: + people: + p1: + age: 26 + income_support_reported: true + c1: + age: 4 + p2: + age: 26 + income_support_reported: true + c2: + age: 4 + benunits: + b1: + members: [p1, c1] + would_claim_IS: true + income_support_assessable_capital: 16_001 + legacy_prescribed_accommodation_capital_exception: true + b2: + members: [p2, c2] + would_claim_IS: true + income_support_assessable_capital: 16_001 + legacy_prescribed_accommodation_capital_exception: false + households: + household_1: + members: [p1, c1] + household_2: + members: [p2, c2] + output: + # Reg 45: no award where capital exceeds £16,000. + income_support_eligible: [false, false] + income_support: [0, 0] + +- name: Income Support keeps a fractional ordinary threshold when the exception input is omitted + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.income_support.means_test.capital.tariff_income.threshold: 6_000.02 + income_support_assessable_capital: [6_000.02, 6_000.03, 6_250.02] + output: + # Reg 53(1) with the threshold reformed to £6,000.02: excess 0, 0.01 + # and 250; steps of £250 or part 0, 1 and 1; each £1 x 52. + income_support_tariff_income: [0, 52, 52] + +- name: Income Support keeps a fractional ordinary threshold when the exception input is false + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.income_support.means_test.capital.tariff_income.threshold: 6_000.02 + income_support_assessable_capital: [6_000.02, 6_000.03, 6_250.02] + legacy_prescribed_accommodation_capital_exception: [false, false, false] + output: + # Reg 53(1): excess over £6,000.02 of 0, 0.01 and 250; 0, 1 and 1 steps. + income_support_tariff_income: [0, 52, 52] + +- name: Income Support applies a fractional prescribed accommodation threshold + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.income_support.means_test.capital.residential_threshold: 10_000.07 + income_support_assessable_capital: [10_000.07, 10_000.08, 10_250.07] + legacy_prescribed_accommodation_capital_exception: [true, true, true] + output: + # Reg 53(1A)-(1C) with the threshold reformed to £10,000.07: excess 0, + # 0.01 and 250; 0, 1 and 1 steps; each £1 x 52. + income_support_tariff_income: [0, 52, 52] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_residential_capital.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_residential_capital.yaml new file mode 100644 index 0000000000..6effd37cf8 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/jsa_income/jsa_income_residential_capital.yaml @@ -0,0 +1,162 @@ +# Income-based JSA tariff income from capital: JSA Regs 1996 reg 116 (NI: SR +# 1996/198 reg 116). DWP and the Department for Communities count capital +# above the lower threshold as £1 a week for each complete £250 (reg +# 116(1)-(1A)) and £1 for any part of £250 left over (reg 116(2)), over 52 +# weeks. They use a lower threshold of £6,000 (reg 116(1)), or £10,000 where +# the claimant lives permanently in prescribed accommodation (reg +# 116(1A)-(1C)), and stop counting capital at £16,000. The model pays no +# income-based JSA above the £16,000 capital limit (reg 107). At £16,001 the +# model continues the arithmetic, which the regulation does not prescribe, and +# the award case checks for a nil award at the configured limit. + +- name: Income-based JSA ordinary capital threshold and partial steps + period: 2025 + absolute_error_margin: 0.01 + input: + jsa_income_assessable_capital: [6000, 6001, 10000, 10001, 10251, 16000, 16001] + legacy_prescribed_accommodation_capital_exception: [false, false, false, false, false, false, false] + output: + # Excess over £6,000: 0, 1, 4,000, 4,001, 4,251, 10,000, 10,001. + # Steps of £250 or part: 0, 1, 16, 17, 18, 40, 41; each £1 x 52. + jsa_income_tariff_income: [0, 52, 832, 884, 936, 2080, 2132] + +- name: Income-based JSA prescribed accommodation capital threshold and partial steps + period: 2025 + absolute_error_margin: 0.01 + input: + jsa_income_assessable_capital: [6000, 6001, 10000, 10001, 10251, 16000, 16001] + legacy_prescribed_accommodation_capital_exception: [true, true, true, true, true, true, true] + output: + # Excess over £10,000: 0, 0, 0, 1, 251, 6,000, 6,001. + # Steps of £250 or part: 0, 0, 0, 1, 2, 24, 25; each £1 x 52. + jsa_income_tariff_income: [0, 0, 0, 52, 104, 1248, 1300] + +- name: Omitted prescribed accommodation status retains the ordinary JSA threshold + period: 2025 + absolute_error_margin: 0.01 + input: + jsa_income_assessable_capital: 10001 + output: + legacy_prescribed_accommodation_capital_exception: false + jsa_income_tariff_income: 884 + +- name: Housing Benefit's residential exception does not set the JSA threshold + period: 2025 + absolute_error_margin: 0.01 + input: + jsa_income_assessable_capital: 10001 + housing_benefit_residential_capital_exception: true + output: + legacy_prescribed_accommodation_capital_exception: false + # Reg 116(1): £4,001 over £6,000, 17 steps. + jsa_income_tariff_income: 884 + +- name: Income-based JSA in prescribed accommodation is paid at the upper capital limit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + jsa_income_reported: 3_000 + benunits: + benunit: + members: claimant + jsa_income_assessable_capital: 16_000 + legacy_prescribed_accommodation_capital_exception: true + households: + household: + members: claimant + output: + jsa_income_eligible: true + # Reg 116(1A): £6,000 over £10,000 is 24 steps, £24 x 52 = £1,248. + jsa_income_tariff_income: 1_248 + # £3,000 reported award less £1,248 tariff income. + jsa_income: 1_752 + +- name: Income-based JSA outside prescribed accommodation is paid at the upper capital limit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 30 + jsa_income_reported: 3_000 + benunits: + benunit: + members: claimant + jsa_income_assessable_capital: 16_000 + legacy_prescribed_accommodation_capital_exception: false + households: + household: + members: claimant + output: + jsa_income_eligible: true + # Reg 116(1): £10,000 over £6,000 is 40 steps, £40 x 52 = £2,080. + jsa_income_tariff_income: 2_080 + # £3,000 reported award less £2,080 tariff income. + jsa_income: 920 + +- name: Income-based JSA is not paid above the capital limit with or without the exception + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant_1: + age: 30 + jsa_income_reported: 3_000 + claimant_2: + age: 30 + jsa_income_reported: 3_000 + benunits: + benunit_1: + members: claimant_1 + jsa_income_assessable_capital: 16_001 + legacy_prescribed_accommodation_capital_exception: true + benunit_2: + members: claimant_2 + jsa_income_assessable_capital: 16_001 + legacy_prescribed_accommodation_capital_exception: false + households: + household_1: + members: claimant_1 + household_2: + members: claimant_2 + output: + # Reg 107: no award where capital exceeds £16,000. + jsa_income_eligible: [false, false] + jsa_income: [0, 0] + +- name: Income-based JSA keeps a fractional ordinary threshold when the exception input is omitted + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.JSA.income.capital.tariff_income.threshold: 6_000.02 + jsa_income_assessable_capital: [6_000.02, 6_000.03, 6_250.02] + output: + # Reg 116(1) with the threshold reformed to £6,000.02: excess 0, 0.01 + # and 250; steps of £250 or part 0, 1 and 1; each £1 x 52. + jsa_income_tariff_income: [0, 52, 52] + +- name: Income-based JSA keeps a fractional ordinary threshold when the exception input is false + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.JSA.income.capital.tariff_income.threshold: 6_000.02 + jsa_income_assessable_capital: [6_000.02, 6_000.03, 6_250.02] + legacy_prescribed_accommodation_capital_exception: [false, false, false] + output: + # Reg 116(1): excess over £6,000.02 of 0, 0.01 and 250; 0, 1 and 1 steps. + jsa_income_tariff_income: [0, 52, 52] + +- name: Income-based JSA applies a fractional prescribed accommodation threshold + period: 2025 + absolute_error_margin: 0.01 + input: + gov.dwp.JSA.income.capital.residential_threshold: 10_000.07 + jsa_income_assessable_capital: [10_000.07, 10_000.08, 10_250.07] + legacy_prescribed_accommodation_capital_exception: [true, true, true] + output: + # Reg 116(1A)-(1C) with the threshold reformed to £10,000.07: excess 0, + # 0.01 and 250; 0, 1 and 1 steps; each £1 x 52. + jsa_income_tariff_income: [0, 52, 52] diff --git a/policyengine_uk/tests/test_legacy_capital_tariff_properties.py b/policyengine_uk/tests/test_legacy_capital_tariff_properties.py new file mode 100644 index 0000000000..fc39af1ebc --- /dev/null +++ b/policyengine_uk/tests/test_legacy_capital_tariff_properties.py @@ -0,0 +1,311 @@ +"""Property tests for tariff income from capital in the legacy means tests. + +DWP and Northern Ireland's Department for Communities treat capital above a +lower threshold as a weekly income of £1 for each complete £250 and £1 for +any part of £250 left over, counting capital up to £16,000. They apply this +rule in Income Support (IS Regs 1987 reg 53), income-based JSA (JSA Regs +1996 reg 116) and income-related ESA (ESA Regs 2008 reg 118), and under the +same regulations of the Northern Ireland rules. They use a lower threshold of +£6,000, or £10,000 where the claimant lives permanently in prescribed +accommodation (IS reg 53(1A)-(1C), JSA reg 116(1A)-(1C), ESA reg +118(2)-(4)). Working-age Housing Benefit (HB Regs 2006 reg 52) uses the same +figures, with its own residential circumstances. + +These statutory invariants hold for any capital on a penny grid up to +£16,000 and either exception status: + +1. Reference: each programme's tariff income is 52 x ceil(max(0, capital - + threshold) / 250) x £1, with the regulation's threshold. +2. Own parameters: each programme reads its own ordinary and prescribed + accommodation thresholds. When the test sets all six thresholds, two per + programme, to different amounts in pence, each programme's tariff income + follows its own two. + +These model invariants hold for any capital and either exception status: + +3. Continuation: above £16,000 the regulations prescribe no tariff income. + The variables continue the same arithmetic there, and the model pays no + award above the configured £16,000 capital limit. +4. Monotone: tariff income never falls as capital rises. +5. Bounded: tariff income with the prescribed accommodation exception never + exceeds the ordinary tariff income on the same capital. +6. Differential: the four programmes whose tariff parameters match give the + same tariff income at every capital level (the test sets Housing + Benefit's own exception the same way). +""" + +from math import ceil +from pathlib import Path + +import numpy as np +import pytest +import yaml +from hypothesis import HealthCheck, assume, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +WEEKS_IN_YEAR = 52 +PROPERTY_SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +PARAMETERS = Path(__file__).resolve().parents[1] / "parameters/gov/dwp" + +# Each programme: its tariff income variable, the capital it reads, the +# exception input that selects the higher threshold, and its parameters. +PROGRAMMES = { + "esa_income": ( + "esa_income_tariff_income", + "esa_income_assessable_capital", + "legacy_prescribed_accommodation_capital_exception", + lambda p: p.gov.dwp.ESA.income.capital, + ), + "jsa_income": ( + "jsa_income_tariff_income", + "jsa_income_assessable_capital", + "legacy_prescribed_accommodation_capital_exception", + lambda p: p.gov.dwp.JSA.income.capital, + ), + "income_support": ( + "income_support_tariff_income", + "income_support_assessable_capital", + "legacy_prescribed_accommodation_capital_exception", + lambda p: p.gov.dwp.income_support.means_test.capital, + ), + "housing_benefit": ( + "housing_benefit_tariff_income", + "housing_benefit_assessable_capital", + "housing_benefit_residential_capital_exception", + lambda p: p.gov.dwp.housing_benefit.means_test.capital.working_age, + ), +} +LEGACY = ("esa_income", "jsa_income", "income_support") + +# The statutory figures, from the regulations; the reference never reads the +# model's parameters. +ORDINARY_THRESHOLD = 6_000 +PRESCRIBED_ACCOMMODATION_THRESHOLD = 10_000 +UPPER_LIMIT = 16_000 +STEP = 250 +WEEKLY_AMOUNT = 1 + +# PENCE draws capital in pence, so every value names a sum of money, and +# biases it towards the thresholds and the upper capital limit, where the +# branches meet. +BOUNDARIES = [0, 6_000, 6_250, 10_000, 10_250, 16_000] +PENCE = st.one_of( + st.integers(0, 4_000_000), + st.builds( + lambda pounds, offset: max(0, pounds * 100 + offset), + st.sampled_from(BOUNDARIES), + st.integers(-300, 300), + ), +) +YEARS = st.sampled_from([2015, 2020, 2024, 2025, 2026, 2029]) + + +def tariff_arithmetic(capital, exception): + """52 x £1 for each complete or part £250 of capital above the threshold. + + Up to UPPER_LIMIT the function returns the annual tariff income under + the regulations. Above UPPER_LIMIT, where the regulations prescribe no + tariff income, it continues the same arithmetic, as the model does. + """ + threshold = PRESCRIBED_ACCOMMODATION_THRESHOLD if exception else ORDINARY_THRESHOLD + return WEEKS_IN_YEAR * WEEKLY_AMOUNT * ceil(max(0, capital - threshold) / STEP) + + +# One simulation serves every example: each example enters its capital +# amounts as inputs and recalculates tariff income, which reads only inputs. +SLOTS = 64 + + +@pytest.fixture(scope="module") +def simulation(cloned_uk_tax_benefit_system): + people = {f"person_{i}": {"age": {2025: 40}} for i in range(SLOTS)} + benunits = {f"benunit_{i}": {"members": [f"person_{i}"]} for i in range(SLOTS)} + households = {f"household_{i}": {"members": [f"person_{i}"]} for i in range(SLOTS)} + return Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + + +def tariffs(simulation, capitals, exceptions, year): + """Each programme's tariff income for one benefit unit per capital.""" + n = len(capitals) + capital = np.zeros(SLOTS) + capital[:n] = capitals + exception = np.zeros(SLOTS, dtype=bool) + exception[:n] = exceptions + for _, capital_variable, exception_variable, _ in PROGRAMMES.values(): + simulation.set_input(capital_variable, year, capital) + simulation.set_input(exception_variable, year, exception) + # Apply working-age Housing Benefit rules, with no Pension Credit passport. + for variable in ( + "housing_benefit_pension_age_regulations_apply", + "in_receipt_of_guarantee_credit", + "in_receipt_of_savings_credit_only", + ): + simulation.set_input(variable, year, np.zeros(SLOTS, dtype=bool)) + result = {} + for name, (variable, _, _, _) in PROGRAMMES.items(): + simulation.delete_arrays(variable) + result[name] = simulation.calculate(variable, year)[:n] + return result + + +def parameter_key(simulation, name, year): + """The tariff figures a programme uses in a year.""" + parameters = simulation.tax_benefit_system.parameters(f"{year}-01-01") + p = PROGRAMMES[name][3](parameters) + return ( + p.tariff_income.threshold, + p.residential_threshold, + p.tariff_income.step, + p.tariff_income.amount, + ) + + +def test_prescribed_accommodation_uses_higher_threshold(simulation): + # £10,001 exceeds £6,000 by £4,001 (17 steps) and £10,000 by £1 (1 step). + result = tariffs(simulation, [10_001, 10_001], [False, True], 2025) + for name in LEGACY: + assert result[name].tolist() == [884, 52], name + + +@pytest.mark.parametrize( + "path, start", + [ + # IS Regs 1987 reg 53(1A), inserted by SI 1996/462 reg 12(1)(b). + ("income_support/means_test/capital/residential_threshold.yaml", "1996-04-08"), + # JSA Regs 1996 reg 116(1A), inserted by SI 1996/1516 reg 12(b). + ("JSA/income/capital/residential_threshold.yaml", "1996-10-07"), + # ESA Regs 2008 reg 118(2), in force with the regulations. + ("ESA/income/capital/residential_threshold.yaml", "2008-10-27"), + ], +) +def test_prescribed_accommodation_threshold_starts_with_the_regulation(path, start): + data = yaml.safe_load((PARAMETERS / path).read_text()) + values = {str(date): entry["value"] for date, entry in data["values"].items()} + assert values == {start: PRESCRIBED_ACCOMMODATION_THRESHOLD} + + +@PROPERTY_SETTINGS +@given( + pence=st.lists(PENCE, min_size=1, max_size=8), + increments=st.lists(st.integers(0, 1_500_000), min_size=8, max_size=8), + year=YEARS, +) +def test_tariff_income_properties(simulation, pence, increments, year): + lower = [p / 100 for p in pence] + higher = [(p + d) / 100 for p, d in zip(pence, increments)] + capitals = lower + lower + higher + higher + exceptions = [False] * len(lower) + [True] * len(lower) + exceptions = exceptions + exceptions + result = tariffs(simulation, capitals, exceptions, year) + n = len(lower) + # The model stores capital as float32; the reference reads the same value. + stored = np.asarray(capitals, dtype=np.float32).astype(float) + expected = np.array([tariff_arithmetic(c, e) for c, e in zip(stored, exceptions)]) + statutory = stored <= UPPER_LIMIT + + for name in PROGRAMMES: + tariff = result[name] + # 1. Reference: the regulation's formula, up to £16,000. + np.testing.assert_allclose( + tariff[statutory], expected[statutory], atol=0.01, err_msg=name + ) + # 3. Continuation: the same arithmetic above £16,000. + np.testing.assert_allclose( + tariff[~statutory], expected[~statutory], atol=0.01, err_msg=name + ) + ordinary_low, excepted_low = tariff[:n], tariff[n : 2 * n] + ordinary_high, excepted_high = tariff[2 * n : 3 * n], tariff[3 * n :] + # 4. Monotone: more capital never lowers tariff income. + assert np.all(ordinary_high >= ordinary_low), name + assert np.all(excepted_high >= excepted_low), name + # 5. Bounded: the exception never raises tariff income. + assert np.all(excepted_low <= ordinary_low), name + assert np.all(excepted_high <= ordinary_high), name + + # 6. Differential: programmes with the same figures agree everywhere. + groups = {} + for name in PROGRAMMES: + groups.setdefault(parameter_key(simulation, name, year), []).append(name) + shared = [names for names in groups.values() if len(names) > 1] + assert shared, f"no two programmes share tariff parameters in {year}" + for names in shared: + for name in names[1:]: + np.testing.assert_array_equal( + result[name], result[names[0]], err_msg=f"{name} vs {names[0]}" + ) + + +# Each legacy programme's own capital parameters. +CAPITAL_PARAMETERS = { + "esa_income": "gov.dwp.ESA.income.capital", + "jsa_income": "gov.dwp.JSA.income.capital", + "income_support": "gov.dwp.income_support.means_test.capital", +} +THRESHOLD_PENCE = st.integers(100_000, 1_500_000) + + +def set_parameters(simulation, year, values): + """Set parameters for one year in place; return the values they replace.""" + parameters = simulation.tax_benefit_system.parameters + replaced = {} + for path, value in values.items(): + parameter = parameters.get_child(path) + replaced[path] = parameter(f"{year}-01-01") + parameter.update(period=str(year), value=value) + simulation.tax_benefit_system.reset_parameter_caches() + return replaced + + +@PROPERTY_SETTINGS +@given( + ordinary=st.lists(THRESHOLD_PENCE, min_size=3, max_size=3), + residential=st.lists(THRESHOLD_PENCE, min_size=3, max_size=3), + pence=st.lists(st.integers(0, UPPER_LIMIT * 100), max_size=10), + year=YEARS, +) +def test_each_programme_reads_its_own_thresholds( + simulation, ordinary, residential, pence, year +): + # 2. Own parameters. Hypothesis draws six different thresholds, and the + # test puts capital at each one and a penny either side, so a programme + # that read another programme's threshold would return a different tariff + # next to its own. + assume(len(set(ordinary + residential)) == 6) + values = {} + for name, own_ordinary, own_residential in zip(LEGACY, ordinary, residential): + path = CAPITAL_PARAMETERS[name] + values[f"{path}.tariff_income.threshold"] = own_ordinary / 100 + values[f"{path}.residential_threshold"] = own_residential / 100 + points = {t + d for t in ordinary + residential for d in (-1, 0, 1)} + points = sorted(points | set(pence)) + capitals = points + points + exceptions = [False] * len(points) + [True] * len(points) + replaced = set_parameters(simulation, year, values) + try: + result = tariffs(simulation, [c / 100 for c in capitals], exceptions, year) + finally: + set_parameters(simulation, year, replaced) + + for name, own_ordinary, own_residential in zip(LEGACY, ordinary, residential): + for capital, exception, tariff in zip(capitals, exceptions, result[name]): + excess = capital - (own_residential if exception else own_ordinary) + # Most amounts in pence have no exact float32 value. Where capital + # lies a whole number of £250 steps above the threshold, the stored + # figures can fall either side of the step, so the test skips that + # point. Anywhere else the excess lies at least a penny from a + # step, which exceeds that rounding error. + if excess > 0 and excess % (STEP * 100) == 0: + continue + expected = ( + WEEKS_IN_YEAR * WEEKLY_AMOUNT * ceil(max(0, excess) / (STEP * 100)) + ) + assert tariff == expected, (name, capital, exception, tariff, expected) diff --git a/policyengine_uk/variables/gov/dwp/esa_income_tariff_income.py b/policyengine_uk/variables/gov/dwp/esa_income_tariff_income.py index ea97f52279..d967faffdd 100644 --- a/policyengine_uk/variables/gov/dwp/esa_income_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/esa_income_tariff_income.py @@ -6,13 +6,43 @@ class esa_income_tariff_income(Variable): value_type = float entity = BenUnit label = "Income-related ESA tariff income from capital" + documentation = ( + "Weekly tariff income from capital under ESA Regs 2008 reg 118 (NI: SR " + "2008/280 reg 118), over 52 weeks: £1 for each complete £250 of capital " + "above the lower threshold and £1 for any part left over. DWP and " + "Northern Ireland's Department for Communities use a lower threshold of " + "£6,000, or £10,000 where " + "legacy_prescribed_accommodation_capital_exception is true, and stop " + "counting capital at £16,000. Above that amount this variable continues " + "the same arithmetic, which the regulation doesn't prescribe. The model " + "pays no ESA through esa_income_eligible or the own-right claims in " + "esa_income while capital exceeds the configured capital limit (reg 110, " + "£16,000), so while the limit stays at £16,000 the arithmetic above it " + "changes no award. Under a reform that raises the limit, the model " + "subtracts the continued tariff from awards." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/2008/794/regulation/118", + "https://www.legislation.gov.uk/nisr/2008/280/regulation/118", + ) def formula(benunit, period, parameters): ESA = parameters(period).gov.dwp.ESA.income capital = benunit("esa_income_assessable_capital", period) mt = ESA.capital - excess_capital = max_(0, capital - mt.tariff_income.threshold) + # Under reg 118(1)-(4), DWP and the Department for Communities use a + # lower threshold of £6,000, or £10,000 for a claimant who lives + # permanently in prescribed accommodation. Each branch subtracts its + # own scalar threshold, so both keep the stored capital's float32 + # precision. + ordinary_excess = max_(0, capital - mt.tariff_income.threshold) + residential_excess = max_(0, capital - mt.residential_threshold) + excess_capital = where( + benunit("legacy_prescribed_accommodation_capital_exception", period), + residential_excess, + ordinary_excess, + ) tariff_units = np.ceil(excess_capital / mt.tariff_income.step) return tariff_units * mt.tariff_income.amount * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/income_support_tariff_income.py b/policyengine_uk/variables/gov/dwp/income_support_tariff_income.py index 05ce115632..398bc0bcba 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_tariff_income.py @@ -6,13 +6,44 @@ class income_support_tariff_income(Variable): value_type = float entity = BenUnit label = "Income Support tariff income from capital" + documentation = ( + "Weekly tariff income from capital under IS Regs 1987 reg 53 (NI: SR " + "1987/459 reg 53), over 52 weeks: £1 for each complete £250 of capital " + "above the lower threshold and £1 for any part left over. DWP and " + "Northern Ireland's Department for Communities use a lower threshold of " + "£6,000, or £10,000 where " + "legacy_prescribed_accommodation_capital_exception is true, and stop " + "counting capital at £16,000. Above that amount this variable continues " + "the same arithmetic, which the regulation doesn't prescribe. " + "income_support_applicable_income adds this variable in full, and the " + "model pays no IS through income_support_entitlement while " + "income_support_eligible finds capital above the configured capital limit " + "(reg 45, £16,000), so while the limit stays at £16,000 the arithmetic " + "above it changes no award. Under a reform that raises the limit, the " + "model subtracts the continued tariff from awards." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/53", + "https://www.legislation.gov.uk/nisr/1996/93/regulation/11/made", + ) def formula(benunit, period, parameters): IS = parameters(period).gov.dwp.income_support capital = benunit("income_support_assessable_capital", period) mt = IS.means_test.capital - excess_capital = max_(0, capital - mt.tariff_income.threshold) + # Under reg 53(1)-(1C), DWP and the Department for Communities use a + # lower threshold of £6,000, or £10,000 for a claimant who lives + # permanently in prescribed accommodation. Each branch subtracts its + # own scalar threshold, so both keep the stored capital's float32 + # precision. + ordinary_excess = max_(0, capital - mt.tariff_income.threshold) + residential_excess = max_(0, capital - mt.residential_threshold) + excess_capital = where( + benunit("legacy_prescribed_accommodation_capital_exception", period), + residential_excess, + ordinary_excess, + ) tariff_units = np.ceil(excess_capital / mt.tariff_income.step) return tariff_units * mt.tariff_income.amount * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/jsa_income_tariff_income.py b/policyengine_uk/variables/gov/dwp/jsa_income_tariff_income.py index bc818cfba6..07f38c69f7 100644 --- a/policyengine_uk/variables/gov/dwp/jsa_income_tariff_income.py +++ b/policyengine_uk/variables/gov/dwp/jsa_income_tariff_income.py @@ -6,13 +6,43 @@ class jsa_income_tariff_income(Variable): value_type = float entity = BenUnit label = "Income-based JSA tariff income from capital" + documentation = ( + "Weekly tariff income from capital under JSA Regs 1996 reg 116 (NI: SR " + "1996/198 reg 116), over 52 weeks: £1 for each complete £250 of capital " + "above the lower threshold and £1 for any part left over. DWP and " + "Northern Ireland's Department for Communities use a lower threshold of " + "£6,000, or £10,000 where " + "legacy_prescribed_accommodation_capital_exception is true, and stop " + "counting capital at £16,000. Above that amount this variable continues " + "the same arithmetic, which the regulation doesn't prescribe. The model " + "pays no JSA through jsa_income_eligible or the own-right claims in " + "jsa_income while capital exceeds the configured capital limit (reg 107, " + "£16,000), so while the limit stays at £16,000 the arithmetic above it " + "changes no award. Under a reform that raises the limit, the model " + "subtracts the continued tariff from awards." + ) definition_period = YEAR unit = GBP + reference = ( + "https://www.legislation.gov.uk/uksi/1996/207/regulation/116", + "https://www.legislation.gov.uk/nisr/1996/356/regulation/10/made", + ) def formula(benunit, period, parameters): JSA = parameters(period).gov.dwp.JSA.income capital = benunit("jsa_income_assessable_capital", period) mt = JSA.capital - excess_capital = max_(0, capital - mt.tariff_income.threshold) + # Under reg 116(1)-(1C), DWP and the Department for Communities use a + # lower threshold of £6,000, or £10,000 for a claimant who lives + # permanently in prescribed accommodation. Each branch subtracts its + # own scalar threshold, so both keep the stored capital's float32 + # precision. + ordinary_excess = max_(0, capital - mt.tariff_income.threshold) + residential_excess = max_(0, capital - mt.residential_threshold) + excess_capital = where( + benunit("legacy_prescribed_accommodation_capital_exception", period), + residential_excess, + ordinary_excess, + ) tariff_units = np.ceil(excess_capital / mt.tariff_income.step) return tariff_units * mt.tariff_income.amount * WEEKS_IN_YEAR diff --git a/policyengine_uk/variables/gov/dwp/legacy_prescribed_accommodation_capital_exception.py b/policyengine_uk/variables/gov/dwp/legacy_prescribed_accommodation_capital_exception.py new file mode 100644 index 0000000000..f102a07748 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/legacy_prescribed_accommodation_capital_exception.py @@ -0,0 +1,50 @@ +from policyengine_uk.model_api import * + + +class legacy_prescribed_accommodation_capital_exception(Variable): + value_type = bool + entity = BenUnit + label = "Legacy benefit prescribed accommodation capital tariff exception" + definition_period = YEAR + default_value = False + documentation = ( + "True where the claimant lives permanently in prescribed accommodation " + "for the capital tariff of Income Support, income-based JSA and " + "income-related ESA (GB: IS Regs 1987 reg 53(1B)-(1C), JSA Regs 1996 reg " + "116(1B)-(1C), ESA Regs 2008 reg 118(3)-(4); NI: the same regulations of " + "SR 1987/459, SR 1996/198 and SR 2008/280). Setting it true raises the " + "lower threshold from £6,000 to £10,000. Assess the claimant's own stay: " + "the claimant doesn't qualify through a partner who lives in a home. In " + "Great Britain the claimant must live permanently in a care home, an " + "independent hospital, an Abbeyfield Home, or Polish Resettlement Act " + "1947 accommodation where they require personal care by reason of old " + "age, disablement, past or present dependence on alcohol or drugs, past " + "or present mental disorder or a terminal illness, and receive that care " + "there. In Northern Ireland the claimant must live permanently in a " + "residential care home, a nursing home, an independent hospital or an " + "Abbeyfield Home. A claimant still counts as living there permanently " + "while away: from a home or hospital for up to 13 weeks (52 weeks for a " + "claimant over pensionable age under IS and ESA), or from Polish " + "Resettlement Act accommodation for as long as they intend, with the " + "manager's agreement, to return in due course, with no fixed limit. " + "Assess the input under the rules in force for the year and jurisdiction " + "you model. For Housing Benefit, callers set different circumstances " + "through housing_benefit_residential_capital_exception, and neither input " + "sets the other. The model gives other members who claim in their own " + "right the benefit unit's tariff income, so this input sets their " + "threshold too; the model does not assess their own stay or divide " + "capital between claims. No dataset supplies this input. It defaults to " + "false, which keeps the £6,000 threshold and may understate these " + "benefits for qualifying residents. It does not change eligibility, how " + "the model values capital or the upper capital limit." + ) + reference = ( + "https://www.legislation.gov.uk/uksi/1987/1967/regulation/53", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/116", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/118", + "https://www.legislation.gov.uk/nisr/1996/93/regulation/11/made", + "https://www.legislation.gov.uk/nisr/1996/356/regulation/10/made", + "https://www.legislation.gov.uk/nisr/2005/458/schedule/1/made", + "https://www.legislation.gov.uk/nisr/2005/458/schedule/3/made", + "https://www.legislation.gov.uk/nisr/2008/280/regulation/118", + )