diff --git a/changelog.d/legacy-award-readers-claimant-partner.added.md b/changelog.d/legacy-award-readers-claimant-partner.added.md new file mode 100644 index 0000000000..a40af33b58 --- /dev/null +++ b/changelog.d/legacy-award-readers-claimant-partner.added.md @@ -0,0 +1,2 @@ +- Add `claimant_or_partner_esa_income` and `claimant_or_partner_jsa_income`: the income-related award of the claimant and partner. When `esa_income` or `jsa_income` holds what the reported awards give (its formula, or their plain total), this is the same on the claimant's and partner's reports; any other stored value is taken to be theirs, and a stored zero is zero. An entered award equal to either reported amount, to within half a penny at stored precision, is read through the reports, so enter these variables directly to state the claimant's or partner's award. +- Add `is_on_income_related_esa`, `is_on_income_based_jsa` and `is_on_income_support`: whether the award is payable to the person. Of the claimant and partner, that is the payee of their award (the one who reports it, or the claimant); for anyone else, the award on their own report alone, after tariff income from the benefit unit's capital and within the capital limit, while the benefit unit's award is positive (for Income Support: their own report while Income Support is active and not neutralised), so another member's report never changes it. diff --git a/changelog.d/legacy-award-readers-claimant-partner.fixed.md b/changelog.d/legacy-award-readers-claimant-partner.fixed.md new file mode 100644 index 0000000000..12eda88728 --- /dev/null +++ b/changelog.d/legacy-award-readers-claimant-partner.fixed.md @@ -0,0 +1,3 @@ +- Count only the claimant's and partner's income-related ESA and income-based JSA in Housing Benefit, council tax reduction and tax credit income, and in the passports for Housing Benefit (`in_receipt_of_income_support_jsa_ib_or_esa_ir`), working-age council tax reduction, Scottish Child Payment and targeted childcare. Another member of the benefit unit, such as a non-dependent adult, who reports an award claims in their own right; their award still counts in household income. +- Exempt a council tax reduction non-dependant in the local working-age schemes only when Income Support, income-based JSA or income-related ESA is payable to them, not to their partner or another benefit-unit member. A non-dependant couple on one of these benefits therefore pays the partner's deduction. The maintenance loan schedule for students entitled to benefits now reads whether income-related ESA is payable to the student (HB Regs 2006 reg 56(2)(a)), alongside the existing parent and disability tests, and adds reg 56(2)(c): a student who is the claimant or partner qualifies when their applicable amount would include the disability or severe disability premium, as when their partner receives a disability benefit. +- Treat a family as claiming Income Support (`would_claim_IS`) when the claimant or partner reports it, or when every entitled benefit is claimed (`claims_all_entitled_benefits`); another member's report no longer counts. diff --git a/docs/book/programs/gov/dwp/housing-benefit.ipynb b/docs/book/programs/gov/dwp/housing-benefit.ipynb index 704cabf965..7328c12022 100644 --- a/docs/book/programs/gov/dwp/housing-benefit.ipynb +++ b/docs/book/programs/gov/dwp/housing-benefit.ipynb @@ -43,7 +43,7 @@ "1. **Eligible rent**: the rent the benunit is liable for (`benunit_rent`). Where the Local Housing Allowance applies (`LHA_eligible`: renting, and no member in social housing), the eligible rent is the maximum rent (LHA), which is the LHA rate or, where the rent is lower, the rent (`LHA_cap`; SI 2006/213 regs [12D(2)(a)](https://www.legislation.gov.uk/uksi/2006/213/regulation/12D) and [13D(5)](https://www.legislation.gov.uk/uksi/2006/213/regulation/13D)).\n", "2. **Applicable amount** (`housing_benefit_applicable_amount`): a personal allowance derived from the benunit's composition (single, couple, lone parent) and age, plus any disability/carer premiums. This is the \"minimum income\" the household needs before HB tapers begin.\n", "3. **Means test** (`housing_benefit_entitlement`): the appropriate maximum Housing Benefit is the eligible rent less `housing_benefit_non_dep_deductions` for any non-dependant adults living in the household ([reg. 70](https://www.legislation.gov.uk/uksi/2006/213/regulation/70)). The published `withdrawal_rate` (currently 0.65) of the benunit's `housing_benefit_applicable_income` in excess of the applicable amount is deducted from that maximum ([SSCBA 1992 s. 130(3)(b)](https://www.legislation.gov.uk/ukpga/1992/4/section/130), [reg. 71](https://www.legislation.gov.uk/uksi/2006/213/regulation/71)). Because the LHA caps the eligible rent, the taper comes off the LHA: a private tenant with rent above the LHA and income above the applicable amount receives the LHA less the taper, not the lower of the LHA and the rent less the taper. The pension-age regulations (SI 2006/214) have the same rules at regs 12D, 13D, 50 and 51, and the Northern Ireland regulations at SR 2006/405 regs 13C, 14D, 68 and 69 and SR 2006/406 regs 13C, 14D, 48 and 49.\n", - " If the benunit is on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`housing_benefit_on_passporting_benefit`), the whole of its earnings, other income and capital is disregarded ([SI 2006/213 Sch. 4 para. 12](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12), [Sch. 5 para. 4](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4) and [Sch. 6 para. 5](https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5); Northern Ireland: [SR 2006/405 Sch. 5 para. 12](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12), [Sch. 6 para. 4](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4) and [Sch. 7 para. 5](https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5)). `housing_benefit_applicable_income` and `housing_benefit_assessable_capital` are then zero, so the award is that maximum. This applies at any age: a mixed-age couple whose younger member is on one of these benefits is assessed under the working-age regulations ([SI 2006/213 reg. 5(1)(b)](https://www.legislation.gov.uk/uksi/2006/213/regulation/5)). Income Support, income-based Jobseeker's Allowance and income-related Employment and Support Allowance count when a positive amount is paid (`in_receipt_of_income_support_jsa_ib_or_esa_ir`). A person is on Universal Credit on any day they are entitled to it, whether it is in payment or not ([reg. 2(3B)](https://www.legislation.gov.uk/uksi/2006/213/regulation/2)); PolicyEngine reads that as Universal Credit above nil before the benefit cap and deductions (`is_uc_entitled`), for a family that claims it, so the passport adds no circular dependency through the benefit cap. The universal credit limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). It matters only where Housing Benefit is paid alongside Universal Credit, for specified or temporary accommodation, which the eligibility rules above do not yet model.\n", + " If the claimant or partner is on Universal Credit, Income Support, income-based Jobseeker's Allowance or income-related Employment and Support Allowance (`housing_benefit_on_passporting_benefit`), the whole of the family's earnings, other income and capital is disregarded ([SI 2006/213 Sch. 4 para. 12](https://www.legislation.gov.uk/uksi/2006/213/schedule/4/paragraph/12), [Sch. 5 para. 4](https://www.legislation.gov.uk/uksi/2006/213/schedule/5/paragraph/4) and [Sch. 6 para. 5](https://www.legislation.gov.uk/uksi/2006/213/schedule/6/paragraph/5); Northern Ireland: [SR 2006/405 Sch. 5 para. 12](https://www.legislation.gov.uk/nisr/2006/405/schedule/5/paragraph/12), [Sch. 6 para. 4](https://www.legislation.gov.uk/nisr/2006/405/schedule/6/paragraph/4) and [Sch. 7 para. 5](https://www.legislation.gov.uk/nisr/2006/405/schedule/7/paragraph/5)). `housing_benefit_applicable_income` and `housing_benefit_assessable_capital` are then zero, so the award is that maximum. This applies at any age: a mixed-age couple whose younger member is on one of these benefits is assessed under the working-age regulations ([SI 2006/213 reg. 5(1)(b)](https://www.legislation.gov.uk/uksi/2006/213/regulation/5)). Income Support, income-based Jobseeker's Allowance and income-related Employment and Support Allowance count when a positive amount is paid on the claimant's or partner's award (`in_receipt_of_income_support_jsa_ib_or_esa_ir`, which reads `claimant_or_partner_jsa_income` and `claimant_or_partner_esa_income`). Another member's own award, such as a non-dependant's, is payable to them and does not passport the claimant. A person is on Universal Credit on any day they are entitled to it, whether it is in payment or not ([reg. 2(3B)](https://www.legislation.gov.uk/uksi/2006/213/regulation/2)); PolicyEngine reads that as Universal Credit above nil before the benefit cap and deductions (`is_uc_entitled`), for a family that claims it, so the passport adds no circular dependency through the benefit cap. The universal credit limb applies from 28 October 2013 in Great Britain and 8 May 2018 in Northern Ireland (`gov.dwp.housing_benefit.means_test.universal_credit_passport`). It matters only where Housing Benefit is paid alongside Universal Credit, for specified or temporary accommodation, which the eligibility rules above do not yet model.\n", "4. **Benefit cap** (`housing_benefit`): the post-means-test award is finally reduced by `benefit_cap_reduction` if the household's total benefit income exceeds the cap and no cap exemption applies.\n", "\n", "The take-up gate (`would_claim_housing_benefit`) is applied inside `housing_benefit_pre_benefit_cap`." diff --git a/policyengine_uk/parameters/gov/dfe/targeted_childcare_entitlement/qualifying_benefits.yaml b/policyengine_uk/parameters/gov/dfe/targeted_childcare_entitlement/qualifying_benefits.yaml index 7b0297e3ce..6cf7f7b91a 100644 --- a/policyengine_uk/parameters/gov/dfe/targeted_childcare_entitlement/qualifying_benefits.yaml +++ b/policyengine_uk/parameters/gov/dfe/targeted_childcare_entitlement/qualifying_benefits.yaml @@ -1,6 +1,8 @@ -description: The Department for Education designates these programs as qualifying benefits for the targeted childcare entitlement eligibility (the 2-year-old entitlement) in England. +description: The Department for Education designates these programs as qualifying benefits for the targeted childcare entitlement eligibility (the 2-year-old entitlement) in England. The child's parent must be entitled to one, so income-based JSA and income-related ESA count only as the claimant's or partner's award. metadata: reference: + - title: The Local Authority (Duty to Secure Early Years Provision Free of Charge) Regulations 2014 - regulation 1(2), "eligible child" + href: https://www.legislation.gov.uk/uksi/2014/2147/regulation/1 - title: The Local Authority (Duty to Secure Early Years Provision Free of Charge) (Amendment) Regulations 2018 - part 2.a href: https://www.legislation.gov.uk/uksi/2018/146/regulation/2/made period: year @@ -9,6 +11,6 @@ metadata: values: 2014-01-01: - income_support - - jsa_income - - esa_income + - claimant_or_partner_jsa_income + - claimant_or_partner_esa_income - guarantee_credit \ No newline at end of file diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml new file mode 100644 index 0000000000..06af3598f5 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/claimant_or_partner_awards.yaml @@ -0,0 +1,393 @@ +# claimant_or_partner_esa_income and claimant_or_partner_jsa_income: the +# income-related award on the claimant's and partner's reported amounts, with +# the same capital test as esa_income and jsa_income. "other" is a member who +# is neither the claimant, the partner nor a child or young person they are +# responsible for (an adult, not in education, with an explicit role), so +# their award is their own claim. It still counts in esa_income / jsa_income. +# Capital: tariff income is £1 a week for each £250, or part, above £6,000 +# (ESA Regs 2008 reg 118; JSA Regs 1996 reg 116), times 52 weeks. + +- name: The claimant's own income-related ESA is the claimant-or-partner award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + esa_income: 3_000 + claimant_or_partner_esa_income: 3_000 + +- name: The partner's income-related ESA is the claimant-or-partner award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + partner: + age: 38 + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + esa_income: 3_000 + claimant_or_partner_esa_income: 3_000 + +- name: Another member's income-related ESA is household income but not the claimant's award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + is_child_or_young_person_for_legacy_benefits: [false, false] + esa_income: 5_000 + claimant_or_partner_esa_income: 0 + +- name: The claimant-or-partner ESA award takes the same tariff income from capital + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + savings: 10_000 + output: + # (10,000 - 6,000) / 250 = 16 steps; 16 x £1 x 52 = £832. + esa_income_tariff_income: 832 + esa_income: 7_168 # 3,000 + 5,000 - 832 + claimant_or_partner_esa_income: 2_168 # 3,000 - 832 + +- name: Capital above the income-related ESA limit removes the claimant-or-partner award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + savings: 16_001 + output: + esa_income: 0 + claimant_or_partner_esa_income: 0 + +- name: A directly entered esa_income is the claimant's award whatever another member reports + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + esa_income: 3_000 + households: + household: + members: [claimant, other] + output: + claimant_or_partner_esa_income: 3_000 + +- name: A directly entered zero esa_income overrides the claimant's report + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + esa_income: 0 + households: + household: + members: [claimant] + output: + claimant_or_partner_esa_income: 0 + +- name: The claimant's own income-based JSA is the claimant-or-partner award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + jsa_income: 3_000 + claimant_or_partner_jsa_income: 3_000 + +- name: Another member's income-based JSA is household income but not the claimant's award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + jsa_income: 4_000 + claimant_or_partner_jsa_income: 0 + +- name: The claimant-or-partner JSA award takes the same tariff income from capital + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + savings: 7_000 + output: + # (7,000 - 6,000) / 250 = 4 steps; 4 x £1 x 52 = £208. + jsa_income_tariff_income: 208 + jsa_income: 6_792 # 3,000 + 4,000 - 208 + claimant_or_partner_jsa_income: 2_792 # 3,000 - 208 + +- name: No claimant-or-partner JSA award once income-based JSA has closed + period: 2027 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + jsa_income: 0 + claimant_or_partner_jsa_income: 0 + +- name: A directly entered jsa_income is the claimant's award whatever another member reports + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + jsa_income: 2_000 + households: + household: + members: [claimant, other] + output: + claimant_or_partner_jsa_income: 2_000 + +- name: An esa_income entered for an earlier year only does not make another member's award the claimant's + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + esa_income: + 2024: 1_000 + households: + household: + members: [claimant, other] + output: + esa_income: 5_000 + claimant_or_partner_esa_income: 0 + +- name: A jsa_income entered for an earlier year only does not make another member's award the claimant's + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + jsa_income: + 2024: 1_000 + households: + household: + members: [claimant, other] + output: + jsa_income: 4_000 + claimant_or_partner_jsa_income: 0 + +- name: An entered claimant-or-partner award is the claimant's whatever another member reports + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant, other] + esa_income: 3_000 + claimant_or_partner_esa_income: 3_000 + households: + household: + members: [claimant, other] + output: + # esa_income alone equals the other member's report, so it would be read + # through the reports; entering the scoped award states whose it is. + claimant_or_partner_esa_income: 3_000 + council_tax_reduction_relevant_income_based_benefit: true + +- name: The claimant's raw reported award passes through when esa_income holds the raw total + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 200 + benunits: + benunit: + members: [claimant] + esa_income: 200 + households: + household: + members: [claimant] + savings: 10_000 + output: + # The formula would give max(0, 200 - 832) = 0; the stored 200 is the raw + # total of the reports (as the disable_simulated_benefits reform sets), + # so the claimant's raw report is used. + claimant_or_partner_esa_income: 200 + +- name: A stored zero esa_income is never the claimant's award + period: 2025 + absolute_error_margin: 0 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 0.004 + benunits: + benunit: + members: [claimant] + esa_income: 0 + households: + household: + members: [claimant] + output: + claimant_or_partner_esa_income: 0 + council_tax_reduction_relevant_income_based_benefit: false diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml new file mode 100644 index 0000000000..88a4f16cfa --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/council_tax_reduction_passports.yaml @@ -0,0 +1,313 @@ +# Working-age council tax reduction passport: the applicant or partner is on +# Income Support, income-based JSA or income-related ESA (CTR (Default Scheme) +# (England) Regs 2012, Schedule; the local schemes follow it). A member who is +# neither the applicant, the partner nor a child or young person they are +# responsible for ("other") claims in their own right. + +- name: Another member's income-related ESA does not passport the applicant + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + council_tax_reduction_relevant_income_based_benefit: false + +- name: The applicant's own income-related ESA passports them + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + council_tax_reduction_relevant_income_based_benefit: true + +- name: Another member's income-based JSA does not passport the applicant + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + council_tax_reduction_relevant_income_based_benefit: false + +- name: The partner's income-based JSA passports the applicant + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + partner: + age: 38 + is_claimant_or_partner: true + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + council_tax_reduction_relevant_income_based_benefit: true + +- name: Another member's Income Support report does not passport the applicant + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + income_support_reported: 4_000 + receives_carer_benefit: true + care_hours: 35 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + income_support: 0 + council_tax_reduction_relevant_income_based_benefit: false + +- name: An entered Income Support award passports the applicant + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + benunits: + benunit: + members: [claimant] + income_support: 1_000 + households: + household: + members: [claimant] + output: + council_tax_reduction_relevant_income_based_benefit: true + +# Non-dependant deductions (Merton's working-age scheme, which follows the +# Default Scheme): no deduction for a non-dependant "who is on" Income Support, +# income-based JSA or income-related ESA. One deduction is made per non- +# dependant benefit unit (the higher of its members'); Merton's lowest band is +# £5.20 a week for a non-dependant not in remunerative work, £270.40 a year. +# The applicant is the eldest, so their benefit unit holds the household head; +# the non-dependant benefit unit is a claimant ("nondep") and another adult +# ("other") who is neither their partner nor their dependant. Universal Credit +# is entered as 0 for that benefit unit so that its no-earnings exemption does +# not apply. + +- name: Another member's ESA does not exempt a non-dependant from the deduction + period: 2027 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + nondep: + age: 30 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + other: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 4_000 + benunits: + applicant_unit: + members: [applicant] + nondep_unit: + members: [nondep, other] + universal_credit: 0 + households: + household: + members: [applicant, nondep, other] + country: ENGLAND + local_authority: MERTON + output: + merton_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 0] + +- name: A non-dependant's own ESA exempts them but not another adult in their benefit unit + period: 2027 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + nondep: + age: 30 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + esa_income_reported: 4_000 + other: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + applicant_unit: + members: [applicant] + nondep_unit: + members: [nondep, other] + universal_credit: 0 + households: + household: + members: [applicant, nondep, other] + country: ENGLAND + local_authority: MERTON + output: + merton_council_tax_reduction_individual_non_dep_deduction: [0, 0, 270.40] + +- name: A non-dependant couple on ESA pays the partner's deduction + period: 2027 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + payee: + age: 30 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + esa_income_reported: 4_000 + partner: + age: 29 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + benunits: + applicant_unit: + members: [applicant] + nondep_unit: + members: [payee, partner] + universal_credit: 0 + households: + household: + members: [applicant, payee, partner] + country: ENGLAND + local_authority: MERTON + output: + # ESA is payable to the payee only (HB Regs 2006 reg 2(3A), as the + # schemes define it), so only they are exempt. One deduction is made for + # the couple, the higher of the two (Default Scheme Sch para 30(3)): the + # partner's £5.20 a week. + is_on_income_related_esa: [false, true, false] + merton_council_tax_reduction_individual_non_dep_deduction: [0, 0, 270.40] + +- name: A non-dependant couple on ESA with a working partner pays the partner's deduction on their joint income + period: 2027 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + payee: + age: 30 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + partner: + age: 29 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + employment_income: 15_000 + weekly_hours: 20 + benunits: + applicant_unit: + members: [applicant] + nondep_unit: + members: [payee, partner] + universal_credit: 0 + households: + household: + members: [applicant, payee, partner] + country: ENGLAND + local_authority: MERTON + output: + # Only the payee is on the couple's ESA (Default Scheme Sch para 2(4), + # "payable to him"), so only they are exempt (para 30(8)(a)). The partner + # works 20 hours, at least Merton's 16, so the deduction follows the + # couple's joint gross income (para 30(4)): 15,000 / 52 = £288.46 a week, + # in the band from £279, £10.60 a week. One deduction is made for the + # couple, the higher of the two (para 30(3)): 10.60 x 52 = 551.20. + is_on_income_related_esa: [false, true, false] + merton_council_tax_reduction_individual_non_dep_deduction: [0, 0, 551.20] + merton_council_tax_reduction_non_dep_deductions: [551.20, 0] + +- name: A non-dependant's own ESA report below the tariff income does not exempt them, whatever another member reports + period: 2027 + absolute_error_margin: 0.01 + input: + people: + applicant: + age: 50 + nondep: + age: 30 + is_claimant_or_partner: true + current_education: NOT_IN_EDUCATION + lodger: + age: 45 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 200 + other: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 3_000 + benunits: + applicant_unit: + members: [applicant] + nondep_unit: + members: [nondep, lodger, other] + universal_credit: 0 + households: + household: + members: [applicant, nondep, lodger, other] + country: ENGLAND + local_authority: MERTON + savings: 7_000 + output: + # Tariff income on £7,000 of savings is £208 a year (ESA Regs 2008 + # reg 118), so the £200 report alone gives no award: that adult is not on + # income-related ESA and pays the £5.20 a week deduction, as the + # non-dependant claimant does. The adult reporting £3,000 is paid £2,792 + # and is exempt (Default Scheme Sch para 30(8)(a)). + is_on_income_related_esa: [false, false, false, true] + merton_council_tax_reduction_individual_non_dep_deduction: [0, 270.40, 270.40, 0] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/housing_benefit_earnings_disregard.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/housing_benefit_earnings_disregard.yaml new file mode 100644 index 0000000000..2106b91ad1 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/housing_benefit_earnings_disregard.yaml @@ -0,0 +1,106 @@ +# HB Regs 2006 Sch 4 para 12: where a claimant is on Universal Credit, Income +# Support, income-based JSA or income-related ESA, all their earnings are +# disregarded. The partner's earnings are the claimant's (reg 25) and a +# couple's award covers both of them. "other" is a member who is neither the +# claimant, the partner nor a child or young person they are responsible for, +# so an award they report is their own claim, and the claimant keeps the +# standard disregard: £5 a week for a single claimant (Sch 4 para 10), capped +# at net earnings. The claimant's £3,000 of earnings is below the income tax +# and National Insurance thresholds, so it is all net earnings. Income-based +# JSA is in payment until April 2026, so these cases are in 2025. + +- name: Another member's income-related ESA leaves the claimant's earnings with the standard disregard + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 3_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + would_claim_uc: false + households: + household: + members: [claimant, other] + output: + housing_benefit_net_earnings: 3_000 + # 5 x 52 + housing_benefit_applicable_income_disregard: 260 + +- name: Another member's income-based JSA leaves the claimant's earnings with the standard disregard + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 3_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + would_claim_uc: false + households: + household: + members: [claimant, other] + output: + housing_benefit_net_earnings: 3_000 + housing_benefit_applicable_income_disregard: 260 + +- name: The claimant's own income-related ESA disregards all their earnings + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 3_000 + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + output: + housing_benefit_net_earnings: 3_000 + housing_benefit_applicable_income_disregard: 3_000 + +- name: The partner's income-based JSA disregards all the couple's earnings + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 3_000 + partner: + age: 38 + is_claimant_or_partner: true + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, partner] + would_claim_uc: false + households: + household: + members: [claimant, partner] + output: + housing_benefit_net_earnings: 3_000 + housing_benefit_applicable_income_disregard: 3_000 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/housing_benefit_income_related_benefit_passport.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/housing_benefit_income_related_benefit_passport.yaml new file mode 100644 index 0000000000..c481882eb2 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/housing_benefit_income_related_benefit_passport.yaml @@ -0,0 +1,158 @@ +# HB Regs 2006 Sch 5 para 4 and Sch 6 para 5 (NI: SR 2006/405 Sch 6 para 4 +# and Sch 7 para 5): where the claimant is on Income Support, income-based JSA +# or income-related ESA, the whole of their income and capital is disregarded. +# The claimant's partner's income and capital are the claimant's (reg 25), and +# a couple's award covers both of them. "other" is a member who is neither the +# claimant, the partner nor a child or young person they are responsible for, +# so an award they report is their own claim and passports nobody. The +# household's £12,000 of savings all fall to the claimant's family, the only +# family in the household with a claimant or partner. Income-based JSA is in +# payment until April 2026, so these cases are in 2025. + +- name: Another member's income-related ESA does not passport the claimant's Housing Benefit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + would_claim_uc: false + households: + household: + members: [claimant, other] + savings: 12_000 + output: + claimant_or_partner_esa_income: 0 + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + housing_benefit_assessable_capital: 12_000 + +- name: Another member's income-based JSA does not passport the claimant's Housing Benefit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + would_claim_uc: false + households: + household: + members: [claimant, other] + savings: 12_000 + output: + claimant_or_partner_jsa_income: 0 + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + housing_benefit_assessable_capital: 12_000 + +- name: The claimant's own income-related ESA passports their income and capital + # Income-related ESA on £12,000 of capital: £1 a week of tariff income for + # each £250 above £6,000, so 5,000 - 24 x 52 = 3,752. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + savings: 12_000 + output: + claimant_or_partner_esa_income: 3_752 + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + +- name: The partner's income-based JSA passports the couple's income and capital + # 4,000 - 24 x 52 = 2,752 after tariff income on £12,000 of capital. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 3_000 + partner: + age: 38 + is_claimant_or_partner: true + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, partner] + would_claim_uc: false + households: + household: + members: [claimant, partner] + savings: 12_000 + output: + claimant_or_partner_jsa_income: 2_752 + in_receipt_of_income_support_jsa_ib_or_esa_ir: true + housing_benefit_assessable_capital: 0 + housing_benefit_applicable_income: 0 + +- name: A non-dependant son's income-related ESA does not passport his working mother's Housing Benefit + # The mother earns £30,000 and rents from the council for £10,000 a year. + # Her 25-year-old son is neither her partner nor a young person she is + # responsible for, so the £5,000 of income-related ESA he reports is his own + # claim and her income is assessed. Income tax (30,000 - 12,570) x 20% = + # 3,486 and National Insurance (30,000 - 12,570) x 8% = 1,394.40 leave net + # earnings of 25,119.60; less the £5 a week single disregard (Sch 4 para 10), + # applicable income is 24,859.60. 65% of its excess over her applicable + # amount covers the rent whenever that amount is at most £9,474.98 + # (24,859.60 - 10,000 / 0.65), which a single person's personal allowance + # is, so Housing Benefit is nil. Read as hers, his award would passport her + # whole income and give the full £10,000. + period: 2025 + absolute_error_margin: 0.01 + input: + people: + mother: + age: 50 + is_claimant_or_partner: true + employment_income: 30_000 + housing_benefit_reported: 1 + son: + age: 25 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [mother, son] + would_claim_uc: false + households: + household: + members: [mother, son] + tenure_type: RENT_FROM_COUNCIL + rent: 10_000 + output: + esa_income: 5_000 + claimant_or_partner_esa_income: 0 + in_receipt_of_income_support_jsa_ib_or_esa_ir: false + housing_benefit_on_passporting_benefit: false + housing_benefit_applicable_income: 24_859.60 + housing_benefit: 0 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml new file mode 100644 index 0000000000..561dd35b63 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/income_support_take_up_and_person_awards.yaml @@ -0,0 +1,261 @@ +# would_claim_IS is the family's take-up flag: the claimant or partner +# reports Income Support. An Income Support report from a member who is neither +# the claimant, the partner nor their dependant ("other") is that member's own +# claim. Any report in the simulation turns claims_all_entitled_benefits off. + +- name: Another member's Income Support report is not this family's claim + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + income_support_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + claims_all_entitled_benefits: false + would_claim_IS: false + +- name: The partner's Income Support report is this family's claim + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + partner: + age: 38 + is_claimant_or_partner: true + income_support_reported: 4_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + would_claim_IS: true + +# income_support_assessable_capital gives full household capital to a benefit +# unit on the Income Support claim path when anyone in the household is on it, +# and otherwise shares it by claimants and partners. With no claimant or +# partner reporting Income Support, £10,000 of savings is shared 1:1 between +# the two benefit units. + +- name: Another member's Income Support report does not move household capital to their benefit unit + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + income_support_reported: 4_000 + lodger: + age: 35 + is_claimant_or_partner: true + benunits: + family: + members: [claimant, other] + lodger_unit: + members: [lodger] + households: + household: + members: [claimant, other, lodger] + savings: 10_000 + output: + would_claim_IS: [false, false] + income_support_assessable_capital: [5_000, 5_000] + +# is_on_income_related_esa, is_on_income_based_jsa and is_on_income_support: +# "on" a benefit when it "is payable to him" (HB Regs 2006 reg 2(3), (3A)) or +# they are "in receipt of" it (reg 2(1)). A couple's award is paid to the +# claimant: of the claimant and partner only the payee (the one who reports +# it) is on it. Another member is on it only through an award they report +# themselves. + +- name: Only the payee is on the couple's income-related ESA + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + partner: + age: 38 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [claimant, partner, other] + households: + household: + members: [claimant, partner, other] + output: + is_on_income_related_esa: [true, false, false] + +- name: Another member is on their own income-related ESA and the couple is not + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + partner: + age: 38 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, partner, other] + households: + household: + members: [claimant, partner, other] + output: + is_on_income_related_esa: [false, false, true] + +- name: The claimant's income-related ESA removed by the capital test leaves them off it + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 3_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + savings: 16_001 + output: + is_on_income_related_esa: false + +- name: Another member is on their own income-based JSA while it is paid + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + is_on_income_based_jsa: [false, true] + +- name: Nobody is on income-based JSA once it has closed + period: 2027 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + jsa_income_reported: 3_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + is_on_income_based_jsa: [false, false] + +- name: Another member is on their own Income Support and the claimant is not + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + income_support_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + income_support: 0 + is_on_income_support: [false, true] + +- name: The claimant is on an entered Income Support award and another member is not + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [claimant, other] + income_support: 1_000 + households: + household: + members: [claimant, other] + output: + is_on_income_support: [true, false] + +- name: Nobody is on Income Support once it has closed + period: 2027 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + income_support_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + is_on_income_support: [false, false] diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml new file mode 100644 index 0000000000..8e4bb2be1a --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/means_test_income.yaml @@ -0,0 +1,125 @@ +# Housing Benefit and council tax reduction count the claimant's (applicant's) +# and partner's income only: SSCBA 1992 s.136(1) and HB Regs 2006 reg 25(1); +# CTR (Prescribed Requirements) (England) Regs 2012 Sch 1 para 11. "other" is +# a member who is neither the claimant, the partner nor a child or young person +# they are responsible for, so an award they report is their own claim and is +# not the claimant's income. +# HB: the model takes the single-person disregard (Sch 4) from all income, so +# with no income of the claimant's own the result is 0. That the claimant's and +# partner's own awards are counted in full is checked in +# test_legacy_award_readers_properties.py, independently of the disregard. +# CTR: no disregard in council_tax_reduction_applicable_income; universal_credit +# is entered as 0 so the award is the only income. + +- name: HB counts no income-related ESA of another member + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + housing_benefit_applicable_income: 0 # max(0, 0 - 260) + +- name: HB counts no income-based JSA of another member + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + housing_benefit_applicable_income: 0 + +- name: CTR counts no income-related ESA of another member + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + universal_credit: 0 + households: + household: + members: [claimant, other] + output: + council_tax_reduction_applicable_income: 0 + +- name: CTR counts no income-based JSA of another member + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + universal_credit: 0 + households: + household: + members: [claimant, other] + output: + council_tax_reduction_applicable_income: 0 + +- name: CTR counts the claimant's own income-related ESA + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [claimant, other] + universal_credit: 0 + households: + household: + members: [claimant, other] + output: + council_tax_reduction_applicable_income: 5_000 diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml new file mode 100644 index 0000000000..c48cc616df --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/family/legacy_award_readers/passports.yaml @@ -0,0 +1,539 @@ +# Passports that the law ties to the claimant, the partner, the parent or the +# student. "other" is a member who is neither the claimant, the partner nor a +# child or young person they are responsible for, so an award they report is +# their own claim. + +# Tax credits: no income test while "the person, or either of the persons" +# claiming is entitled to Income Support, income-based JSA or income-related +# ESA (TCA 2002 s.7(2); SI 2002/2008 reg 4). Otherwise the claimant's earnings +# of £10,000 are the applicable income (the £300 disregard only applies to +# step-one income, of which there is none). + +- name: Another member's income-related ESA leaves the tax credit income test in place + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 10_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + tax_credits_applicable_income: 10_000 + +- name: The claimant's own income-related ESA lifts the tax credit income test + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 10_000 + esa_income_reported: 5_000 + benunits: + benunit: + members: [claimant] + households: + household: + members: [claimant] + output: + tax_credits_applicable_income: 0 + +- name: Another member's income-based JSA leaves the tax credit income test in place + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 10_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, other] + households: + household: + members: [claimant, other] + output: + tax_credits_applicable_income: 10_000 + +- name: The partner's income-based JSA lifts the tax credit income test + period: 2025 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + employment_income: 10_000 + partner: + age: 38 + is_claimant_or_partner: true + jsa_income_reported: 4_000 + benunits: + benunit: + members: [claimant, partner] + households: + household: + members: [claimant, partner] + output: + tax_credits_applicable_income: 0 + +# Scottish Child Payment: the individual responsible for the child has been +# awarded a qualifying benefit (SSI 2020/351 reg 18(e)-(f), regs 9, 12 and 14). +# Responsibility runs through the individual's or partner's Child Benefit, +# Pension Credit or Universal Credit award, or kinship care (reg 12(1)(b)); the +# model takes the responsible individual to be the claimant or partner. +# Universal Credit, tax credits and Pension Credit are entered as 0 so the +# income-related award is the only possible qualifying benefit. + +- name: Another member's income-related ESA does not qualify the child for Scottish Child Payment + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + child: + age: 3 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [parent, child, other] + universal_credit: 0 + child_tax_credit: 0 + working_tax_credit: 0 + pension_credit: 0 + households: + household: + members: [parent, child, other] + region: SCOTLAND + output: + is_scp_eligible: [false, false, false] + +- name: The parent's own income-related ESA qualifies the child for Scottish Child Payment + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + esa_income_reported: 5_000 + child: + age: 3 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, child, other] + universal_credit: 0 + child_tax_credit: 0 + working_tax_credit: 0 + pension_credit: 0 + households: + household: + members: [parent, child, other] + region: SCOTLAND + output: + is_scp_eligible: [false, true, false] + +- name: Another member's income-based JSA does not qualify the child for Scottish Child Payment + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + child: + age: 3 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [parent, child, other] + universal_credit: 0 + child_tax_credit: 0 + working_tax_credit: 0 + pension_credit: 0 + households: + household: + members: [parent, child, other] + region: SCOTLAND + output: + is_scp_eligible: [false, false, false] + +- name: The parent's own income-based JSA qualifies the child for Scottish Child Payment + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + jsa_income_reported: 4_000 + child: + age: 3 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, child, other] + universal_credit: 0 + child_tax_credit: 0 + working_tax_credit: 0 + pension_credit: 0 + households: + household: + members: [parent, child, other] + region: SCOTLAND + output: + is_scp_eligible: [false, true, false] + +# Targeted childcare (England's two-year-old offer): the child's parent is +# entitled to a qualifying benefit (SI 2014/2147 reg 1(2), "eligible child"). +# The other routes (UC, CTC and WTC criteria, Guarantee Credit) are switched +# off with inputs. + +- name: Another member's income-related ESA does not qualify the family for targeted childcare + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + child: + age: 2 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 5_000 + benunits: + benunit: + members: [parent, child, other] + guarantee_credit: 0 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false + working_tax_credit_run_on: false + extended_childcare_entitlement_eligible: false + households: + household: + members: [parent, child, other] + country: ENGLAND + output: + targeted_childcare_entitlement_eligible: false + +- name: The parent's own income-related ESA qualifies the family for targeted childcare + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + esa_income_reported: 5_000 + child: + age: 2 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, child, other] + guarantee_credit: 0 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false + working_tax_credit_run_on: false + extended_childcare_entitlement_eligible: false + households: + household: + members: [parent, child, other] + country: ENGLAND + output: + targeted_childcare_entitlement_eligible: true + +- name: Another member's income-based JSA does not qualify the family for targeted childcare + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + child: + age: 2 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + jsa_income_reported: 4_000 + benunits: + benunit: + members: [parent, child, other] + guarantee_credit: 0 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false + working_tax_credit_run_on: false + extended_childcare_entitlement_eligible: false + households: + household: + members: [parent, child, other] + country: ENGLAND + output: + targeted_childcare_entitlement_eligible: false + +- name: The parent's own income-based JSA qualifies the family for targeted childcare + period: 2025 + input: + people: + parent: + age: 30 + is_claimant_or_partner: true + jsa_income_reported: 4_000 + child: + age: 2 + other: + age: 20 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [parent, child, other] + guarantee_credit: 0 + meets_universal_credit_criteria_for_targeted_childcare_entitlement: false + meets_child_tax_credit_criteria_for_targeted_childcare_entitlement: false + working_tax_credit_run_on: false + extended_childcare_entitlement_eligible: false + households: + household: + members: [parent, child, other] + country: ENGLAND + output: + targeted_childcare_entitlement_eligible: true + +# Benefit cap: no cap where "the claimant or the claimant's partner" receives +# ESA with the support component (HB Regs 2006 reg 75F(1)(a)). The model +# exempts any income-related ESA. This PR leaves the cap's reading of the +# benefit unit's ESA unchanged: scoping the exemption alone would cap the +# claimant with another member's own award in the capped sum +# (benefit_cap_reduction), which is out of scope here. The claimant's own +# award still exempts the family. + +- name: The claimant's own income-related ESA exempts the family from the benefit cap + period: 2025 + input: + people: + claimant: + age: 40 + is_claimant_or_partner: true + esa_income_reported: 5_000 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + benunits: + benunit: + members: [claimant, other] + working_tax_credit: 0 + households: + household: + members: [claimant, other] + output: + is_benefit_cap_exempt_health_disability: true + +# Maintenance loans: the "students entitled to benefits" schedule (SI 2011/1986 +# reg 71(1)(h)(iii)) follows the special support grant test in reg 61(2)(b), +# which follows HB Regs 2006 reg 56(2)(a): a full-time student "who is a +# person on" income-related ESA, that is, one to whom it "is payable" +# (reg 2(3A)). The model's proxy reads whether income-related ESA is payable +# to the student; a parent's or partner's award does not make the student +# eligible. + +- name: A parent's income-related ESA does not put a student on the benefits schedule + period: 2025 + input: + people: + parent: + age: 50 + is_claimant_or_partner: true + is_parent: true + esa_income_reported: 5_000 + student: + age: 19 + is_claimant_or_partner: false + current_education: TERTIARY + benunits: + benunit: + members: [parent, student] + households: + household: + members: [parent, student] + country: ENGLAND + output: + is_child_or_young_person_for_legacy_benefits: [false, false] + maintenance_loan_entitled_to_benefits: [true, false] + +- name: A student's own income-related ESA puts them on the benefits schedule + period: 2025 + input: + people: + parent: + age: 50 + is_claimant_or_partner: true + is_parent: true + student: + age: 19 + is_claimant_or_partner: false + current_education: TERTIARY + esa_income_reported: 5_000 + benunits: + benunit: + members: [parent, student] + households: + household: + members: [parent, student] + country: ENGLAND + output: + maintenance_loan_entitled_to_benefits: [true, true] + +- name: A student whose partner is paid income-related ESA is not on the benefits schedule through it + period: 2025 + input: + people: + student: + age: 22 + is_claimant_or_partner: true + current_education: TERTIARY + partner: + age: 23 + is_claimant_or_partner: true + esa_income_reported: 5_000 + benunits: + benunit: + members: [student, partner] + households: + household: + members: [student, partner] + country: ENGLAND + output: + maintenance_loan_entitled_to_benefits: [false, true] + +- name: A student whose partner receives PIP qualifies through the disability premium + period: 2025 + input: + people: + student: + age: 22 + is_claimant_or_partner: true + current_education: TERTIARY + partner: + age: 23 + is_claimant_or_partner: true + esa_income_reported: 5_000 + pip_dl: 2_000 + is_disabled_for_benefits: true + benunits: + benunit: + members: [student, partner] + households: + household: + members: [student, partner] + country: ENGLAND + output: + # ESA is payable to the partner, not the student (reg 2(3A)). But the + # partner is under State Pension Credit age and receives PIP, so the + # couple's applicable amount includes the disability premium (HB Regs 2006 + # Sch 3 paras 12(b)(ii) and 13(1)(a)(i)), and reg 56(2)(c) lifts the + # student exclusion for the student too. + is_on_income_related_esa: [false, true] + maintenance_loan_entitled_to_benefits: [true, true] + +- name: A parent's disability premium does not put a student who is not the claimant or partner on the benefits schedule + period: 2025 + input: + people: + parent: + age: 50 + is_claimant_or_partner: true + is_parent: false + pip_dl: 2_000 + is_disabled_for_benefits: true + student: + age: 19 + is_claimant_or_partner: false + current_education: TERTIARY + benunits: + benunit: + members: [parent, student] + households: + household: + members: [parent, student] + country: ENGLAND + output: + # The disability premium is in the parent's applicable amount, not the + # student's: reg 56(2)(c) asks about the student's own applicable amount + # as a claimant. + maintenance_loan_entitled_to_benefits: [true, false] + +- name: A student's own ESA report below the tariff income is not paid, whatever another member reports + period: 2025 + input: + people: + parent: + age: 50 + is_claimant_or_partner: true + is_parent: false + student: + age: 19 + is_claimant_or_partner: false + current_education: TERTIARY + esa_income_reported: 200 + other: + age: 30 + is_claimant_or_partner: false + current_education: NOT_IN_EDUCATION + esa_income_reported: 3_000 + benunits: + benunit: + members: [parent, student, other] + households: + household: + members: [parent, student, other] + country: ENGLAND + savings: 7_000 + output: + # Savings of £7,000 are £1,000 above £6,000: 4 steps of £250, so tariff + # income of £4 a week, £208 a year (ESA Regs 2008 reg 118). The student's + # £200 alone gives no award; the other adult's £3,000 gives £2,792. The + # benefit unit's award on both reports, £2,992, is household income but + # does not make the student's report paid. + esa_income: [2_992] + claimant_or_partner_esa_income: [0] + is_on_income_related_esa: [false, false, true] + maintenance_loan_entitled_to_benefits: [false, false, true] diff --git a/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml b/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml index e15181a4f6..4ba6eaf213 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.yaml @@ -1,5 +1,9 @@ # Receipt of Income Support, income-based JSA or income-related ESA: a -# positive amount of any of them, held for the benefit unit. +# positive amount of any of them on the claimant's or partner's award. The +# cases that enter an award report none, so the entered award is taken to be +# theirs; an entered amount equal to the award the members' reports give is +# read through the reports instead (claimant_or_partner_esa_income). +# Another member's own award is tested in legacy_award_readers/. - name: No income-related benefit period: 2025 diff --git a/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py b/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py new file mode 100644 index 0000000000..8a1104b6c1 --- /dev/null +++ b/policyengine_uk/tests/test_claimant_or_partner_awards_entered_directly.py @@ -0,0 +1,274 @@ +"""The claimant-or-partner awards follow what esa_income and jsa_income hold. + +claimant_or_partner_esa_income and claimant_or_partner_jsa_income use the +claimant's and partner's reported awards when esa_income or jsa_income holds +what the reported awards give, either after the capital test (the formula) +or as their plain total (the disable_simulated_benefits reform). Otherwise +they take the stored value to be the claimant's or partner's: an award entered +directly, or a reform that replaces or removes it. The rule is decided by +value, so it holds through deletion, clones and branches. Each case has a +claimant with no award of their own and an adult outside the couple who +reports £3,000 of the award. Whenever the stored award is what the reports +give, the claimant-or-partner award must be £0; when it is anything else, it +must be that value. + +These mirror #2025's test_entered_directly_lifecycle.py and +test_income_support_direct_inputs.py, which assert the same rule through +income_support_eligible. +""" + +import pytest + +from policyengine_uk import Simulation + +YEAR = 2025 + +AWARDS = { + "esa_income": ("esa_income_reported", "claimant_or_partner_esa_income"), + "jsa_income": ("jsa_income_reported", "claimant_or_partner_jsa_income"), +} + + +def family(award): + reported, _ = AWARDS[award] + members = ["claimant", "other_adult"] + return Simulation( + situation={ + "people": { + "claimant": { + "age": {YEAR: 40}, + "is_claimant_or_partner": {YEAR: True}, + }, + "other_adult": { + "age": {YEAR: 30}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "NOT_IN_EDUCATION"}, + reported: {YEAR: 3_000}, + }, + }, + "benunits": {"family": {"members": members}}, + "households": {"home": {"members": members}}, + } + ) + + +def scoped(simulation, award): + _, variable = AWARDS[award] + simulation.delete_arrays(variable) + return float(simulation.calculate(variable, YEAR)[0]) + + +@pytest.mark.parametrize("award", AWARDS) +def test_another_members_report_is_never_the_claimants_award(award): + simulation = family(award) + assert simulation.calculate(award, YEAR)[0] == 3_000 + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_a_deleted_input_does_not_make_a_later_formula_result_direct(award): + simulation = family(award) + simulation.set_input(award, YEAR, [1_000]) + assert scoped(simulation, award) == 1_000 + simulation.delete_arrays(award) + assert simulation.calculate(award, YEAR)[0] == 3_000 + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_an_input_on_a_clone_does_not_reach_the_original(award): + simulation = family(award) + assert simulation.calculate(award, YEAR)[0] == 3_000 + clone = simulation.clone() + clone.set_input(award, YEAR, [1_000]) + assert scoped(clone, award) == 1_000 + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_a_parent_input_does_not_reach_an_earlier_branch(award): + simulation = family(award) + assert simulation.calculate(award, YEAR)[0] == 3_000 + branch = simulation.get_branch("before", clone_system=False) + simulation.set_input(award, YEAR, [1_000]) + assert scoped(simulation, award) == 1_000 + # The branch still stores the formula result it copied. + assert scoped(branch, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_a_branch_made_after_a_parent_input_inherits_it(award): + simulation = family(award) + simulation.set_input(award, YEAR, [1_000]) + branch = simulation.get_branch("after", clone_system=False) + assert scoped(branch, award) == 1_000 + + +@pytest.mark.parametrize("award", AWARDS) +def test_nested_branches_read_the_nearest_stored_input(award): + simulation = family(award) + outer = simulation.get_branch("outer", clone_system=False) + outer.set_input(award, YEAR, [1_000]) + inner = outer.get_branch("inner", clone_system=False) + assert scoped(inner, award) == 1_000 + inner.set_input(award, YEAR, [500]) + assert scoped(inner, award) == 500 + assert scoped(outer, award) == 1_000 + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_an_entered_award_equal_to_the_reports_is_read_through_them(award): + simulation = family(award) + # 3,000 is what the other adult's report gives, so the reports decide + # whose award it is: not the claimant's. + simulation.set_input(award, YEAR, [3_000]) + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_the_raw_reported_total_is_read_through_the_reports(award): + """disable_simulated_benefits sets the award to the raw total of every + member's report. Here the partner reports £200 and an adult outside the + couple £3,000, with savings of £10,000. Tariff income of + ceil(4,000 / 250) x £1 x 52 = £832 a year extinguishes the partner's + award. The raw total, £3,200, differs from the screened award, £2,368, + but is still what the reports give. As the reform uses reported amounts + unscreened, the claimant-or-partner award is the partner's raw report, + £200.""" + reported, _ = AWARDS[award] + members = ["claimant", "partner", "other_adult"] + simulation = Simulation( + situation={ + "people": { + "claimant": {"age": {YEAR: 40}, "is_claimant_or_partner": {YEAR: True}}, + "partner": { + "age": {YEAR: 40}, + "is_claimant_or_partner": {YEAR: True}, + reported: {YEAR: 200}, + }, + "other_adult": { + "age": {YEAR: 30}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "NOT_IN_EDUCATION"}, + reported: {YEAR: 3_000}, + }, + }, + "benunits": {"family": {"members": members}}, + "households": {"home": {"members": members, "savings": {YEAR: 10_000}}}, + } + ) + assert simulation.calculate(award, YEAR)[0] == 2_368 + assert scoped(simulation, award) == 0 + simulation.set_input(award, YEAR, [3_200]) + assert scoped(simulation, award) == 200 + # A different award entered directly is the couple's. + simulation.set_input(award, YEAR, [4_000]) + assert scoped(simulation, award) == 4_000 + + +@pytest.mark.parametrize("award", AWARDS) +def test_a_neutralised_award_switches_the_claimants_award_off(award): + reported, _ = AWARDS[award] + simulation = family(award) + simulation.set_input(reported, YEAR, [3_000, 0]) + simulation.delete_arrays(award) + assert scoped(simulation, award) == 3_000 + simulation.tax_benefit_system.neutralize_variable(award) + simulation.delete_arrays(award) + assert simulation.calculate(award, YEAR)[0] == 0 + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_a_stored_zero_is_never_the_claimants_award(award): + """A stored award of zero is no award, even when the reports give a + fraction of a penny that rounds to it.""" + reported, _ = AWARDS[award] + simulation = family(award) + simulation.set_input(reported, YEAR, [0.004, 0]) + simulation.set_input(award, YEAR, [0]) + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_large_reports_are_compared_at_storage_precision(award): + """Awards are stored as float32. Two reports of £65,536.01 and £65,536.00 + from adults outside the couple give £131,072.01, which float32 rounds; + the formula's own award must still be recognised as the formula's.""" + reported, _ = AWARDS[award] + members = ["claimant", "other_adult", "third_adult"] + simulation = Simulation( + situation={ + "people": { + "claimant": {"age": {YEAR: 40}, "is_claimant_or_partner": {YEAR: True}}, + "other_adult": { + "age": {YEAR: 30}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "NOT_IN_EDUCATION"}, + reported: {YEAR: 65_536.01}, + }, + "third_adult": { + "age": {YEAR: 31}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "NOT_IN_EDUCATION"}, + reported: {YEAR: 65_536.00}, + }, + }, + "benunits": {"family": {"members": members}}, + "households": {"home": {"members": members}}, + } + ) + assert simulation.calculate(award, YEAR)[0] > 131_000 + assert scoped(simulation, award) == 0 + + +@pytest.mark.parametrize("award", AWARDS) +def test_an_entered_scoped_award_states_ownership(award): + """Entering the claimant-or-partner award itself is the explicit way to + say whose an award is: another member's report then never changes it.""" + _, variable = AWARDS[award] + for other_report in [2_000, 3_000]: + simulation = family(award) + reported, _ = AWARDS[award] + simulation.set_input(reported, YEAR, [0, other_report]) + simulation.set_input(award, YEAR, [3_000]) + simulation.set_input(variable, YEAR, [3_000]) + assert simulation.calculate(variable, YEAR)[0] == 3_000 + + +def test_a_removed_esa_award_takes_an_excluded_students_status_with_it(): + """An adult outside the couple who reports income-related ESA is on it + while the model pays it; once a reform removes esa_income, neither their + status nor the maintenance loan schedule follows the report alone.""" + members = ["parent", "student"] + simulation = Simulation( + situation={ + "people": { + "parent": { + "age": {YEAR: 50}, + "is_claimant_or_partner": {YEAR: True}, + "is_parent": {YEAR: False}, + }, + "student": { + "age": {YEAR: 19}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "TERTIARY"}, + "esa_income_reported": {YEAR: 3_000}, + }, + }, + "benunits": {"family": {"members": members}}, + "households": {"home": {"members": members, "country": {YEAR: "ENGLAND"}}}, + } + ) + assert simulation.calculate("is_on_income_related_esa", YEAR)[1] + assert simulation.calculate("maintenance_loan_entitled_to_benefits", YEAR)[1] + simulation.tax_benefit_system.neutralize_variable("esa_income") + for variable in [ + "esa_income", + "is_on_income_related_esa", + "maintenance_loan_entitled_to_benefits", + ]: + simulation.delete_arrays(variable) + assert not simulation.calculate("is_on_income_related_esa", YEAR)[1] + assert not simulation.calculate("maintenance_loan_entitled_to_benefits", YEAR)[1] diff --git a/policyengine_uk/tests/test_legacy_award_readers_properties.py b/policyengine_uk/tests/test_legacy_award_readers_properties.py new file mode 100644 index 0000000000..7c6ef57d72 --- /dev/null +++ b/policyengine_uk/tests/test_legacy_award_readers_properties.py @@ -0,0 +1,648 @@ +"""Readers of the legacy income-related awards follow the claimant and partner. + +Income-related ESA, income-based JSA and Income Support are awarded to a +claimant for themselves and their partner. Each means test or passport that +reads them names the claimant (or applicant, or person claiming) and partner, +the child's parent, or the person themselves: + +- Housing Benefit and council tax reduction income: SSCBA 1992 s.136(1), HB + Regs 2006 reg 25(1); CTR (Prescribed Requirements) (England) Regs 2012 + Sch 1 para 11. +- The Housing Benefit passport, which disregards the earnings, income and + capital of a claimant on Income Support, income-based JSA or income-related + ESA: HB Regs 2006 Sch 4 para 12, Sch 5 para 4 and Sch 6 para 5. +- The working-age council tax reduction passport and non-dependant + exemption: CTR (Default Scheme) (England) Regs 2012, Schedule. +- The tax credit income test: TCA 2002 s.7(2), SI 2002/2008 reg 4. +- Scottish Child Payment: SSI 2020/351 reg 18(e)-(f). +- Targeted childcare: SI 2014/2147 reg 1(2). +- Maintenance loans for students entitled to benefits: SI 2011/1986 reg + 71(1)(h)(iii), through reg 61(2)(b) and HB Regs 2006 reg 56(2)(a) (the + student is on the award) and (c) (the student's applicable amount would + include a disability or severe disability premium). + +A member of the benefit unit who is neither the claimant, the partner nor a +child or young person they are responsible for (for example a non-dependent +adult) claims in their own right, so: + +- adding such a member, whatever Income Support, income-based JSA or + income-related ESA they report, never changes any of these readers for the + claimant's family or for its existing members; +- the claimant-or-partner awards are bounded by the benefit-unit awards + (0 <= claimant_or_partner_esa_income <= esa_income, likewise for JSA) and + equal them when no other member reports an award; +- a person is on an award exactly when they are the payee of their couple's + positive award (the claimant or partner who reports it, or the claimant) + or, for anyone else, when the award on their own report alone is positive + after tariff income from the household's capital and within the capital + limit (Income Support: when they report it). + +Roles are given explicitly (is_claimant_or_partner), so the properties test +the readers rather than role inference. The take-up mode is fixed +(claims_all_entitled_benefits false): that flag sums reports across the +whole simulation, so with it a report anywhere can change would_claim_IS. Each example builds many families in +one simulation, in separate households and benefit units. +""" + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation + +YEAR = 2025 +# Merton's working-age council tax reduction scheme has parameters from +# April 2026, so the non-dependant property runs in 2027 (income-related ESA +# only: Income Support and income-based JSA closed in April 2026). +NON_DEP_YEAR = 2027 + +FAMILY_READERS = [ + "claimant_or_partner_esa_income", + "claimant_or_partner_jsa_income", + "housing_benefit_applicable_income", + "in_receipt_of_income_support_jsa_ib_or_esa_ir", + "housing_benefit_on_passporting_benefit", + "housing_benefit_applicable_income_disregard", + "housing_benefit_assessable_capital", + "council_tax_reduction_applicable_income", + "council_tax_reduction_relevant_income_based_benefit", + "tax_credits_applicable_income", + "targeted_childcare_entitlement_eligible", + "would_claim_IS", + "income_support_eligible", +] +MEMBER_READERS = [ + "is_scp_eligible", + "maintenance_loan_entitled_to_benefits", + "is_on_income_related_esa", + "is_on_income_based_jsa", + "is_on_income_support", +] +AWARDS = st.sampled_from([0, 0, 200, 3_000]) + + +@st.composite +def award_reports(draw): + return { + "esa_income_reported": draw(AWARDS), + "jsa_income_reported": draw(AWARDS), + "income_support_reported": draw(AWARDS), + } + + +@st.composite +def families(draw): + """A claimant, an optional partner, up to two dependants and sometimes an + adult outside the couple who is already a member. + + Qualifying young persons and the existing outside adult may report awards + of their own, so the properties also cover members who are on an award + in their own right. + """ + dependants = [] + for _ in range(draw(st.integers(0, 2))): + if draw(st.booleans()): + dependants.append({"age": draw(st.integers(0, 15))}) + else: + # A qualifying young person: 16-19 in non-advanced education. + dependants.append( + { + "age": draw(st.integers(16, 19)), + "current_education": "UPPER_SECONDARY", + **draw(award_reports()), + } + ) + eldest_dependant = max([d["age"] for d in dependants], default=0) + adults = [] + for i in range(1 + draw(st.booleans())): + adults.append( + { + "age": draw(st.integers(max(18, eldest_dependant + 16), 60)), + "employment_income": draw(st.sampled_from([0, 0, 8_000, 20_000])), + "is_parent": bool(dependants), + "receives_carer_benefit": draw(st.booleans()), + # A disabled claimant or partner gives the family the + # disability premium, which can put a student partner on the + # maintenance-loan benefits schedule (HB Regs 2006 reg + # 56(2)(c)). + **draw( + st.sampled_from( + [{}, {"is_disabled_for_benefits": True, "pip_dl": 2_000}] + ) + ), + "care_hours": draw(st.sampled_from([0, 35])), + "current_education": draw( + st.sampled_from(["NOT_IN_EDUCATION", "TERTIARY"]) + ), + **draw(award_reports()), + } + ) + non_couple = list(dependants) + if draw(st.booleans()): + non_couple.append( + { + "age": draw(st.integers(20, 60)), + "current_education": "NOT_IN_EDUCATION", + **draw(award_reports()), + } + ) + household = { + "country": draw(st.sampled_from(["ENGLAND", "SCOTLAND", "WALES"])), + # £7,000 gives tariff income of £208 a year, more than a £200 report + # on its own but less than two reports together. + "savings": draw(st.sampled_from([0, 7_000, 20_000])), + } + return adults, non_couple, household + + +@st.composite +def other_members(draw): + """An adult who is neither claimant, partner nor a dependant. + + They are under state pension age and not in education, so a 16 to 19 year + old is not a qualifying young person. They have no income other than the + awards they report. + """ + return { + "age": draw(st.integers(16, 60)), + "current_education": "NOT_IN_EDUCATION", + **draw(award_reports()), + } + + +def situation(units, year=YEAR): + people, benunits, households = {}, {}, {} + for i, (adults, non_couple, household, other) in enumerate(units): + members = [(m, True) for m in adults] + [(m, False) for m in non_couple] + if other is not None: + members.append((other, False)) + names = [] + for j, (inputs, claimant_or_partner) in enumerate(members): + name = f"p{i}_{j}" + people[name] = {k: {year: v} for k, v in inputs.items()} + people[name]["is_claimant_or_partner"] = {year: claimant_or_partner} + names.append(name) + # claims_all_entitled_benefits sums reports across the whole + # simulation, so fix the take-up mode per benefit unit: the variants + # must not share it. + benunits[f"b{i}"] = { + "members": names, + "claims_all_entitled_benefits": {year: False}, + } + households[f"h{i}"] = { + "members": names, + **{k: {year: v} for k, v in household.items()}, + } + return {"people": people, "benunits": benunits, "households": households} + + +SETTINGS = settings( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + + +@SETTINGS +@given(st.lists(st.tuples(families(), other_members()), min_size=1, max_size=6)) +def test_other_members_awards_never_change_the_readers(drawn): + without = [(*family, None) for family, _ in drawn] + with_other = [(*family, other) for family, other in drawn] + sim = Simulation(situation=situation(without + with_other)) + flags = sim.calculate("is_claimant_or_partner", YEAR) + dependant = sim.calculate("is_child_or_young_person_for_legacy_benefits", YEAR) + sizes = [len(a) + len(d) + (o is not None) for a, d, _, o in without + with_other] + starts = np.cumsum([0] + sizes[:-1]) + k = len(drawn) + for i in range(k): + # The added member is the last of its benefit unit: neither claimant, + # partner nor a dependant. + last = starts[k + i] + sizes[k + i] - 1 + assert not flags[last] and not dependant[last], drawn[i] + for variable in FAMILY_READERS: + values = sim.calculate(variable, YEAR) + for i in range(k): + assert np.isclose(values[i], values[k + i], atol=0.01), ( + variable, + drawn[i], + ) + for variable in MEMBER_READERS: + values = sim.calculate(variable, YEAR) + for i in range(k): + n = sizes[i] + assert np.array_equal( + values[starts[i] : starts[i] + n], + values[starts[k + i] : starts[k + i] + n], + ), (variable, drawn[i]) + + +@SETTINGS +@given(st.lists(st.tuples(families(), other_members()), min_size=1, max_size=6)) +def test_claimant_or_partner_awards_bounded_by_benefit_unit_awards(drawn): + units = [(*family, other) for family, other in drawn] + sim = Simulation(situation=situation(units)) + for scoped, total, report in [ + ("claimant_or_partner_esa_income", "esa_income", "esa_income_reported"), + ("claimant_or_partner_jsa_income", "jsa_income", "jsa_income_reported"), + ]: + scoped_award = sim.calculate(scoped, YEAR) + total_award = sim.calculate(total, YEAR) + assert (scoped_award >= 0).all() + assert (scoped_award <= total_award + 0.01).all() + for i, (_, non_couple, _, other) in enumerate(units): + if other[report] == 0 and all(m.get(report, 0) == 0 for m in non_couple): + # Nobody outside the couple reports this award: the two + # coincide. + assert np.isclose(scoped_award[i], total_award[i]), (scoped, units[i]) + + +def reference_award(reported, capital, params): + """The income-related award on a reported amount, read directly from the + regulations. Works on scalars or arrays. + + Capital is the household's savings (the only capital input here), all of + it the benefit unit's, as it is the only one in its household. Tariff + income is £1 a week for each £250 or part above £6,000, and capital above + £16,000 removes the award (ESA Regs 2008 regs 110 and 118; JSA Regs 1996 + regs 107 and 116, as parameterised). + """ + capital_rules = params.capital + steps = np.ceil( + np.maximum(0, capital - capital_rules.tariff_income.threshold) + / capital_rules.tariff_income.step + ) + tariff = steps * capital_rules.tariff_income.amount * 52 + return np.where( + (reported > 0) & (capital <= capital_rules.limit), + np.maximum(0.0, reported - tariff), + 0.0, + ) + + +def reference_claimant_or_partner_award(adults, household, report, params): + """The award on the claimant's and partner's reports.""" + return float( + reference_award(sum(a[report] for a in adults), household["savings"], params) + ) + + +@SETTINGS +@given(st.lists(st.tuples(families(), other_members()), min_size=1, max_size=6)) +def test_claimant_or_partner_awards_match_reference(drawn): + units = [(*family, other) for family, other in drawn] + sim = Simulation(situation=situation(units)) + dwp = sim.tax_benefit_system.parameters(YEAR).gov.dwp + for variable, report, params, active in [ + ("claimant_or_partner_esa_income", "esa_income_reported", dwp.ESA.income, 1), + ( + "claimant_or_partner_jsa_income", + "jsa_income_reported", + dwp.JSA.income, + dwp.JSA.income.active, + ), + ]: + award = sim.calculate(variable, YEAR) + for i, (adults, _, household, other) in enumerate(units): + expected = active * reference_claimant_or_partner_award( + adults, household, report, params + ) + assert np.isclose(award[i], expected), (variable, units[i]) + + +def payee(sim, claimant_or_partner, reports): + """The claimant or partner who reports the award, or the claimant (the + head, else the eldest of the couple) where neither does.""" + benunit = sim.calculate("benunit_id", YEAR, map_to="person") + head = sim.calculate("is_benunit_head", YEAR) & claimant_or_partner + age = sim.calculate("age", YEAR) + result = np.zeros(len(benunit), dtype=bool) + for unit in np.unique(benunit): + members = benunit == unit + couple = members & claimant_or_partner + reporting = couple & reports + if reporting.any(): + result |= reporting + elif (members & head).any(): + result |= members & head + elif couple.any(): + eldest = np.flatnonzero(couple)[np.argmax(age[couple])] + result[eldest] = True + return result + + +@SETTINGS +@given(st.lists(st.tuples(families(), other_members()), min_size=1, max_size=6)) +def test_person_is_on_award_from_couple_or_own_report(drawn): + units = [(*family, other) for family, other in drawn] + sim = Simulation(situation=situation(units)) + claimant_or_partner = sim.calculate("is_claimant_or_partner", YEAR) + capital = sim.calculate("savings", YEAR, map_to="person") + dwp = sim.tax_benefit_system.parameters(YEAR).gov.dwp + for person_variable, couple_award, report, params, active in [ + ( + "is_on_income_related_esa", + "claimant_or_partner_esa_income", + "esa_income_reported", + dwp.ESA.income, + True, + ), + ( + "is_on_income_based_jsa", + "claimant_or_partner_jsa_income", + "jsa_income_reported", + dwp.JSA.income, + dwp.JSA.income.active, + ), + ( + "is_on_income_support", + "income_support", + "income_support_reported", + None, + dwp.income_support.active, + ), + ]: + on = sim.calculate(person_variable, YEAR) + reported = sim.calculate(report, YEAR) + # Of the claimant and partner, only the payee is on a couple's award: + # the one who reports it, or the claimant where neither does + # (HB Regs 2006 reg 2(3), (3A): "payable to him"). + couple = (sim.calculate(couple_award, YEAR, map_to="person") > 0) & payee( + sim, claimant_or_partner, reported > 0 + ) + if params is None: + # Income Support: their own report, while it is in payment. + own = active & (reported > 0) + else: + # The award on their own report alone, on the household's capital. + own = active & (reference_award(reported, capital, params) > 0) + expected = np.where(claimant_or_partner, couple, own) + assert np.array_equal(on, expected), (person_variable, units) + + +@settings( + max_examples=10, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given( + st.lists( + st.tuples( + st.lists(st.sampled_from([0, 0, 200, 3_000]), min_size=1, max_size=2), + st.sampled_from([None, 0, 200, 3_000]), + st.sampled_from([0, 3_000]), + st.sampled_from([0, 7_000]), + ), + min_size=1, + max_size=6, + ) +) +def test_other_members_esa_never_changes_non_dependant_exemption(drawn): + """A non-dependant is exempt only through their own (or couple's) award. + + Each household in Merton has an older applicant (the household head) and a + non-dependant benefit unit of one or two claimants, sometimes with an adult + outside their couple who is already a member, with or without a further + adult who reports income-related ESA. Savings of £7,000 give tariff income + of £208 a year, more than a £200 report on its own. The existing members' + individual deductions never change when the further adult is added. + """ + year = NON_DEP_YEAR + people, benunits, households = {}, {}, {} + layouts = [] + for variant, other_present in [(0, False), (1, True)]: + for i, (claimant_awards, existing_award, other_award, savings) in enumerate( + drawn + ): + tag = f"{variant}_{i}" + names = [f"applicant_{tag}"] + people[names[0]] = {"age": {year: 60}} + unit = [] + for j, award in enumerate(claimant_awards): + name = f"nondep_{tag}_{j}" + people[name] = { + "age": {year: 30 + j}, + "is_claimant_or_partner": {year: True}, + "current_education": {year: "NOT_IN_EDUCATION"}, + "esa_income_reported": {year: award}, + } + unit.append(name) + if existing_award is not None: + name = f"existing_{tag}" + people[name] = { + "age": {year: 45}, + "is_claimant_or_partner": {year: False}, + "current_education": {year: "NOT_IN_EDUCATION"}, + "esa_income_reported": {year: existing_award}, + } + unit.append(name) + if other_present: + name = f"other_{tag}" + people[name] = { + "age": {year: 25}, + "is_claimant_or_partner": {year: False}, + "current_education": {year: "NOT_IN_EDUCATION"}, + "esa_income_reported": {year: other_award}, + } + unit.append(name) + people[names[0]]["is_claimant_or_partner"] = {year: True} + benunits[f"applicant_unit_{tag}"] = {"members": names} + benunits[f"nondep_unit_{tag}"] = { + "members": unit, + "universal_credit": {year: 0}, + } + households[f"h_{tag}"] = { + "members": names + unit, + "country": {year: "ENGLAND"}, + "local_authority": {year: "MERTON"}, + "savings": {year: savings}, + } + layouts.append(names + unit) + sim = Simulation( + situation={"people": people, "benunits": benunits, "households": households} + ) + deduction = dict( + zip( + people, + sim.calculate( + "merton_council_tax_reduction_individual_non_dep_deduction", year + ), + ) + ) + for i in range(len(drawn)): + without, with_other = layouts[i], layouts[len(drawn) + i] + for a, b in zip(without, with_other): + assert np.isclose(deduction[a], deduction[b]), drawn[i] + + +def _difference(variable, base_people, award_people, benunit_inputs=None): + def build(people): + names = list(people) + return { + "people": { + n: {k: {YEAR: v} for k, v in inputs.items()} + for n, inputs in people.items() + }, + "benunits": { + "b": { + "members": names, + **{k: {YEAR: v} for k, v in (benunit_inputs or {}).items()}, + } + }, + "households": {"h": {"members": names}}, + } + + base = Simulation(situation=build(base_people)).calculate(variable, YEAR)[0] + award = Simulation(situation=build(award_people)).calculate(variable, YEAR)[0] + return award - base + + +CLAIMANT = {"age": 40, "is_claimant_or_partner": True, "employment_income": 10_000} +OTHER = { + "age": 30, + "is_claimant_or_partner": False, + "current_education": "NOT_IN_EDUCATION", +} + + +def test_claimant_and_partner_awards_count_in_full_as_their_ctr_income(): + """CTR (Prescribed Requirements) (England) Regs 2012 Sch 1 para 11: the + partner's income is the applicant's. + + The claimant earns £10,000, below the tax and NI thresholds, so each + award adds exactly its amount. + """ + ctr = "council_tax_reduction_applicable_income" + no_uc = {"universal_credit": 0} + claimant_esa = {"claimant": {**CLAIMANT, "esa_income_reported": 5_000}} + assert np.isclose( + _difference(ctr, {"claimant": CLAIMANT}, claimant_esa, no_uc), 5_000 + ) + partner = {"age": 38, "is_claimant_or_partner": True} + couple = {"claimant": CLAIMANT, "partner": partner} + couple_jsa = { + "claimant": CLAIMANT, + "partner": {**partner, "jsa_income_reported": 3_000}, + } + assert np.isclose(_difference(ctr, couple, couple_jsa, no_uc), 3_000) + # Another member's award adds nothing. + other_esa = { + "claimant": CLAIMANT, + "other": {**OTHER, "esa_income_reported": 5_000}, + } + with_other = {"claimant": CLAIMANT, "other": OTHER} + assert np.isclose(_difference(ctr, with_other, other_esa, no_uc), 0) + + +def _value(variable, people): + # Not claiming Universal Credit, so the Universal Credit limb of the + # Housing Benefit passport cannot apply: only the legacy awards can. + names = list(people) + situation = { + "people": { + n: {k: {YEAR: v} for k, v in inputs.items()} for n, inputs in people.items() + }, + "benunits": {"b": {"members": names, "would_claim_uc": {YEAR: False}}}, + "households": {"h": {"members": names}}, + } + return Simulation(situation=situation).calculate(variable, YEAR)[0] + + +def test_claimant_and_partner_awards_passport_housing_benefit_income(): + """HB Regs 2006 Sch 5 para 4: the whole income of a claimant on + income-related ESA or income-based JSA is disregarded. The partner's + award is the couple's (reg 25(1)), so it passports them too, and another + member's award passports nobody. + + The claimant earns £10,000, below the tax and NI thresholds. Without an + award their income is assessed, less the £5 a week single or £10 a week + couple disregard (Sch 4 paras 10 and 7): 10,000 - 260 = 9,740 and + 10,000 - 520 = 9,480. + """ + hb = "housing_benefit_applicable_income" + assert np.isclose(_value(hb, {"claimant": CLAIMANT}), 9_740) + claimant_esa = {"claimant": {**CLAIMANT, "esa_income_reported": 5_000}} + assert np.isclose(_value(hb, claimant_esa), 0) + partner = {"age": 38, "is_claimant_or_partner": True} + assert np.isclose(_value(hb, {"claimant": CLAIMANT, "partner": partner}), 9_480) + couple_jsa = { + "claimant": CLAIMANT, + "partner": {**partner, "jsa_income_reported": 3_000}, + } + assert np.isclose(_value(hb, couple_jsa), 0) + # Another member's award: the claimant's income is assessed as before. + with_other = {"claimant": CLAIMANT, "other": OTHER} + other_esa = { + "claimant": CLAIMANT, + "other": {**OTHER, "esa_income_reported": 5_000}, + } + assert np.isclose(_value(hb, with_other), 9_740) + assert np.isclose(_value(hb, other_esa), 9_740) + + +def _claimant_and_other(other_report): + people = { + "claimant": {"age": {YEAR: 40}, "is_claimant_or_partner": {YEAR: True}}, + "other": { + "age": {YEAR: 30}, + "is_claimant_or_partner": {YEAR: False}, + "current_education": {YEAR: "NOT_IN_EDUCATION"}, + **{k: {YEAR: v} for k, v in other_report.items()}, + }, + } + return { + "people": people, + "benunits": {"b": {"members": list(people)}}, + "households": {"h": {"members": list(people)}}, + } + + +def test_award_set_after_construction_is_taken_as_entered(): + """An award set with set_input after the simulation is built is entered + directly, so it is the claimant's whatever another member reports.""" + for variable, scoped, report in [ + ("esa_income", "claimant_or_partner_esa_income", "esa_income_reported"), + ("jsa_income", "claimant_or_partner_jsa_income", "jsa_income_reported"), + ]: + sim = Simulation(situation=_claimant_and_other({report: 5_000})) + sim.set_input(variable, YEAR, np.array([3_000.0])) + assert np.isclose(sim.calculate(scoped, YEAR)[0], 3_000), variable + + +def test_award_set_on_a_branch_is_taken_as_entered_on_that_branch(): + for variable, scoped, report in [ + ("esa_income", "claimant_or_partner_esa_income", "esa_income_reported"), + ("jsa_income", "claimant_or_partner_jsa_income", "jsa_income_reported"), + ]: + sim = Simulation(situation=_claimant_and_other({report: 5_000})) + branch = sim.get_branch("entered") + branch.set_input(variable, YEAR, np.array([3_000.0])) + assert np.isclose(branch.calculate(scoped, YEAR)[0], 3_000), variable + # The main simulation still reads the reports: another member's award + # is not the claimant's. + assert np.isclose(sim.calculate(scoped, YEAR)[0], 0), variable + + +def test_award_set_after_the_value_is_cached_is_taken_as_entered(): + for variable, scoped, report in [ + ("esa_income", "claimant_or_partner_esa_income", "esa_income_reported"), + ("jsa_income", "claimant_or_partner_jsa_income", "jsa_income_reported"), + ]: + sim = Simulation(situation=_claimant_and_other({report: 5_000})) + assert np.isclose(sim.calculate(scoped, YEAR)[0], 0), variable + sim.set_input(variable, YEAR, np.array([3_000.0])) + sim.delete_arrays(scoped) + assert np.isclose(sim.calculate(scoped, YEAR)[0], 3_000), variable + + +def test_late_zero_entry_overrides_the_claimants_reports(): + for variable, scoped, report in [ + ("esa_income", "claimant_or_partner_esa_income", "esa_income_reported"), + ("jsa_income", "claimant_or_partner_jsa_income", "jsa_income_reported"), + ]: + situation = _claimant_and_other({}) + situation["people"]["claimant"][report] = {YEAR: 3_000} + sim = Simulation(situation=situation) + sim.set_input(variable, YEAR, np.array([0.0])) + assert np.isclose(sim.calculate(scoped, YEAR)[0], 0), variable diff --git a/policyengine_uk/utils/benefit_unit.py b/policyengine_uk/utils/benefit_unit.py index 853726105f..80e936c34b 100644 --- a/policyengine_uk/utils/benefit_unit.py +++ b/policyengine_uk/utils/benefit_unit.py @@ -9,7 +9,10 @@ def add_for_members(benunit, period, variables, members): are. Means tests pass the claimant and partner plus the programme's own children or young persons, so a member who is none of these (for example an 18-year-old who is neither a partner nor a qualifying young person) - does not count. + does not count. A benefit-unit variable must therefore already cover only + those members: pass claimant_or_partner_esa_income and + claimant_or_partner_jsa_income, not esa_income and jsa_income, which also + hold awards that other members claim in their own right. """ person = benunit.members total = 0 diff --git a/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_entitled_to_benefits.py b/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_entitled_to_benefits.py index fce3b27d78..5a19a40043 100644 --- a/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_entitled_to_benefits.py +++ b/policyengine_uk/variables/gov/dfe/maintenance_loans/maintenance_loan_entitled_to_benefits.py @@ -7,9 +7,21 @@ class maintenance_loan_entitled_to_benefits(Variable): label = "Entitled to benefits for maintenance loan assessment" documentation = ( "Proxy for the Student Finance England 'students entitled to benefits' maintenance loan schedule. " - "This uses observable parent/disability/ESA-related signals already present in the model." + "This uses observable parent/disability/ESA-related signals already present in the model: " + "income-related ESA payable to the student (HB Regs 2006 reg 56(2)(a)), and, for a student " + "who is the claimant or partner, a disability or severe disability premium in their applicable " + "amount (reg 56(2)(c)), which a partner's disability benefit can give. The premiums are the " + "model's disability_premium and severe_disability_premium, which do not apply the State " + "Pension Credit age or limited capability for work conditions." ) definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2011/1986/regulation/71", + "https://www.legislation.gov.uk/uksi/2011/1986/regulation/61", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/56", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/12", + "https://www.legislation.gov.uk/uksi/2006/213/schedule/3/paragraph/13", + ) def formula(person, period, parameters): qualifying_disability_support = ( @@ -27,6 +39,29 @@ def formula(person, period, parameters): > 0 ) has_child = person("is_parent", period) - receives_income_related_esa = person.benunit("esa_income", period) > 0 + # Income-related ESA payable to the student, not to a partner, parent or + # other benefit-unit member. The schedule (SI 2011/1986 reg 71(1)(h)(iii)) + # follows the special support grant test in reg 61(2)(b), which in turn + # follows HB Regs 2006 reg 56(2)(a): a full-time student "who is a person + # on ... an income-related employment and support allowance", that is, + # one to whom it "is payable" (reg 2(3A)). + receives_income_related_esa = person("is_on_income_related_esa", period) + # Reg 56(2)(c): a full-time student "whose applicable amount would, but + # for paragraph (1), include the ... disability premium or severe + # disability premium". For a couple the disability premium turns on + # either partner's disability benefit (Sch 3 paras 12 and 13(1)(a)), so + # a student whose partner receives PIP qualifies though the student + # does not. The premiums are the claimant's family's, so this applies + # to a student who is the claimant or partner, not to a dependant or + # another member of the benefit unit. + premium_in_applicable_amount = person("is_claimant_or_partner", period) & ( + (person.benunit("disability_premium", period) > 0) + | (person.benunit("severe_disability_premium", period) > 0) + ) - return has_child | qualifying_disability_support | receives_income_related_esa + return ( + has_child + | qualifying_disability_support + | receives_income_related_esa + | premium_in_applicable_amount + ) diff --git a/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py new file mode 100644 index 0000000000..651d32fff1 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/_legacy_award_payee.py @@ -0,0 +1,52 @@ +"""Who is "on" a legacy income-related award.""" + +from policyengine_uk.model_api import * + + +def is_payee_of_couple_award(person, period, reported): + """Whether the claimant's or partner's family award is payable to this + person. A person is on income-based JSA or income-related ESA on a day + the allowance "is payable to him" (HB Regs 2006 reg 2(3) and (3A)); a + person on Income Support is one "in receipt of" it (reg 2(1)). A couple's + award is paid to the claimant, not the partner. The payee is the + claimant or partner who reports the award, or the claimant where neither + does (an award entered directly). Another member's report never moves + the payee.""" + claimant_or_partner = person("is_claimant_or_partner", period) + reports = (person(reported, period) > 0) & claimant_or_partner + head = claimant_or_partner & person("is_benunit_head", period) + eldest = claimant_or_partner & ( + person.get_rank( + person.benunit, -person("age", period), condition=claimant_or_partner + ) + == 0 + ) + claimant = where(person.benunit.any(head), head, eldest) + return where(person.benunit.any(reports), reports, claimant) + + +def own_report_is_paid(person, period, award): + """Whether income-related ESA or income-based JSA (award: "esa_income" or + "jsa_income") is paid on this person's own report, for a member who is + neither the claimant nor the partner and so claims in their own right. + It is paid when the award on their report alone is positive: the report + exceeds the tariff income from the benefit unit's capital, within the + capital limit, as income_related_esa_award and income_related_jsa_award + screen it. Their own capital is not observed, so the benefit unit's + stands in for it. The benefit unit's award must also be positive, so a + reform that removes or zeroes the benefit removes the status too. + + Another member's report never changes this: once this person reports, + the capital test reads the household's capital whoever else reports, and + the award on their report alone does not depend on other reports. On the + formula path a positive award on their report implies a positive benefit + unit award, since adding reports never lowers it.""" + benunit = person.benunit + return ( + benunit(f"{award}_eligible", period) + & ( + person(f"{award}_reported", period) + > benunit(f"{award}_tariff_income", period) + ) + & (benunit(award, period) > 0) + ) diff --git a/policyengine_uk/variables/gov/dwp/claimant_or_partner_esa_income.py b/policyengine_uk/variables/gov/dwp/claimant_or_partner_esa_income.py new file mode 100644 index 0000000000..ae5dabed94 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/claimant_or_partner_esa_income.py @@ -0,0 +1,64 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.esa_income import income_related_esa_award + + +class claimant_or_partner_esa_income(Variable): + value_type = float + entity = BenUnit + label = "income-related ESA of the claimant or partner" + documentation = ( + "Income-related ESA of the claimant and partner. Means tests and passports that " + "ask whether the claimant or partner is on income-related ESA read this. " + "Anyone else in the benefit unit claims in their own right, so their " + "award is left out here: a non-dependent adult, or a young person " + "with an award of their own, which is payable to them. It still " + "counts in esa_income and so in household income. Which awards " + "esa_income holds is decided by value: " + "when it holds the award its formula gives on all reported awards, " + "this is the same award on the claimant's and partner's reports; " + "when it holds the plain total of the reports (the " + "disable_simulated_benefits reform), this is the plain total of the " + "claimant's and partner's reports; when it holds anything else (an " + "award entered directly, or a reform that replaces it), that value " + "is taken to be the claimant's or partner's. Values are compared to " + "within half a penny after rounding to the precision esa_income is stored " + "in. A stored zero is zero. " + "An entered esa_income equal to either reported amount is read through " + "the reports, so to enter the claimant's or partner's award whatever " + "other members report, enter this variable directly." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/4/section/136", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/25", + "https://www.legislation.gov.uk/uksi/2008/794/regulation/83", + ) + + def formula(benunit, period, parameters): + person = benunit.members + esa_income = benunit("esa_income", period) + reported = person("esa_income_reported", period) + reported_total = benunit.sum(reported) + claimant_or_partner_reported = benunit.sum( + reported * person("is_claimant_or_partner", period) + ) + award_on_all_reports = income_related_esa_award(benunit, period, reported_total) + award_on_claimant_or_partner_reports = income_related_esa_award( + benunit, period, claimant_or_partner_reported + ) + # Compare in the precision esa_income is stored in (float32), so the + # formula's own award always matches the award recomputed here. + stored = esa_income.dtype + as_formula = np.isclose( + esa_income, award_on_all_reports.astype(stored), rtol=0, atol=0.005 + ) + as_reported_total = np.isclose( + esa_income, reported_total.astype(stored), rtol=0, atol=0.005 + ) + scoped = where( + as_formula, + award_on_claimant_or_partner_reports, + where(as_reported_total, claimant_or_partner_reported, esa_income), + ) + return where(esa_income > 0, scoped, 0) diff --git a/policyengine_uk/variables/gov/dwp/claimant_or_partner_jsa_income.py b/policyengine_uk/variables/gov/dwp/claimant_or_partner_jsa_income.py new file mode 100644 index 0000000000..b528c41a52 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/claimant_or_partner_jsa_income.py @@ -0,0 +1,64 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp.jsa_income import income_related_jsa_award + + +class claimant_or_partner_jsa_income(Variable): + value_type = float + entity = BenUnit + label = "income-based JSA of the claimant or partner" + documentation = ( + "Income-based JSA of the claimant and partner. Means tests and passports that " + "ask whether the claimant or partner is on income-based JSA read this. " + "Anyone else in the benefit unit claims in their own right, so their " + "award is left out here: a non-dependent adult, or a young person " + "with an award of their own, which is payable to them. It still " + "counts in jsa_income and so in household income. Which awards " + "jsa_income holds is decided by value: " + "when it holds the award its formula gives on all reported awards, " + "this is the same award on the claimant's and partner's reports; " + "when it holds the plain total of the reports (the " + "disable_simulated_benefits reform), this is the plain total of the " + "claimant's and partner's reports; when it holds anything else (an " + "award entered directly, or a reform that replaces it), that value " + "is taken to be the claimant's or partner's. Values are compared to " + "within half a penny after rounding to the precision jsa_income is stored " + "in. A stored zero is zero. " + "An entered jsa_income equal to either reported amount is read through " + "the reports, so to enter the claimant's or partner's award whatever " + "other members report, enter this variable directly." + ) + definition_period = YEAR + unit = GBP + reference = ( + "https://www.legislation.gov.uk/ukpga/1992/4/section/136", + "https://www.legislation.gov.uk/uksi/2006/213/regulation/25", + "https://www.legislation.gov.uk/uksi/1996/207/regulation/88", + ) + + def formula(benunit, period, parameters): + person = benunit.members + jsa_income = benunit("jsa_income", period) + reported = person("jsa_income_reported", period) + reported_total = benunit.sum(reported) + claimant_or_partner_reported = benunit.sum( + reported * person("is_claimant_or_partner", period) + ) + award_on_all_reports = income_related_jsa_award(benunit, period, reported_total) + award_on_claimant_or_partner_reports = income_related_jsa_award( + benunit, period, claimant_or_partner_reported + ) + # Compare in the precision jsa_income is stored in (float32), so the + # formula's own award always matches the award recomputed here. + stored = jsa_income.dtype + as_formula = np.isclose( + jsa_income, award_on_all_reports.astype(stored), rtol=0, atol=0.005 + ) + as_reported_total = np.isclose( + jsa_income, reported_total.astype(stored), rtol=0, atol=0.005 + ) + scoped = where( + as_formula, + award_on_claimant_or_partner_reports, + where(as_reported_total, claimant_or_partner_reported, jsa_income), + ) + return where(jsa_income > 0, scoped, 0) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py index 5496bc0a06..b97d9ac316 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/applicable_income/housing_benefit_applicable_income.py @@ -42,11 +42,16 @@ def formula(benunit, period, parameters): pension_age_regulations = benunit( "housing_benefit_pension_age_regulations_apply", period ) + # Only the claimant's and partner's income-related awards: their income is + # the claimant's (SSCBA s.136(1); HB Regs 2006 reg 25(1)). A member outside + # the family who has an award of their own claims in their own right. + # Income Support already needs the claimant's or partner's own award + # (income_support_eligible). BENUNIT_MEANS_TESTED_BENEFITS = [ "child_benefit", "income_support", - "jsa_income", - "esa_income", + "claimant_or_partner_jsa_income", + "claimant_or_partner_esa_income", ] PERSONAL_BENEFITS = [ "carers_allowance", @@ -132,17 +137,18 @@ def formula(benunit, period, parameters): # income support, an income-based jobseeker's allowance or an # income-related employment and support allowance. Para 5 does the # same where the claimant's partner in a joint-claim couple is on - # income-based JSA. The model holds these awards for the benefit unit - # and cannot tell which member claims Housing Benefit (a couple - # choose, reg 82(1)), so it applies the disregard whichever member is - # on the benefit. There is no age condition: SI 2006/213 reg 5(1)(b) - # (NI: SR 2006/405 reg 5(1)(b)) applies these regulations to a - # claimant over the qualifying age for State Pension Credit whose - # partner is on one of these benefits, so a mixed-age couple whose - # younger member is on income-related ESA or Universal Credit is - # covered. Universal Credit is read before the benefit cap - # (housing_benefit_on_passporting_benefit), which avoids a circular - # dependency through the cap. + # income-based JSA. The model cannot tell which of the claimant and + # partner claims Housing Benefit (a couple choose, reg 82(1)), so it + # applies the disregard whichever of them is on the benefit. Another + # member's own award does not count: they claim in their own right + # (in_receipt_of_income_support_jsa_ib_or_esa_ir). There is no age + # condition: SI 2006/213 reg 5(1)(b) (NI: SR 2006/405 reg 5(1)(b)) + # applies these regulations to a claimant over the qualifying age for + # State Pension Credit whose partner is on one of these benefits, so a + # mixed-age couple whose younger member is on income-related ESA or + # Universal Credit is covered. Universal Credit is read before the + # benefit cap (housing_benefit_on_passporting_benefit), which avoids a + # circular dependency through the cap. on_passporting_benefit = benunit( "housing_benefit_on_passporting_benefit", period ) diff --git a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py index b0b3a3edc8..6f525e3d70 100644 --- a/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py +++ b/policyengine_uk/variables/gov/dwp/housing_benefit/housing_benefit_on_passporting_benefit.py @@ -9,7 +9,8 @@ class housing_benefit_on_passporting_benefit(Variable): "Support Allowance, which disregards the whole of its earnings, other " "income and capital in the Housing Benefit means test. Income Support, " "income-based JSA and income-related ESA count when a positive amount " - "is paid (in_receipt_of_income_support_jsa_ib_or_esa_ir). A person is " + "is paid on the claimant's or partner's award, not another member's " + "(in_receipt_of_income_support_jsa_ib_or_esa_ir). A person is " "on Universal Credit on any day they are entitled to it, whether it is " "in payment or not (SI 2006/213 and SR 2006/405 reg 2(3B)). The model " "reads that as Universal Credit above nil before the benefit cap and " diff --git a/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py b/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py index 6603fedd03..931bb81550 100644 --- a/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py +++ b/policyengine_uk/variables/gov/dwp/in_receipt_of_income_support_jsa_ib_or_esa_ir.py @@ -6,12 +6,22 @@ class in_receipt_of_income_support_jsa_ib_or_esa_ir(Variable): documentation = ( "Whether this benefit unit is paid Income Support, income-based " "Jobseeker's Allowance or income-related Employment and Support " - "Allowance: a positive amount of any of them. The model holds these " - "awards at benefit-unit level, so it does not distinguish the claimant " - "from the partner. The regulations also count as on income-based JSA " - "or income-related ESA some days on which nothing is paid (a sanction, " - "a waiting day, a loss-of-benefit restriction), which the model does " - "not see." + "Allowance: a positive amount of any of them on the claimant's or " + "partner's award. A couple's award covers both of them, so the model " + "does not distinguish the claimant from the partner. Anyone else in " + "the benefit unit claims in their own right, so their award does not " + "count: a non-dependent adult, or a young person with an award of " + "their own, which is payable to them, not to the claimant. Income " + "Support is already only the claimant's or partner's award. JSA and " + "ESA are read through claimant_or_partner_jsa_income and " + "claimant_or_partner_esa_income, so a jsa_income or esa_income " + "entered for the benefit unit is taken to be the claimant's or " + "partner's unless it equals the award the members' reports give, or " + "their plain total, in which case it is read through the reports. " + "The regulations also count as on " + "income-based JSA or income-related ESA some days on which nothing is " + "paid (a sanction, a waiting day, a loss-of-benefit restriction), which " + "the model does not see." ) entity = BenUnit definition_period = YEAR @@ -22,8 +32,10 @@ class in_receipt_of_income_support_jsa_ib_or_esa_ir(Variable): ) def formula(benunit, period, parameters): + # Income Support already needs the claimant's or partner's own award + # (income_support_eligible). return ( (benunit("income_support", period) > 0) - | (benunit("jsa_income", period) > 0) - | (benunit("esa_income", period) > 0) + | (benunit("claimant_or_partner_jsa_income", period) > 0) + | (benunit("claimant_or_partner_esa_income", period) > 0) ) diff --git a/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py b/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py new file mode 100644 index 0000000000..d22686eb13 --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_on_income_based_jsa.py @@ -0,0 +1,39 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp._legacy_award_payee import ( + is_payee_of_couple_award, + own_report_is_paid, +) + + +class is_on_income_based_jsa(Variable): + value_type = bool + entity = Person + label = "on income-based JSA" + documentation = ( + "Whether an income-based jobseeker's allowance is payable to this " + 'person (HB Regs 2006 reg 2(3): on any day it "is payable to him"; ' + "the council tax reduction schemes use the same definition). A " + "couple's award is paid to the claimant, not the partner: of the " + "claimant and partner, only the payee is on it, meaning the one who " + "reports the award, or the claimant where neither does, and only " + "while their award (claimant_or_partner_jsa_income) is positive. The " + "model has no joint-claim JSA input. Any other member of the benefit " + "unit, such as a non-dependent adult, claims in their own right and " + "is on it only if the award on their own report alone is positive: " + "the report exceeds the tariff income from the benefit unit's " + "capital, within the capital limit, while the benefit unit's " + "modelled award is positive." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + ) + + def formula(person, period, parameters): + couple_award = ( + person.benunit("claimant_or_partner_jsa_income", period) > 0 + ) & (is_payee_of_couple_award(person, period, "jsa_income_reported")) + # The award on their own report alone, never another member's. + own_award = own_report_is_paid(person, period, "jsa_income") + return where(person("is_claimant_or_partner", period), couple_award, own_award) diff --git a/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py b/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py new file mode 100644 index 0000000000..0989dfd6de --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_on_income_related_esa.py @@ -0,0 +1,38 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp._legacy_award_payee import ( + is_payee_of_couple_award, + own_report_is_paid, +) + + +class is_on_income_related_esa(Variable): + value_type = bool + entity = Person + label = "on income-related ESA" + documentation = ( + "Whether an income-related employment and support allowance is " + 'payable to this person (HB Regs 2006 reg 2(3A): on any day it "is ' + 'payable to him"; the council tax reduction schemes use the same ' + "definition). A couple's award is paid to the claimant, not the " + "partner: of the claimant and partner, only the payee is on it, " + "meaning the one who reports the award, or the claimant where neither " + "does, and only while their award (claimant_or_partner_esa_income) " + "is positive. Any other member of the benefit unit, such as a " + "non-dependent adult, claims in their own right and is on it only if " + "the award on their own report alone is positive: the report exceeds " + "the tariff income from the benefit unit's capital, within the " + "capital limit, while the benefit unit's modelled award is positive." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + ) + + def formula(person, period, parameters): + couple_award = ( + person.benunit("claimant_or_partner_esa_income", period) > 0 + ) & (is_payee_of_couple_award(person, period, "esa_income_reported")) + # The award on their own report alone, never another member's. + own_award = own_report_is_paid(person, period, "esa_income") + return where(person("is_claimant_or_partner", period), couple_award, own_award) diff --git a/policyengine_uk/variables/gov/dwp/is_on_income_support.py b/policyengine_uk/variables/gov/dwp/is_on_income_support.py new file mode 100644 index 0000000000..efc258cdfa --- /dev/null +++ b/policyengine_uk/variables/gov/dwp/is_on_income_support.py @@ -0,0 +1,48 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.gov.dwp._legacy_award_payee import ( + is_payee_of_couple_award, +) + + +class is_on_income_support(Variable): + value_type = bool + entity = Person + label = "on Income Support" + documentation = ( + 'Whether this person is in receipt of Income Support ("person on ' + 'income support", HB Regs 2006 reg 2(1); the council tax reduction ' + "schemes use the same definition). Income Support is calculated for " + "the claimant's family and needs the claimant's or partner's own " + "award (income_support_eligible). It is paid to the claimant, not " + "the partner: of the claimant and partner, only the payee is in " + "receipt, meaning the one who reports it, or the claimant where " + "neither does, and only while it is positive. Any other member of the " + "benefit unit, such as a non-dependent adult, claims in their own " + "right and is on it only if they report an award themselves while " + "Income Support is in payment (gov.dwp.income_support.active) and not " + "neutralised. A reform that removes Income Support must do one of " + "those for the status to follow: replacing its formula with zero " + "leaves another member's own report standing, because the model " + "calculates the award only for the claimant and partner." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/uksi/2006/213/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + ) + + def formula(person, period, parameters): + active = parameters(period).gov.dwp.income_support.active + removed = person.simulation.tax_benefit_system.get_variable( + "income_support" + ).is_neutralized + couple_award = (person.benunit("income_support", period) > 0) & ( + is_payee_of_couple_award(person, period, "income_support_reported") + ) + # Their own report, while Income Support is in payment and no reform + # removes it. The model calculates the award only for the claimant + # and partner, so it cannot gate another member's own claim. + own_award = ( + active & (not removed) & (person("income_support_reported", period) > 0) + ) + return where(person("is_claimant_or_partner", period), couple_award, own_award) diff --git a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py index 0d3a4cc2c2..389e51840d 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -39,7 +39,15 @@ def formula(benunit, period, parameters): if bi.interactions.include_in_means_tests: STEP_2_COMPONENTS.append("basic_income") income += add_for_members(benunit, period, STEP_2_COMPONENTS, members) - EXEMPT_BENEFITS = ["income_support", "esa_income", "jsa_income"] + # TCA 2002 s.7(2) and SI 2002/2008 reg 4: no income test while "the + # person, or either of the persons" claiming is entitled to Income + # Support, income-based JSA or income-related ESA. That is the claimant's + # or partner's award, not another member's. + EXEMPT_BENEFITS = [ + "income_support", + "claimant_or_partner_esa_income", + "claimant_or_partner_jsa_income", + ] on_exempt_benefits = ( add_for_members(benunit, period, EXEMPT_BENEFITS, members) > 0 ) diff --git a/policyengine_uk/variables/gov/dwp/would_claim_IS.py b/policyengine_uk/variables/gov/dwp/would_claim_IS.py index f2bed79a2e..4211ea3ce1 100644 --- a/policyengine_uk/variables/gov/dwp/would_claim_IS.py +++ b/policyengine_uk/variables/gov/dwp/would_claim_IS.py @@ -5,10 +5,22 @@ class would_claim_IS(Variable): value_type = bool entity = BenUnit label = "Would claim Income Support" - documentation = "Whether this family would claim Income Support if eligible" + documentation = ( + "Whether this family would claim Income Support if eligible: the " + "claimant or partner reports an award. Another member of the benefit " + "unit who reports Income Support, such as a non-dependent adult, " + "claims for themselves, not for this family." + ) definition_period = YEAR def formula(benunit, period, parameters): - reported_is = add(benunit, period, ["income_support_reported"]) > 0 + person = benunit.members + reported_is = ( + benunit.sum( + person("income_support_reported", period) + * person("is_claimant_or_partner", period) + ) + > 0 + ) claims_all_entitled_benefits = benunit("claims_all_entitled_benefits", period) return reported_is | claims_all_entitled_benefits diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py index 824e70fdbe..db0d6ba794 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/_legacy.py @@ -143,10 +143,14 @@ def normal_gross_income_non_dep_deduction( ctr.non_dep_deduction.amount.calc(0), ) full_time_student = is_full_time_student_non_dep(person, period) + # No deduction for a non-dependant "who is on" Income Support, income-based + # JSA or income-related ESA: an award payable to them (Default Scheme Sch + # para 2(3)-(4)). Of a couple, only the payee is on the couple's award, and + # another member of their benefit unit's award never counts. income_based_benefit = ( - (person.benunit("income_support", period) > 0) - | (person.benunit("jsa_income", period) > 0) - | (person.benunit("esa_income", period) > 0) + person("is_on_income_support", period) + | person("is_on_income_based_jsa", period) + | person("is_on_income_related_esa", period) | (person.benunit("pension_credit", period) > 0) ) has_uc = person.benunit("universal_credit", period) > 0 diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index 01fdaaedb1..282263cce5 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py @@ -42,11 +42,16 @@ def formula(benunit, period, parameters): members = person("is_claimant_or_partner", period) | person( "is_child_or_young_person_for_legacy_benefits", period ) + # Only the claimant's and partner's income-related awards: the scheme + # counts the applicant's and partner's income (Sch 1 para 11 above). A + # member outside the family who has an award of their own claims in their + # own right. Income Support already needs the claimant's or partner's own + # award (income_support_eligible). benunit_means_tested_benefits = [ "child_benefit", "income_support", - "jsa_income", - "esa_income", + "claimant_or_partner_jsa_income", + "claimant_or_partner_esa_income", "universal_credit", ] personal_benefits = [ diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py index 74df5864f3..cb9300b1c1 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_relevant_income_based_benefit.py @@ -5,7 +5,27 @@ class council_tax_reduction_relevant_income_based_benefit(Variable): value_type = bool entity = BenUnit label = "CTR claimant has an income-based passporting benefit" + documentation = ( + "Whether the applicant or partner is on Income Support, income-based " + "JSA or income-related ESA, which passports the working-age council " + "tax reduction schemes to their maximum reduction. Only the " + "applicant's and partner's awards count: another member of the " + "benefit unit, such as a non-dependent adult, who has an award of " + "their own claims in their own right." + ) definition_period = YEAR + reference = "https://www.legislation.gov.uk/uksi/2012/2886/schedule" def formula(benunit, period, parameters): - return add(benunit, period, ["income_support", "jsa_income", "esa_income"]) > 0 + return ( + add( + benunit, + period, + [ + "income_support", + "claimant_or_partner_jsa_income", + "claimant_or_partner_esa_income", + ], + ) + > 0 + ) diff --git a/policyengine_uk/variables/gov/social_security_scotland/is_scp_eligible.py b/policyengine_uk/variables/gov/social_security_scotland/is_scp_eligible.py index 4cb03989f1..c51be2694e 100644 --- a/policyengine_uk/variables/gov/social_security_scotland/is_scp_eligible.py +++ b/policyengine_uk/variables/gov/social_security_scotland/is_scp_eligible.py @@ -38,8 +38,17 @@ def formula(person, period, parameters): receives_income_support = ( benunit("income_support", period) > 0 ) & qb.income_support - receives_jsa_income = (benunit("jsa_income", period) > 0) & qb.jsa_income - receives_esa_income = (benunit("esa_income", period) > 0) & qb.esa_income + # SSI 2020/351 reg 18(e)-(f): the individual responsible for the child + # (through their own or their partner's Child Benefit, Pension Credit or + # Universal Credit award, or as a kinship carer: regs 9, 11 and 12) has + # been awarded the benefit. The model takes that individual to be the + # claimant or partner, not another member of the benefit unit. + receives_jsa_income = ( + benunit("claimant_or_partner_jsa_income", period) > 0 + ) & qb.jsa_income + receives_esa_income = ( + benunit("claimant_or_partner_esa_income", period) > 0 + ) & qb.esa_income receives_pension_credit = ( benunit("pension_credit", period) > 0 ) & qb.pension_credit