Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/fix-2295-scottish-ctr-nondependants.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use Scotland's working-age Council Tax Reduction young-person definition for the severe disability premium's non-dependant test within the applicant's benefit unit, retaining existing looked-after and other-benefit-unit treatment.
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# SSI2021/249 regs6,8(2)(a),Sch1para13. Existing2026 model controls:
# allowance95.55*52=4968.60; severe86.05*52=4474.60; withdrawal20%.
# PIP qualifies SDP but generic disability flag is separately supplied/defaultfalse.
# Pension12570:no tax; PIP disregarded; Child Benefit claim declined.
# CTR=2000-20%*(12570-allowance):479.72 or1374.64.
# Annual UC terminal-date input represents unavailable assessment-period detail.
# Reverse case supplies CB family status: input-contract composition,
# not a naturally derived eligibility pair or full statutory rate audit.
# Shared HB premium component and IS applicable amount are protected below.
# This household has no continuing HB claim, so its gated HB applicable
# amount is0; it is not used as a positive HB-payment regression oracle.
- name: 'Terminal-date input contract: CB young person after UC terminal date removes
Scottish CTR premium'
period: 2026
absolute_error_margin: 0.01
input:
people:
applicant:
age: 45
is_claimant_or_partner: true
pip_dl_category: ENHANCED
private_pension_income: 12570
student:
age: 19
current_education: UPPER_SECONDARY
age_started_or_accepted_current_education_or_training: 17
is_before_universal_credit_qualifying_young_person_terminal_date: false
is_claimant_or_partner: false
benunits:
family:
members:
- applicant
- student
claims_all_entitled_benefits: true
would_claim_uc: false
would_claim_IS: false
would_claim_pc: false
would_claim_CTC: false
would_claim_WTC: false
would_claim_child_benefit: false
households:
home:
members:
- applicant
- student
country: SCOTLAND
local_authority: CITY_OF_EDINBURGH
council_tax: 2000
savings: 0
output:
simulated_council_tax_reduction_benunit: 479.72
disability_premium: 0
enhanced_disability_premium: 0
severe_disability_premium: 4474.6
council_tax_reduction_pensioner: false
council_tax_reduction_applicable_income: 12570
council_tax_reduction_applicable_amount: 4968.6
council_tax_reduction_non_dep_deductions: 0
severe_disability_premium_before_non_dependant_test: 4474.6
council_tax_reduction_severe_disability_premium: 0
working_age_benefits_premiums: 4474.6
benefits_premiums: 4474.6
income_support_applicable_amount: 9443.2
- name: Before terminal date both definitions retain Scottish CTR premium
period: 2026
absolute_error_margin: 0.01
input:
people:
applicant:
age: 45
is_claimant_or_partner: true
pip_dl_category: ENHANCED
private_pension_income: 12570
student:
age: 19
current_education: UPPER_SECONDARY
age_started_or_accepted_current_education_or_training: 17
is_before_universal_credit_qualifying_young_person_terminal_date: true
is_claimant_or_partner: false
benunits:
family:
members:
- applicant
- student
claims_all_entitled_benefits: true
would_claim_uc: false
would_claim_IS: false
would_claim_pc: false
would_claim_CTC: false
would_claim_WTC: false
would_claim_child_benefit: false
households:
home:
members:
- applicant
- student
country: SCOTLAND
local_authority: CITY_OF_EDINBURGH
council_tax: 2000
savings: 0
output:
simulated_council_tax_reduction_benunit: 1374.64
disability_premium: 0
enhanced_disability_premium: 0
severe_disability_premium: 4474.6
council_tax_reduction_pensioner: false
council_tax_reduction_applicable_income: 12570
council_tax_reduction_applicable_amount: 9443.2
council_tax_reduction_non_dep_deductions: 0
severe_disability_premium_before_non_dependant_test: 4474.6
council_tax_reduction_severe_disability_premium: 4474.6
working_age_benefits_premiums: 4474.6
benefits_premiums: 4474.6
income_support_applicable_amount: 9443.2
- name: 'Supplied-status contract: UC young person outside CB family restores Scottish
CTR premium'
period: 2026
absolute_error_margin: 0.01
input:
people:
applicant:
age: 45
is_claimant_or_partner: true
pip_dl_category: ENHANCED
private_pension_income: 12570
student:
age: 19
current_education: UPPER_SECONDARY
age_started_or_accepted_current_education_or_training: 17
is_before_universal_credit_qualifying_young_person_terminal_date: true
is_claimant_or_partner: false
is_child_or_qualifying_young_person_for_child_benefit: false
benunits:
family:
members:
- applicant
- student
claims_all_entitled_benefits: true
would_claim_uc: false
would_claim_IS: false
would_claim_pc: false
would_claim_CTC: false
would_claim_WTC: false
would_claim_child_benefit: false
households:
home:
members:
- applicant
- student
country: SCOTLAND
local_authority: CITY_OF_EDINBURGH
council_tax: 2000
savings: 0
output:
simulated_council_tax_reduction_benunit: 1374.64
disability_premium: 0
enhanced_disability_premium: 0
severe_disability_premium: 0
council_tax_reduction_pensioner: false
council_tax_reduction_applicable_income: 12570
council_tax_reduction_applicable_amount: 9443.2
council_tax_reduction_non_dep_deductions: 0
severe_disability_premium_before_non_dependant_test: 4474.6
council_tax_reduction_severe_disability_premium: 4474.6
working_age_benefits_premiums: 0
benefits_premiums: 0
income_support_applicable_amount: 4968.6
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""CTR family-mask input contracts, separate from derived eligibility tests.

