From a7d0127f95a5147d985991c767b900362745ad82 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 19:50:26 -0400 Subject: [PATCH 1/4] Use the Universal Credit young person in Scotland's working-age CTR The Council Tax Reduction (Scotland) Regulations 2021 reg. 6(1), in force 1 April 2022, define a young person as a qualifying young person under UC Regs 2013 reg. 5. The model used the Child Benefit definition for every scheme: is_lone_parent in the applicable amount and the legacy flag for the members whose income counts. Add is_child_or_young_person_for_council_tax_reduction (the Universal Credit test, minus the claimant and partner, in Scotland's working-age scheme from 2022-23; the legacy flag elsewhere), with the responsibility and lone-parent flags built on it, and read them in council_tax_reduction_applicable_amount and council_tax_reduction_applicable_income. England, Wales, Scotland's pension-age scheme (SSI 2012/319 reg. 4) and Scotland before 2022-23 (SSI 2012/303 reg. 4) keep the Child Benefit definition. Fixes #2286 Co-Authored-By: Claude Opus 5.5 --- .../scottish-ctr-uc-young-person.fixed.md | 1 + ...young_person_follows_universal_credit.yaml | 15 + .../council_tax_reduction_young_person.yaml | 265 ++++++++++++++++ ...l_tax_reduction_young_person_properties.py | 294 ++++++++++++++++++ ...council_tax_reduction_applicable_amount.py | 8 +- ...council_tax_reduction_applicable_income.py | 2 +- ..._young_person_for_council_tax_reduction.py | 53 ++++ .../is_council_tax_reduction_lone_parent.py | 28 ++ ..._young_person_for_council_tax_reduction.py | 27 ++ 9 files changed, 690 insertions(+), 3 deletions(-) create mode 100644 changelog.d/scottish-ctr-uc-young-person.fixed.md create mode 100644 policyengine_uk/parameters/gov/local_authorities/scotland/council_tax_reduction/working_age/young_person_follows_universal_credit.yaml create mode 100644 policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml create mode 100644 policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_child_or_young_person_for_council_tax_reduction.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_council_tax_reduction_lone_parent.py create mode 100644 policyengine_uk/variables/gov/local_authorities/council_tax_reduction/is_responsible_for_child_or_young_person_for_council_tax_reduction.py 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..c4c2f1f889 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/local_authorities/council_tax_reduction/council_tax_reduction_young_person.yaml @@ -0,0 +1,265 @@ +# 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. + 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 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..0f7b7f34da --- /dev/null +++ b/policyengine_uk/tests/test_council_tax_reduction_young_person_properties.py @@ -0,0 +1,294 @@ +"""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. +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. + +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 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 + + +@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): + 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() + + +@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: the two definitions do differ, in both directions, so the + # Scottish working-age flag is a different rule from the legacy flag. + 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) 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..fef871ff88 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 @@ -40,7 +40,7 @@ def formula(benunit, period, parameters): # 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 + "is_child_or_young_person_for_council_tax_reduction", 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 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..034bab6abf --- /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,53 @@ +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). + """ + + 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 + ) + ) From 76ed60e50a9a003d3f4513c8fc42e0a841555c14 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 20:01:19 -0400 Subject: [PATCH 2/4] Check the Scottish young person rule over every combination of inputs Mutation testing showed the generated benefit units rarely draw a Scottish working-age 19-year-old past the Universal Credit terminal date, so the scheme-selection property passed with the Scottish branch removed. Add an exhaustive grid (ages 14 to 21, every education value and flag, each country and pensioner test, 2021, 2022 and 2026) that also asserts the two definitions part only there. Co-Authored-By: Claude Opus 5.5 --- ...l_tax_reduction_young_person_properties.py | 69 +++++++++++++++++-- 1 file changed, 65 insertions(+), 4 deletions(-) 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 0f7b7f34da..fb5c51ede4 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 @@ -15,7 +15,9 @@ 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. + 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 @@ -138,9 +140,7 @@ def follows_universal_credit(unit, year): return unit["country"] == "SCOTLAND" and not unit["pensioner"] and year >= 2022 -@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): +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]) @@ -169,6 +169,67 @@ def test_each_scheme_reads_its_own_young_person(units, year): 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. + 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) + ] + 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) @SETTINGS From 9dc3e3cb196a7a344d88563bfec5da866eccb84f Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 20:22:05 -0400 Subject: [PATCH 3/4] Check that a lone claimant is never their own young person, in every scheme Mutation testing showed that dropping the claimant filter from the Scottish branch failed only one generated-unit property. Add each subject of the exhaustive grid as the claimant of a benefit unit of their own, with a check that some are qualifying young persons by status. Co-Authored-By: Claude Opus 5.5 --- ...l_tax_reduction_young_person_properties.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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 fb5c51ede4..00b0b95a95 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 @@ -184,7 +184,8 @@ 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. + # 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", @@ -222,6 +223,18 @@ def test_scheme_selection_over_every_combination(): 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 @@ -230,6 +243,17 @@ def test_scheme_selection_over_every_combination(): 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 From 41b1d318d4e0fa946851d70ec97e707e8c962cf6 Mon Sep 17 00:00:00 2001 From: Max Ghenis Date: Sat, 10 Oct 2026 21:58:05 -0400 Subject: [PATCH 4/4] Keep the applicant's earnings in the Scottish working-age means test Review round 1 of #2288: a household head aged 18 or 19 who 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 the Scottish working-age scheme no longer reads them as a young person, and their earnings dropped out. The applicant stays counted (SSI 2021/249 reg. 36(1)). Adds YAML Cases 9 and 10 and a property test, documents the looked-after approximation, and corrects a test comment. Not yet run locally: the host was over its memory limit. CI runs them. Co-Authored-By: Claude Opus 5.5 --- .../council_tax_reduction_young_person.yaml | 90 ++++++++++++++++++- ...l_tax_reduction_young_person_properties.py | 87 +++++++++++++++++- ...council_tax_reduction_applicable_income.py | 15 +++- ..._young_person_for_council_tax_reduction.py | 6 ++ 4 files changed, 193 insertions(+), 5 deletions(-) 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 c4c2f1f889..d08a38d0e9 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 @@ -86,7 +86,10 @@ # 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. + # 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. @@ -263,3 +266,88 @@ 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 index 00b0b95a95..c57d295b08 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 @@ -28,11 +28,18 @@ 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 @@ -331,8 +338,10 @@ def test_lone_parent_and_working_age_personal_allowance(units, year): def test_scottish_working_age_differs_from_child_benefit_at_19(): - # Not vacuous: the two definitions do differ, in both directions, so the - # Scottish working-age flag is a different rule from the legacy flag. + # 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, @@ -377,3 +386,77 @@ def test_scottish_working_age_differs_from_child_benefit_at_19(): ] 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_income.py b/policyengine_uk/variables/gov/local_authorities/council_tax_reduction/council_tax_reduction_applicable_income.py index fef871ff88..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_council_tax_reduction", 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 index 034bab6abf..476e395c44 100644 --- 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 @@ -20,6 +20,12 @@ class is_child_or_young_person_for_council_tax_reduction(Variable): 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