diff --git a/changelog.d/means-tests-dependants-income.fixed.md b/changelog.d/means-tests-dependants-income.fixed.md new file mode 100644 index 0000000000..30f5b270a6 --- /dev/null +++ b/changelog.d/means-tests-dependants-income.fixed.md @@ -0,0 +1 @@ +Housing Benefit, Income Support, tax credit and Council Tax Reduction means tests no longer count a child's or young person's own income, tax or personal benefits as the claimant's (HB Regs 2006 reg 25(3), IS Regs 1987 reg 23(2), Tax Credits (Definition and Calculation of Income) Regs 2002 reg 3(1), the Council Tax Reduction applicant-and-partner rules). diff --git a/policyengine_uk/tests/policy/baseline/finance/benefit/means_tests_young_person_income.yaml b/policyengine_uk/tests/policy/baseline/finance/benefit/means_tests_young_person_income.yaml new file mode 100644 index 0000000000..87f5a99ce4 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/finance/benefit/means_tests_young_person_income.yaml @@ -0,0 +1,111 @@ +# A child's or young person's own income is never the claimant's: HB Regs +# 2006 reg 25(3), HB (SPC) Regs 2006 reg 23(1), IS Regs 1987 reg 23(2), Tax +# Credits (Definition and Calculation of Income) Regs 2002 reg 3(1), and the +# applicant-and-partner rules of every Council Tax Reduction scheme (SSI +# 2021/249 reg 36(2) says it in terms). +# +# The family in each case: a lone parent and a 17-year-old at school who earns +# £8,000, below the income tax and National Insurance thresholds. Child +# Benefit is not claimed, so it adds nothing to the income totals. + +- name: Case 1, a young person's earnings are not the parent's income in any of the four means tests. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + is_claimant_or_partner: true + student: + age: 17 + current_education: UPPER_SECONDARY + is_claimant_or_partner: false + employment_income: 8_000 + benunits: + benunit: + members: [parent, student] + would_claim_child_benefit: false + would_claim_uc: false + households: + household: + members: [parent, student] + country: ENGLAND + rent: 6_000 + council_tax: 1_800 + output: + # The student is still the family's young person. + is_child_or_young_person_for_legacy_benefits: [false, true] + housing_benefit_applicable_income: 0 + income_support_applicable_income: 0 + tax_credits_applicable_income: 0 + council_tax_reduction_applicable_income: 0 + +- name: Case 2, the parent's own earnings still count. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + parent: + age: 40 + is_parent: true + is_claimant_or_partner: true + employment_income: 5_000 + student: + age: 17 + current_education: UPPER_SECONDARY + is_claimant_or_partner: false + employment_income: 8_000 + benunits: + benunit: + members: [parent, student] + would_claim_child_benefit: false + would_claim_uc: false + households: + household: + members: [parent, student] + country: ENGLAND + rent: 6_000 + council_tax: 1_800 + output: + # £5,000 less the lone parent earnings disregard of £25 a week. + housing_benefit_applicable_income: 3_700 + # £5,000 less the lone parent disregard of £20 a week. + income_support_applicable_income: 3_960 + tax_credits_applicable_income: 5_000 + council_tax_reduction_applicable_income: 5_000 + +- name: Case 3, a pension-age Housing Benefit claimant's grandchild's earnings are not the claimant's. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + grandparent: + age: 70 + is_parent: true + is_claimant_or_partner: true + # Above the Pension Credit guarantee, so no passport zeroes the income. + private_pension_income: 20_000 + student: + age: 17 + current_education: UPPER_SECONDARY + is_claimant_or_partner: false + employment_income: 8_000 + benunits: + benunit: + members: [grandparent, student] + would_claim_child_benefit: false + would_claim_uc: false + households: + household: + members: [grandparent, student] + country: ENGLAND + rent: 6_000 + council_tax: 1_800 + output: + housing_benefit_pension_age_regulations_apply: true + pension_credit: 0 + # The pension of £20,000 less income tax of 20% of (20,000 - 12,570), + # £1,486; not the student's £8,000. + housing_benefit_applicable_income: 18_514 + council_tax_reduction_applicable_income: 18_514 diff --git a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml index d08a38d0e9..1abdbeba87 100644 --- a/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml @@ -85,12 +85,9 @@ is_council_tax_reduction_lone_parent: true # Lone parent aged 18 or over: 95.55 * 52. council_tax_reduction_applicable_amount: 4_968.60 - # Child Benefit 1,406.60 plus the young person's earnings of 2,000, which - # are below the income tax and National Insurance thresholds. Counting a - # young person's own earnings is an older approximation of the model's, - # not the law (reg. 36(2) leaves them out); this pins that the member is - # in the family, not a statutory income figure. - council_tax_reduction_applicable_income: 3_406.60 + # Child Benefit, 27.05 * 52. The young person's own earnings of 2,000 are + # not the applicant's (SSI 2021/249 reg. 36(2)). + council_tax_reduction_applicable_income: 1_406.60 - name: Case 3, England keeps the Child Benefit young person past the Universal Credit terminal date. period: 2026 @@ -121,7 +118,10 @@ is_child_or_young_person_for_council_tax_reduction: [false, true] is_council_tax_reduction_lone_parent: true council_tax_reduction_applicable_amount: 4_968.60 - council_tax_reduction_applicable_income: 3_406.60 + # Child Benefit only: the young person's own earnings are not the + # applicant's (SI 2012/2885 Sch 1 para 11 counts the applicant and + # partner). + council_tax_reduction_applicable_income: 1_406.60 - name: Case 4, Wales keeps the Child Benefit young person past the Universal Credit terminal date. period: 2026 diff --git a/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py b/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py index c57d295b08..1bf2efbf8f 100644 --- a/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py +++ b/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py @@ -32,8 +32,8 @@ 36(1)). A household head aged 18 or 19 who applies alone was reached by the income mask only as a Child Benefit young person; they stay in the means test where Scotland's working-age scheme does not read them as a - young person. A member who is not the applicant counts only as that - scheme's young person. + young person. A member who is not the applicant never counts: a young + person's income is not the applicant's. The pensioner test and the claimant and partner are supplied, so these tests exercise the young person rule alone; the YAML cases use the derived tests. @@ -452,11 +452,10 @@ def test_applicants_own_earnings_count_under_every_scheme(): "is_child_or_young_person_for_council_tax_reduction", year )[0::3] income = sim.calculate("council_tax_reduction_applicable_income", year) - np.testing.assert_allclose(income, earnings * (heads | young_person), atol=0.01) - # The applicant's earnings count in every case. - np.testing.assert_allclose(income[heads], earnings, atol=0.01) - # Not vacuous: some applicants are not young persons under their scheme, - # and a member who is neither does not count. - assert (heads & ~young_person).any() - assert (income[~heads & ~young_person] == 0).all() + np.testing.assert_allclose(income, earnings * heads, atol=0.01) + # The applicant's earnings count in every case, whether or not their + # scheme reads them as a young person. + assert (heads & ~young_person).any() and (heads & young_person).any() + # Not vacuous: a non-applicant who is the scheme's young person does not + # count, nor one who is not. assert (~heads & ~young_person).any() and (~heads & young_person).any() diff --git a/policyengine_uk/tests/test_means_test_income_properties.py b/policyengine_uk/tests/test_means_test_income_properties.py index 49a9088ef2..9f1a3a7e4a 100644 --- a/policyengine_uk/tests/test_means_test_income_properties.py +++ b/policyengine_uk/tests/test_means_test_income_properties.py @@ -1,10 +1,17 @@ -"""A means test never counts the income of someone outside its family. +"""A means test counts the claimant's and partner's income, no one else's. -Invariant: for any benefit unit, giving income to a member who is neither the -claimant, the partner, nor the programme's own child or young person (for -example an 18- or 19-year-old presumed the claimant's child but not in -education, or a third adult) never changes a means test's income. The UC, HB, -IS, tax credit, CTR and Pension Credit income totals are covered. +Invariants: + +1. For any benefit unit, giving income to a member who is neither the + claimant, the partner, nor the programme's own child or young person (for + example an 18- or 19-year-old presumed the claimant's child but not in + education, or a third adult) never changes a means test's income. The UC, + HB, IS, tax credit, CTR and Pension Credit income totals are covered. +2. Giving income to the family's own child or young person never changes it + either (HB Regs 2006 reg 25(3); IS Regs 1987 reg 23(2); Tax Credits + (Definition and Calculation of Income) Regs 2002 reg 3(1); SSI 2021/249 reg + 36(2) and the applicant-and-partner rules of the other Council Tax + Reduction schemes). Each example builds families twice in one simulation: once with the other member's income and once without, in separate households and benefit units. @@ -132,3 +139,81 @@ def test_income_of_non_claimants_never_counts(units): would_claim_uc, units[i], ) + + +@st.composite +def families_with_a_dependant(draw): + # A head, an optional partner, and the family's own child or young + # person: a child under 16, or a 16- to 19-year-old at school on a course + # begun at 17, at least 16 years younger than the head and with the head + # flagged a parent, so every programme counts them as the family's. + dependant_age = draw(st.integers(10, 19)) + head_age = draw(st.integers(dependant_age + 16, 60)) + family = [ + { + "age": head_age, + "is_parent": True, + "employment_income": draw(money), + } + ] + if draw(st.booleans()): + family.append( + { + "age": draw(st.integers(dependant_age + 16, 60)), + "is_parent": True, + "employment_income": draw(money), + } + ) + family.append( + { + "age": dependant_age, + "current_education": "UPPER_SECONDARY", + "age_started_or_accepted_current_education_or_training": 17, + "is_before_universal_credit_qualifying_young_person_terminal_date": True, + "employment_income": draw(st.floats(1, 50_000)), + "private_pension_income": draw(money), + "savings_interest_income": draw(money), + } + ) + return family + + +@settings( + max_examples=15, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) +@given(st.lists(families_with_a_dependant(), min_size=1, max_size=6)) +def test_income_of_children_and_young_persons_never_counts(units): + # Invariant 2: the same families with the dependant's income removed. + without = [ + family[:-1] + + [ + { + **family[-1], + "employment_income": 0, + "private_pension_income": 0, + "savings_interest_income": 0, + } + ] + for family in units + ] + for would_claim_uc in (None, False): + sim = Simulation(situation=situation(units + without, would_claim_uc)) + offsets = np.cumsum([0] + [len(f) for f in units]) + n = len(units) + # Not vacuous: the dependant is every legacy programme's own child or + # young person. + for variable in FAMILY_FLAGS[1:]: + flags = sim.calculate(variable, YEAR) + for i in range(n): + assert flags[offsets[i + 1] - 1], (variable, units[i]) + for variable in INCOME_TESTS: + values = sim.calculate(variable, YEAR) + for i in range(n): + assert abs(values[i] - values[n + i]) < 0.01, ( + variable, + would_claim_uc, + units[i], + ) 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 b97d9ac316..ba52f588f4 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 @@ -31,14 +31,13 @@ class housing_benefit_applicable_income(Variable): unit = GBP def formula(benunit, period, parameters): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (HB Regs 2006 reg 25); dropping dependants' - # own income is a follow-up. Anyone else in the benefit unit does not count. + # Members whose income counts: the claimant and partner (SSCBA 1992 + # s.136(1); HB Regs 2006 reg 25(1), HB (SPC) Regs 2006 reg 23(1)). "The + # income and capital of a child or young person shall not be treated as + # the income and capital of the claimant" (HB Regs 2006 reg 25(3)). + # Anyone else in the benefit unit does not count either. person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_young_person_for_legacy_benefits", period - ) + members = person("is_claimant_or_partner", period) pension_age_regulations = benunit( "housing_benefit_pension_age_regulations_apply", period ) diff --git a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py index eea502562e..52c5e0ddba 100644 --- a/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/income_support_applicable_income.py @@ -10,14 +10,13 @@ class income_support_applicable_income(Variable): reference = "https://www.legislation.gov.uk/uksi/1987/1967/schedule/8" def formula(benunit, period, parameters): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (IS Regs 1987 reg 23); dropping dependants' - # own income is a follow-up. Anyone else in the benefit unit does not count. + # Members whose income counts: the claimant and partner (IS Regs 1987 + # reg 23(1)). The income of a child or young person in the claimant's + # family "shall not be treated as the income or capital of the + # claimant" (reg 23(2)). Anyone else in the benefit unit does not count + # either. person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_young_person_for_legacy_benefits", period - ) + members = person("is_claimant_or_partner", period) IS = parameters(period).gov.dwp.income_support INCOME_COMPONENTS = [ "employment_income", 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 389e51840d..8424884687 100644 --- a/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py +++ b/policyengine_uk/variables/gov/dwp/tax_credits_applicable_income.py @@ -12,14 +12,12 @@ class tax_credits_applicable_income(Variable): ) def formula(benunit, period, parameters): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (TCA 2002 s.7); dropping dependants' own - # income is a follow-up. Anyone else in the benefit unit does not count. + # Members whose income counts: the claimant or, on a joint claim, the + # claimants (TCA 2002 s.7; Tax Credits (Definition and Calculation of + # Income) Regs 2002 reg 3(1)). A child's or qualifying young person's + # own income does not count, nor anyone else's in the benefit unit. person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_qualifying_young_person_for_child_tax_credit", period - ) + members = person("is_claimant_or_partner", period) TC = parameters(period).gov.dwp.tax_credits STEP_1_COMPONENTS = [ "private_pension_income", 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 55575e1987..7c64c59722 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 @@ -33,26 +33,22 @@ class council_tax_reduction_applicable_income(Variable): ] def formula(benunit, period, parameters): - # Members whose income counts: the claimant and partner and, as the model did - # before, the programme's own children or young persons. The regulations count - # only the claimant's and partner's (CTR (Prescribed Requirements) (England) - # Regs 2012 Sch 1 para 11); dropping dependants' own income is a follow-up. - # Anyone else in the benefit unit does not count. + # Members whose income counts: the applicant and partner (CTR + # (Prescribed Requirements) (England) Regs 2012 Sch 1 para 11; WSI + # 2013/3029 Sch 1 para 5; SSI 2012/319 reg 21; SSI 2021/249 reg 36(1)). + # "The income and capital of a child or young person is not to be + # treated as the income and capital of the applicant" (SSI 2021/249 reg + # 36(2)); the other schemes count the applicant's and partner's alone. person = benunit.members # A household head who applies alone - # (council_tax_reduction_head_applies_alone) is not the benefit unit's - # claimant, so this mask has reached them only as a Child Benefit young - # person. The applicant's own income always counts (SSI 2021/249 reg - # 36(1)), so they stay counted where Scotland's working-age scheme no - # longer reads them as a young person. - applicant_counted_as_young_person = person( + # (council_tax_reduction_head_applies_alone) is the applicant but not + # the benefit unit's claimant. The model has counted such a head only + # when they are a Child Benefit young person; that is kept, as their + # own income. Counting every such applicant is #2078's change. + head_applicant_counted = person( "is_council_tax_reduction_applicant_or_partner", period ) & person("is_child_or_young_person_for_legacy_benefits", period) - members = ( - person("is_claimant_or_partner", period) - | person("is_child_or_young_person_for_council_tax_reduction", period) - | applicant_counted_as_young_person - ) + members = person("is_claimant_or_partner", period) | head_applicant_counted # 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