diff --git a/changelog.d/scottish-ctr-uc-young-person.fixed.md b/changelog.d/scottish-ctr-uc-young-person.fixed.md new file mode 100644 index 0000000000..c42c32be17 --- /dev/null +++ b/changelog.d/scottish-ctr-uc-young-person.fixed.md @@ -0,0 +1 @@ +- Defined the young person in Scotland's working-age Council Tax Reduction scheme as a Universal Credit qualifying young person from 2022-23 (Council Tax Reduction (Scotland) Regulations 2021 reg. 6), for lone-parent status and for the members whose income counts; other schemes keep the Child Benefit definition. diff --git a/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/working_age/young_person_follows_universal_credit.yaml b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/working_age/young_person_follows_universal_credit.yaml new file mode 100644 index 0000000000..ef16682b4b --- /dev/null +++ b/policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/working_age/young_person_follows_universal_credit.yaml @@ -0,0 +1,15 @@ +description: Scotland's working-age Council Tax Reduction scheme defines a young person as a Universal Credit qualifying young person if this is true, and as a Child Benefit qualifying young person otherwise. +values: + 2013-04-01: false + 2022-04-01: true +metadata: + unit: bool + period: year + label: Scottish working-age Council Tax Reduction young person follows Universal Credit + reference: + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 6(1) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/6 + - title: The Council Tax Reduction (Scotland) Regulations 2021 reg. 1 (in force 1 April 2022) + href: https://www.legislation.gov.uk/ssi/2021/249/regulation/1 + - title: The Council Tax Reduction (Scotland) Regulations 2012 reg. 4 as made (persons under pension age, to 31 March 2022) + href: https://www.legislation.gov.uk/ssi/2012/303/regulation/4/made 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 new file mode 100644 index 0000000000..d08a38d0e9 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml @@ -0,0 +1,353 @@ +# Each Council Tax Reduction scheme's own "young person". Scotland's +# working-age scheme, from 1 April 2022, uses the Universal Credit qualifying +# young person (SSI 2021/249 reg. 6(1); UC Regs 2013 reg. 5). England, Wales +# and Scotland's pension-age scheme use the Child Benefit qualifying young +# person (SI 2012/2885 reg. 2; WSI 2013/3029 reg. 2; SSI 2012/319 reg. 4), as +# did Scotland's working-age scheme until 31 March 2022 (SSI 2012/303 reg. 4). +# +# The family in every case: a claimant with a 19-year-old at school, on a +# course begun at 17. Child Benefit keeps a 19-year-old on such a course a +# qualifying young person until 20 (Child Benefit (General) Regulations 2006 +# reg. 3). Universal Credit stops on the 1 September after the 19th birthday +# (reg. 5(1)(b)), which the terminal-date input places. +# +# Weekly personal allowances (gov/dwp/housing_benefit/allowances): 2026-27 +# single under 25 £75.65, lone parent aged 18 or over £95.55, lone parent of +# pension age £256.00; 2021-22 lone parent aged 18 or over £74.70. Child +# Benefit for an eldest child is £27.05 a week in 2026-27. + +- name: Case 1, Scotland working age, past the Universal Credit terminal date, the 19-year-old is not a young person. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + employment_income: 2_000 + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + is_qualifying_young_person_for_child_benefit: [false, true] + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_young_person_for_legacy_benefits: [false, true] + is_child_or_young_person_for_council_tax_reduction: [false, false] + council_tax_reduction_pensioner: false + is_council_tax_reduction_lone_parent: false + # Single and under 25: 75.65 * 52. + council_tax_reduction_applicable_amount: 3_933.80 + # The student is outside the family, so their earnings do not count. The + # claimant's Child Benefit for them does: 27.05 * 52. + council_tax_reduction_applicable_income: 1_406.60 + +- name: Case 2, Scotland working age, before the Universal Credit terminal date, the 19-year-old is a young person. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + 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: 2_000 + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + is_qualifying_young_person_for_universal_credit: [false, true] + is_child_or_young_person_for_council_tax_reduction: [false, true] + 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 + +- name: Case 3, England keeps the Child Benefit young person past the Universal Credit terminal date. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + employment_income: 2_000 + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: ENGLAND + council_tax: 1_800 + output: + is_qualifying_young_person_for_universal_credit: [false, false] + 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 + +- name: Case 4, Wales keeps the Child Benefit young person past the Universal Credit terminal date. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + employment_income: 2_000 + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: WALES + council_tax: 1_800 + output: + 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 + +- name: Case 5, Scotland's pension-age scheme keeps the Child Benefit young person. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 70 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + council_tax_reduction_pensioner: true + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_young_person_for_council_tax_reduction: [false, true] + is_council_tax_reduction_lone_parent: true + # Lone parent of pension age: 256.00 * 52. + council_tax_reduction_applicable_amount: 13_312 + +- name: Case 6, Scotland's working-age scheme used the Child Benefit young person in 2021-22. + period: 2021 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + student: + age: 19 + is_claimant_or_partner: false + current_education: UPPER_SECONDARY + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + benunits: + benunit: + members: [claimant, student] + would_claim_uc: false + households: + household: + members: [claimant, student] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + is_qualifying_young_person_for_universal_credit: [false, false] + is_child_or_young_person_for_council_tax_reduction: [false, true] + is_council_tax_reduction_lone_parent: true + # Lone parent aged 18 or over in 2021-22: 74.70 * 52. + council_tax_reduction_applicable_amount: 3_884.40 + +- name: Case 7, Scotland working age, a child under 16 is in the family under either definition. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 22 + is_claimant_or_partner: true + child: + age: 10 + is_claimant_or_partner: false + benunits: + benunit: + members: [claimant, child] + would_claim_uc: false + households: + household: + members: [claimant, child] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + 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 + +- name: Case 8, Scotland working age, a claimant is never their own young person. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + claimant: + age: 17 + is_claimant_or_partner: true + current_education: UPPER_SECONDARY + benunits: + benunit: + members: [claimant] + would_claim_uc: false + households: + household: + members: [claimant] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + is_qualifying_young_person_for_universal_credit: true + is_child_or_young_person_for_council_tax_reduction: false + is_council_tax_reduction_lone_parent: false + # Single and under 25: 75.65 * 52. + council_tax_reduction_applicable_amount: 3_933.80 + +# The applicant's own income always counts (SSI 2021/249 reg. 36(1)). A +# household head who is liable for the council tax and applies alone is not +# the benefit unit's claimant, and the income mask reached them only as a +# Child Benefit young person. Past the Universal Credit terminal date they are +# not a young person under Scotland's working-age scheme, and their earnings +# must still count. +- name: Case 9, Scotland working age, a 19-year-old household head who applies alone keeps their earnings in the means test. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 19 + is_household_head: true + is_claimant_or_partner: false + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + employment_income: 10_000 + father: + age: 45 + is_household_head: false + is_claimant_or_partner: true + mother: + age: 46 + is_household_head: false + is_claimant_or_partner: true + benunits: + benunit: + members: [head, father, mother] + would_claim_uc: false + would_claim_child_benefit: false + households: + household: + members: [head, father, mother] + country: SCOTLAND + local_authority: CITY_OF_EDINBURGH + council_tax: 1_800 + output: + council_tax_reduction_head_applies_alone: true + is_council_tax_reduction_applicant_or_partner: [true, false, false] + is_qualifying_young_person_for_child_benefit: [true, false, false] + is_qualifying_young_person_for_universal_credit: [false, false, false] + is_child_or_young_person_for_council_tax_reduction: [false, false, false] + # The head's earnings of 10,000, below the income tax and National + # Insurance thresholds. No benefit is claimed. + council_tax_reduction_applicable_income: 10_000 + +- name: Case 10, the same family in England, where the head is a Child Benefit young person. + period: 2026 + absolute_error_margin: 0.01 + input: + people: + head: + age: 19 + is_household_head: true + is_claimant_or_partner: false + is_in_approved_training: true + age_started_or_accepted_current_education_or_training: 17 + is_before_universal_credit_qualifying_young_person_terminal_date: false + employment_income: 10_000 + father: + age: 45 + is_household_head: false + is_claimant_or_partner: true + mother: + age: 46 + is_household_head: false + is_claimant_or_partner: true + benunits: + benunit: + members: [head, father, mother] + would_claim_uc: false + would_claim_child_benefit: false + households: + household: + members: [head, father, mother] + country: ENGLAND + council_tax: 1_800 + output: + council_tax_reduction_head_applies_alone: true + is_council_tax_reduction_applicant_or_partner: [true, false, false] + is_child_or_young_person_for_council_tax_reduction: [true, false, false] + council_tax_reduction_applicable_income: 10_000 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 new file mode 100644 index 0000000000..c57d295b08 --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py @@ -0,0 +1,462 @@ +"""Each Council Tax Reduction scheme's own young person. + +Scotland's working-age scheme, from 1 April 2022, defines a young person as a +Universal Credit qualifying young person (Council Tax Reduction (Scotland) +Regulations 2021 reg. 6(1); UC Regs 2013 reg. 5). England, Wales and +Scotland's pension-age scheme use the Child Benefit qualifying young person +(SI 2012/2885 reg. 2; WSI 2013/3029 reg. 2; SSI 2012/319 reg. 4), as Scotland's +working-age scheme did until 31 March 2022 (SSI 2012/303 reg. 4). Every scheme +makes a child a person under 16, and no applicant is responsible for +themselves or their partner (SSI 2021/249 reg. 7(2)-(3)). + +Invariants: + +1. Scheme selection. The Council Tax Reduction flag is the Universal Credit + test (a child or qualifying young person who is not looked after and not + the claimant or partner) exactly where the family is in Scotland, is not a + pensioner for Council Tax Reduction and the year is 2022-23 or later. + Everywhere else it is the legacy (Child Benefit) flag. Checked over + every combination of the inputs the two definitions read, and over + generated benefit units of any shape. +2. No claimant or partner is their own benefit unit's child or young person, + under any scheme. +3. A member under 16 who is not looked after is in the family under every + scheme. +4. Where a member's Universal Credit and Child Benefit statuses agree, the + flag does not depend on the country, the pensioner test or the year. +5. A lone parent is a single claimant with at least one such member, and the + working-age applicable amount of a family with no premiums is the personal + allowance the table gives for a couple, a lone parent or a single person, + for 52 weeks. +6. The applicant's own earnings count under every scheme (SSI 2021/249 reg. + 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. + +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. +""" + +from itertools import product +from unittest.mock import patch + +import numpy as np +from hypothesis import HealthCheck, given, settings +from hypothesis import strategies as st + +from policyengine_uk import Simulation +from policyengine_uk.tax_benefit_system import system +from policyengine_uk.variables.household.demographic.highest_education import ( + EducationType, +) + +_PROPERTY_SYSTEM = system.clone() +EDUCATIONS = tuple(item.name for item in EducationType) +COUNTRIES = ("ENGLAND", "WALES", "SCOTLAND", "NORTHERN_IRELAND") +# 2021-22 is the last year of SSI 2012/303; SSI 2021/249 applies from 2022-23. +YEARS = (2021, 2022, 2026) +SETTINGS = settings( + max_examples=25, + deadline=None, + derandomize=True, + suppress_health_check=[HealthCheck.too_slow, HealthCheck.data_too_large], +) + +member = st.fixed_dictionaries( + { + "age": st.integers(0, 25), + "current_education": st.sampled_from(EDUCATIONS), + "is_in_approved_training": st.booleans(), + "receives_benefits_in_own_right": st.booleans(), + "is_looked_after_by_local_authority": st.booleans(), + "is_before_universal_credit_qualifying_young_person_terminal_date": ( + st.booleans() + ), + "age_started_or_accepted_current_education_or_training": st.sampled_from( + [17, 1000] + ), + } +) +claimant = st.fixed_dictionaries( + { + "age": st.integers(16, 70), + "current_education": st.sampled_from(EDUCATIONS), + "is_in_approved_training": st.booleans(), + "receives_benefits_in_own_right": st.just(False), + "is_looked_after_by_local_authority": st.just(False), + "is_before_universal_credit_qualifying_young_person_terminal_date": ( + st.booleans() + ), + "age_started_or_accepted_current_education_or_training": st.sampled_from( + [17, 1000] + ), + } +) + + +@st.composite +def families(draw): + claimants = draw(st.lists(claimant, min_size=1, max_size=2)) + others = draw(st.lists(member, max_size=4)) + return { + "country": draw(st.sampled_from(COUNTRIES)), + "pensioner": draw(st.booleans()), + "members": [dict(p, is_claimant_or_partner=True) for p in claimants] + + [dict(p, is_claimant_or_partner=False) for p in others], + } + + +def simulate(units, year): + situation = {"people": {}, "benunits": {}, "households": {}} + people = [] + for i, unit in enumerate(units): + names = [] + for j, inputs in enumerate(unit["members"]): + name = f"p{i}_{j}" + situation["people"][name] = {} + people.append(inputs) + names.append(name) + situation["benunits"][f"b{i}"] = {"members": names} + situation["households"][f"h{i}"] = {"members": names} + with patch( + "policyengine_uk.simulation.CountryTaxBenefitSystem", + side_effect=_PROPERTY_SYSTEM.clone, + ): + sim = Simulation(situation=situation) + for variable in people[0]: + sim.set_input(variable, year, [p[variable] for p in people]) + sim.set_input("country", year, [unit["country"] for unit in units]) + sim.set_input( + "council_tax_reduction_pensioner", year, [unit["pensioner"] for unit in units] + ) + sim.input_variables = list(people[0]) + [ + "country", + "council_tax_reduction_pensioner", + ] + sim.reset_calculations() + return sim + + +def per_person(units, key): + return np.array([unit[key] for unit in units for _ in unit["members"]]) + + +def follows_universal_credit(unit, year): + return unit["country"] == "SCOTLAND" and not unit["pensioner"] and year >= 2022 + + +def assert_scheme_selection(units, year): + sim = simulate(units, year) + people = [p for unit in units for p in unit["members"]] + claimant_or_partner = np.array([p["is_claimant_or_partner"] for p in people]) + looked_after = np.array([p["is_looked_after_by_local_authority"] for p in people]) + under_16 = np.array([p["age"] < 16 for p in people]) + flag = sim.calculate("is_child_or_young_person_for_council_tax_reduction", year) + legacy = sim.calculate("is_child_or_young_person_for_legacy_benefits", year) + universal_credit = ( + ( + under_16 + | sim.calculate("is_qualifying_young_person_for_universal_credit", year) + ) + & ~looked_after + & ~claimant_or_partner + ) + scottish = np.array( + [ + follows_universal_credit(unit, year) + for unit in units + for _ in unit["members"] + ] + ) + # Invariant 1. + np.testing.assert_array_equal(flag, np.where(scottish, universal_credit, legacy)) + # Invariant 2. + assert not (flag & claimant_or_partner).any() + # Invariant 3. + assert flag[under_16 & ~looked_after & ~claimant_or_partner].all() + return flag, legacy, scottish + + +@SETTINGS +@given(st.lists(families(), min_size=1, max_size=6), st.sampled_from(YEARS)) +def test_each_scheme_reads_its_own_young_person(units, year): + # Benefit units of any shape, where a claimant or partner can also be a + # qualifying young person. + assert_scheme_selection(units, year) + + +def test_scheme_selection_over_every_combination(): + # Ages 14 to 21, every education value, training, own-right benefits, + # looked-after status, the Universal Credit terminal date and the entry + # age, under each country and pensioner test, in each year: 2,048 + # subjects * 8 schemes * 3 years, each with a claimant aged 45, and again + # as the claimant of a benefit unit of their own. + adult = { + "age": 45, + "current_education": "NOT_IN_EDUCATION", + "is_in_approved_training": False, + "receives_benefits_in_own_right": False, + "is_looked_after_by_local_authority": False, + "is_before_universal_credit_qualifying_young_person_terminal_date": False, + "age_started_or_accepted_current_education_or_training": 1000, + "is_claimant_or_partner": True, + } + subjects = [ + { + "age": age, + "current_education": education, + "is_in_approved_training": training, + "receives_benefits_in_own_right": own_right, + "is_looked_after_by_local_authority": looked_after, + "is_before_universal_credit_qualifying_young_person_terminal_date": ( + terminal + ), + "age_started_or_accepted_current_education_or_training": entry_age, + "is_claimant_or_partner": False, + } + for age in range(14, 22) + for education in EDUCATIONS + for training in (False, True) + for own_right in (False, True) + for looked_after in (False, True) + for terminal in (False, True) + for entry_age in (17, 1000) + ] + units = [ + {"country": country, "pensioner": pensioner, "members": [adult, subject]} + for subject in subjects + for country in COUNTRIES + for pensioner in (False, True) + ] + # The same subjects as the claimant of a benefit unit of their own: never + # their own child or young person, under any scheme (invariant 2). + alone = [ + { + "country": country, + "pensioner": pensioner, + "members": [dict(subject, is_claimant_or_partner=True)], + } + for subject in subjects + for country in COUNTRIES + for pensioner in (False, True) + ] + for year in YEARS: + flag, legacy, scottish = assert_scheme_selection(units, year) + differs = flag != legacy + # The Universal Credit definition applies nowhere before 2022-23, and + # only in Scotland's working-age scheme after it. + assert not differs[~scottish].any() + # Not vacuous: from 2022-23 the two definitions do part there. + assert differs.any() == (year >= 2022) + own_flag, _, _ = assert_scheme_selection(alone, year) + assert not own_flag.any() + # Not vacuous: some of those lone claimants are qualifying young + # persons by status, in Scotland's working-age scheme too. + sim = simulate(alone, year) + status = sim.calculate("is_qualifying_young_person_for_universal_credit", year) + scottish_alone = np.array( + [follows_universal_credit(unit, year) for unit in alone] + ) + assert status.any() + assert (status & scottish_alone).any() == (year >= 2022) + + +@SETTINGS +@given(st.lists(families(), min_size=1, max_size=4)) +def test_schemes_differ_only_where_the_two_definitions_differ(units): + # Invariant 4: the same members under every country, pensioner test and + # year. The flags can differ only for a member whose Universal Credit and + # Child Benefit qualifying young person statuses differ in that year. + variants = [ + dict(unit, country=country, pensioner=pensioner) + for unit in units + for country in COUNTRIES + for pensioner in (False, True) + ] + size = len(COUNTRIES) * 2 + for year in YEARS: + sim = simulate(variants, year) + flag = sim.calculate("is_child_or_young_person_for_council_tax_reduction", year) + agree = sim.calculate( + "is_qualifying_young_person_for_universal_credit", year + ) == sim.calculate("is_qualifying_young_person_for_child_benefit", year) + offset = 0 + for unit in units: + n = len(unit["members"]) + block = flag[offset : offset + size * n].reshape(size, n) + same = agree[offset : offset + n] + assert (block[:, same] == block[0, same]).all() + offset += size * n + + +@SETTINGS +@given(st.lists(families(), min_size=1, max_size=6), st.sampled_from(YEARS)) +def test_lone_parent_and_working_age_personal_allowance(units, year): + # Invariant 5, from the allowance table alone. + units = [dict(unit, pensioner=False) for unit in units] + sim = simulate(units, year) + allowances = sim.tax_benefit_system.parameters( + f"{year}-06-01" + ).gov.dwp.housing_benefit.allowances + flag = sim.calculate("is_child_or_young_person_for_council_tax_reduction", year) + lone_parent = sim.calculate("is_council_tax_reduction_lone_parent", year) + couple = sim.calculate("is_couple", year) + amount = sim.calculate("council_tax_reduction_applicable_amount", year) + premiums = sim.calculate("working_age_benefits_premiums", year) + offset = 0 + for i, unit in enumerate(units): + n = len(unit["members"]) + claimant_ages = [ + p["age"] for p in unit["members"] if p["is_claimant_or_partner"] + ] + responsible = bool(flag[offset : offset + n].any()) + offset += n + assert couple[i] == (len(claimant_ages) == 2) + assert lone_parent[i] == (len(claimant_ages) == 1 and responsible) + eldest = max(claimant_ages) + if len(claimant_ages) == 2: + weekly = ( + allowances.couple.younger + if eldest < allowances.age_threshold.younger + else allowances.couple.older + ) + elif responsible: + weekly = ( + allowances.lone_parent.younger + if eldest < allowances.age_threshold.younger + else allowances.lone_parent.older + ) + else: + weekly = ( + allowances.single.younger + if eldest < allowances.age_threshold.older + else allowances.single.older + ) + np.testing.assert_allclose(amount[i] - premiums[i], weekly * 52, rtol=1e-6) + + +def test_scottish_working_age_differs_from_child_benefit_at_19(): + # Not vacuous: past the Universal Credit terminal date a 19-year-old at + # school is a Child Benefit young person and not a Universal Credit one, + # so the Scottish working-age flag is a different rule from the legacy + # flag. Before the date the two agree. + base = { + "current_education": "UPPER_SECONDARY", + "is_in_approved_training": False, + "receives_benefits_in_own_right": False, + "is_looked_after_by_local_authority": False, + "age_started_or_accepted_current_education_or_training": 17, + } + adult = dict( + base, + age=45, + current_education="NOT_IN_EDUCATION", + is_before_universal_credit_qualifying_young_person_terminal_date=False, + is_claimant_or_partner=True, + ) + past = dict( + base, + age=19, + is_before_universal_credit_qualifying_young_person_terminal_date=False, + is_claimant_or_partner=False, + ) + before = dict( + past, is_before_universal_credit_qualifying_young_person_terminal_date=True + ) + units = [ + {"country": country, "pensioner": pensioner, "members": [adult, young]} + for country in COUNTRIES + for pensioner in (False, True) + for young in (past, before) + ] + sim = simulate(units, 2026) + flag = sim.calculate("is_child_or_young_person_for_council_tax_reduction", 2026) + young = flag[1::2] + expected = [ + not ( + unit["country"] == "SCOTLAND" + and not unit["pensioner"] + and not unit["members"][1][ + "is_before_universal_credit_qualifying_young_person_terminal_date" + ] + ) + for unit in units + ] + np.testing.assert_array_equal(young, expected) + assert not all(expected) and any(expected) + + +def test_applicants_own_earnings_count_under_every_scheme(): + # Invariant 6. Parents aged 45 and 46 are the benefit unit's claimant and + # partner; with them lives a member aged 18 or 19 at school or in approved + # training. Either that member heads the household, is liable for the + # council tax and applies alone, or the father heads it and the member is + # only a possible young person of the family. No benefit is claimed, and + # the earnings are below the income tax and National Insurance thresholds, + # so the applicable income is the earnings that count. + year, earnings = 2026, 9_000 + cases = list( + product( + ("ENGLAND", "WALES", "SCOTLAND"), + (18, 19), + (False, True), # approved training, or school + (False, True), # before the Universal Credit terminal date + (False, True), # the member heads the household + ) + ) + situation = {"people": {}, "benunits": {}, "households": {}} + for i, (country, age, training, before, heads) in enumerate(cases): + situation["people"][f"member{i}"] = { + "age": {year: age}, + "is_household_head": {year: heads}, + "is_claimant_or_partner": {year: False}, + "current_education": { + year: "NOT_IN_EDUCATION" if training else "UPPER_SECONDARY" + }, + "is_in_approved_training": {year: training}, + "age_started_or_accepted_current_education_or_training": {year: 17}, + "is_before_universal_credit_qualifying_young_person_terminal_date": { + year: before + }, + "employment_income": {year: earnings}, + } + for parent, parent_age in (("father", 45), ("mother", 46)): + situation["people"][f"{parent}{i}"] = { + "age": {year: parent_age}, + "is_household_head": {year: parent == "father" and not heads}, + "is_claimant_or_partner": {year: True}, + } + names = [f"member{i}", f"father{i}", f"mother{i}"] + situation["benunits"][f"b{i}"] = { + "members": names, + "would_claim_uc": {year: False}, + "would_claim_child_benefit": {year: False}, + } + situation["households"][f"h{i}"] = { + "members": names, + "country": {year: country}, + } + with patch( + "policyengine_uk.simulation.CountryTaxBenefitSystem", + side_effect=_PROPERTY_SYSTEM.clone, + ): + sim = Simulation(situation=situation) + applicant = sim.calculate("is_council_tax_reduction_applicant_or_partner", year) + heads = np.array([case[4] for case in cases]) + # Where the member heads the household they are the applicant, alone. + np.testing.assert_array_equal(applicant[0::3], heads) + np.testing.assert_array_equal(applicant[1::3], ~heads) + young_person = sim.calculate( + "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() + assert (~heads & ~young_person).any() and (~heads & young_person).any() diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py index 9b4f657c29..a63c5c4032 100644 --- a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_amount.py @@ -14,6 +14,7 @@ class council_tax_reduction_applicable_amount(Variable): "https://www.legislation.gov.uk/wsi/2013/3029/schedule/7", "https://www.legislation.gov.uk/ssi/2012/319/schedule/1", "https://www.legislation.gov.uk/ssi/2021/249/schedule/1", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/6", ) def formula(benunit, period, parameters): @@ -29,9 +30,12 @@ def formula(benunit, period, parameters): u_25 = eldest_age < older_age_threshold o_25 = (eldest_age >= older_age_threshold) & ~pensioner o_18 = (eldest_age >= younger_age_threshold) & ~pensioner - single = benunit("is_single_person", period) + # Each scheme's own young person decides who is a lone parent: the + # Universal Credit definition in Scotland's working-age scheme (SSI + # 2021/249 regs 4 and 6), the Child Benefit definition elsewhere. couple = benunit("is_couple", period) - lone_parent = benunit("is_lone_parent", period) + lone_parent = benunit("is_council_tax_reduction_lone_parent", period) + single = ~couple & ~lone_parent single_personal_allowance = ( u_25 * p.single.younger + o_25 * p.single.older + pensioner * p.single.aged ) 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 282263cce5..55575e1987 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 @@ -39,8 +39,19 @@ def formula(benunit, period, parameters): # Regs 2012 Sch 1 para 11); dropping dependants' own income is a follow-up. # Anyone else in the benefit unit does not count. person = benunit.members - members = person("is_claimant_or_partner", period) | person( - "is_child_or_young_person_for_legacy_benefits", period + # 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( + "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 ) # 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 diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py new file mode 100644 index 0000000000..476e395c44 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py @@ -0,0 +1,59 @@ +from policyengine_uk.model_api import * +from policyengine_uk.variables.household.demographic.country import Country + + +class is_child_or_young_person_for_council_tax_reduction(Variable): + """Child or young person in the applicant's family for Council Tax Reduction. + + Every scheme defines a child as a person under 16. They differ on the + young person: + + - Scotland's working-age scheme, from 1 April 2022, uses the Universal + Credit qualifying young person (Council Tax Reduction (Scotland) + Regulations 2021 reg. 6(1); UC Regs 2013 reg. 5). The applicant is + responsible for one who normally lives with them, never for one they + live with as a couple, and no one is responsible for one placed with a + foster carer (reg. 7). + - England, Wales and Scotland's pension-age scheme use the Child Benefit + qualifying young person (SI 2012/2885 reg. 2; WSI 2013/3029 reg. 2; SSI + 2012/319 reg. 4), as Housing Benefit does. + + The scheme follows the family's own pensioner status + (council_tax_reduction_pensioner). + + Both branches leave out every child or young person looked after by a + local authority, as the Universal Credit and legacy flags do. That is + broader than Scotland's working-age rule, which names only a child or + young person placed with a foster carer under the Looked After Children + (Scotland) Regulations 2009 (reg. 7(8)). + """ + + value_type = bool + entity = Person + label = "Child or young person for Council Tax Reduction" + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ssi/2021/249/regulation/4", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/6", + "https://www.legislation.gov.uk/ssi/2021/249/regulation/7", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/4", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/2", + ) + + def formula(person, period, parameters): + p = parameters( + period + ).gov.local_authorities.scotland.council_tax_reduction.working_age + scottish_working_age = ( + (person.household("country", period) == Country.SCOTLAND) + & ~person.benunit("council_tax_reduction_pensioner", period) + & p.young_person_follows_universal_credit + ) + # SSI 2021/249 regs 4, 6 and 7. The Universal Credit flag already + # leaves out a child or young person looked after by a local authority. + universal_credit = person( + "is_child_or_qualifying_young_person_for_universal_credit", period + ) & ~person("is_claimant_or_partner", period) + child_benefit = person("is_child_or_young_person_for_legacy_benefits", period) + return where(scottish_working_age, universal_credit, child_benefit) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_council_tax_reduction_lone_parent.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_council_tax_reduction_lone_parent.py new file mode 100644 index 0000000000..bd8582320f --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_council_tax_reduction_lone_parent.py @@ -0,0 +1,28 @@ +from policyengine_uk.model_api import * + + +class is_council_tax_reduction_lone_parent(Variable): + value_type = bool + entity = BenUnit + label = "Lone parent for Council Tax Reduction" + documentation = ( + "A person who has no partner and who is responsible for and a member " + "of the same household as a child or young person, under the young " + "person definition of the Council Tax Reduction scheme that applies " + "to the family: the Universal Credit qualifying young person in " + "Scotland's working-age scheme from 1 April 2022, and the Child " + "Benefit qualifying young person elsewhere." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ssi/2021/249/regulation/4", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/2", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/2", + "https://www.legislation.gov.uk/wsi/2013/3029/regulation/2", + ) + + def formula(benunit, period, parameters): + return benunit("is_single", period) & benunit( + "is_responsible_for_child_or_young_person_for_council_tax_reduction", + period, + ) diff --git a/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_responsible_for_child_or_young_person_for_council_tax_reduction.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_responsible_for_child_or_young_person_for_council_tax_reduction.py new file mode 100644 index 0000000000..b262b92fd8 --- /dev/null +++ b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_responsible_for_child_or_young_person_for_council_tax_reduction.py @@ -0,0 +1,27 @@ +from policyengine_uk.model_api import * + + +class is_responsible_for_child_or_young_person_for_council_tax_reduction(Variable): + value_type = bool + entity = BenUnit + label = "Responsible for a child or young person for Council Tax Reduction" + documentation = ( + "Whether the applicant or partner is responsible for a child or young " + "person in the same household, under the young person definition of " + "the Council Tax Reduction scheme that applies to the family " + "(is_child_or_young_person_for_council_tax_reduction). Benefit-unit " + "membership stands in for 'normally lives with'." + ) + definition_period = YEAR + reference = ( + "https://www.legislation.gov.uk/ssi/2021/249/regulation/7", + "https://www.legislation.gov.uk/ssi/2012/319/regulation/10", + "https://www.legislation.gov.uk/uksi/2012/2885/regulation/7", + ) + + def formula(benunit, period, parameters): + return benunit.any( + benunit.members( + "is_child_or_young_person_for_council_tax_reduction", period + ) + )