SSI 2021/249 regs 6 and 8(2)(a), Sch 1 para 13 retain an applicant's
UC young person in their family. These finite grids deliberately supply the
two young-person statuses to exercise both directions independently of the
upstream eligibility rules. The policy YAML uses school/course inputs and
the annual terminal-date input, plus actual PIP, pension and council tax.

Looked-after and other-benefit-unit treatment remains the existing model
baseline, not a certification of reg 8(2)(b) and its paragraph (3) overrides.
"""

from itertools import product

import numpy as np
import pytest

from policyengine_uk import Simulation


def input_grid(rows, country="SCOTLAND", year=2026, applicant_age=45):
people, benunits, households = {}, {}, {}
for i, row in enumerate(rows):
applicant, student = f"a{i}", f"s{i}"
people[applicant] = {
"age": applicant_age,
"is_claimant_or_partner": True,
"pip_dl_category": "ENHANCED",
}
people[student] = {
"age": row.get("age", 19),
"is_claimant_or_partner": False,
"is_child_or_qualifying_young_person_for_child_benefit": row["cb"],
"is_qualifying_young_person_for_universal_credit": row["uc"],
"is_looked_after_by_local_authority": row.get("looked_after", False),
"is_blind": row.get("blind", False),
"pip_dl_category": "STANDARD" if row.get("benefit", False) else "NONE",
}
members = [applicant, student]
benunits[f"b{i}"] = {
"members": [applicant] if row.get("other_bu", False) else members,
"would_claim_uc": False,
"would_claim_IS": False,
"would_claim_pc": False,
}
if row.get("other_bu", False):
benunits[f"other{i}"] = {"members": [student]}
households[f"h{i}"] = {"members": members, "country": country}
situation = {"people": people, "benunits": benunits, "households": households}
# A situation's default period is the model default, not necessarily year.
for group in situation.values():
for values in group.values():
for name, value in list(values.items()):
if name != "members":
values[name] = {year: value}
return Simulation(situation=situation), [f"b{i}" for i in range(len(rows))]


def values(sim, variable, names, year=2026):
result = sim.calculate(variable, year)
ids = list(sim.populations["benunit"].ids)
return np.array([result[ids.index(name)] for name in names])


@pytest.mark.parametrize(
"country,year,applicant_age,scottish_working_age",
[
("SCOTLAND", 2026, 45, True),
("SCOTLAND", 2021, 45, False),
("SCOTLAND", 2026, 70, False),
("ENGLAND", 2026, 45, False),
("WALES", 2026, 45, False),
("NORTHERN_IRELAND", 2026, 45, False),
],
)
def test_own_family_status_contract_by_applicant_scheme(
country, year, applicant_age, scottish_working_age
):
rows = [
dict(cb=cb, uc=uc, looked_after=looked_after)
for cb, uc, looked_after in product([False, True], repeat=3)
]
sim, names = input_grid(rows, country, year, applicant_age)
expected = [
not (
row["uc"] if scottish_working_age and not row["looked_after"] else row["cb"]
)
for row in rows
]
assert (
values(
sim,
"has_non_dependant_for_council_tax_reduction_severe_disability_premium",
names,
year,
).tolist()
== expected
)
# HB and IS retain the original Child Benefit family classification.
assert values(
sim, "has_non_dependant_for_severe_disability_premium", names, year
).tolist() == [not row["cb"] for row in rows]


def test_age_boundary_and_blind_or_benefit_exceptions():
rows = [
dict(age=age, cb=True, uc=False, blind=blind, benefit=benefit)
for age, blind, benefit in product(
[17.999, 18, 19, 20], [False, True], [False, True]
)
]
sim, names = input_grid(rows)
expected = [
row["age"] >= 18 and not row["blind"] and not row["benefit"] for row in rows
]
assert (
values(
sim,
"has_non_dependant_for_council_tax_reduction_severe_disability_premium",
names,
).tolist()
== expected
)
# The shared gate still disregards these own-BU Child Benefit young people.
assert not values(
sim, "has_non_dependant_for_severe_disability_premium", names
).any()


def test_other_benefit_units_keep_existing_treatment():
rows = [
dict(cb=cb, uc=uc, looked_after=looked_after, other_bu=True)
for cb, uc, looked_after in product([False, True], repeat=3)
]
sim, names = input_grid(rows)
assert values(sim, "has_non_dependant_for_severe_disability_premium", names).all()
assert values(
sim,
"has_non_dependant_for_council_tax_reduction_severe_disability_premium",
names,
).all()
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ def formula(benunit, period, parameters):
premiums = where(
pensioner,
benunit("pension_age_benefits_premiums", period),
benunit("working_age_benefits_premiums", period),
benunit("working_age_benefits_premiums", period)
+ (
benunit("council_tax_reduction_severe_disability_premium", period)
- benunit("severe_disability_premium", period)
),
)
return personal_allowance + premiums
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from policyengine_uk.model_api import *


class council_tax_reduction_severe_disability_premium(Variable):
value_type = float
entity = BenUnit
label = "Working-age CTR severe disability premium"
documentation = (
"The shared legacy premium's amount before its non-dependant test, "
"with the Council Tax Reduction-specific family test applied. This "
"allows either direction of a difference between the Universal Credit "
"and Child Benefit young-person definitions. The test retains the "
"existing classification for looked-after residents and other benefit "
"units; see has_non_dependant_for_council_tax_reduction_severe_disability_premium. "
"Pension-age Council Tax Reduction continues to use its separate premium."
)
definition_period = YEAR
unit = GBP
reference = (
"https://www.legislation.gov.uk/ssi/2021/249/regulation/8",
"https://www.legislation.gov.uk/ssi/2021/249/schedule/1/paragraph/13",
)

def formula(benunit, period, parameters):
amount = benunit("severe_disability_premium_before_non_dependant_test", period)
no_non_dependant = ~benunit(
"has_non_dependant_for_council_tax_reduction_severe_disability_premium",
period,
)
return amount * no_non_dependant
Loading
Loading