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/2610.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Restored neutral pre-2021 Housing Benefit and Council Tax Reduction cohort flags while retaining the existing FY2021-22 annual split.
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Component regressions: upstream pensioner flags explicitly isolate allowance
# selection. These cases do not claim natural pensioner eligibility or cash awards.
# GB SI 2006/214 Sch 3 para 1 at 30 April 2020 and England SI 2012/2885 Sch 2 para 1
# supply 187.75/280.85 weekly. Model annualisation uses 52 weeks.
- name: Case 1, omitted-age component has the single higher allowance in 2020.
period: 2020
absolute_error_margin: 0.01
input:
people:
person1: {}
benunits:
benunit:
members: [person1]
housing_benefit_eligible: true
housing_benefit_pension_age_regulations_apply: true
council_tax_reduction_pensioner: true
benefits_premiums: 0
council_tax_reduction_premiums: 0
households:
household:
members: [person1]
region: LONDON
output:
housing_benefit_applicable_amount: 9_763
council_tax_reduction_applicable_amount: 9_763

- name: Case 2, isolated couple component has the higher allowance in 2020.
period: 2020
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
person2:
age: 40
benunits:
benunit:
members: [person1, person2]
housing_benefit_eligible: true
housing_benefit_pension_age_regulations_apply: true
council_tax_reduction_pensioner: true
benefits_premiums: 0
council_tax_reduction_premiums: 0
households:
household:
members: [person1, person2]
region: LONDON
output:
housing_benefit_applicable_amount: 14_604.2
council_tax_reduction_applicable_amount: 14_604.2

# Annual 2021 means FY2021-22, not legal commencement on 1 January.
# SI 2021/188 reg 2 and NI SR 2021/70 reg 2 insert 177.10 weekly;
# England SI 2021/29 regs 1(2), 7 insert the same lower component.
- name: Case 3, post-cutoff single component retains the lower allowance in 2021.
period: 2021
absolute_error_margin: 0.01
input:
people:
person1:
age: 40
is_male: true
benunits:
benunit:
members: [person1]
housing_benefit_eligible: true
housing_benefit_pension_age_regulations_apply: true
council_tax_reduction_pensioner: true
benefits_premiums: 0
council_tax_reduction_premiums: 0
households:
household:
members: [person1]
region: LONDON
output:
housing_benefit_applicable_amount: 9_209.2
council_tax_reduction_applicable_amount: 9_209.2

# 191.15 is pinned here only for England CTR, directly by SI 2021/29 reg 7.
- name: Case 4, England CTR retains the higher allowance for pre-cutoff attainment.
period: 2021
absolute_error_margin: 0.01
input:
people:
person1:
age: 66
date_of_birth: 19550331
is_male: true
benunits:
benunit:
members: [person1]
council_tax_reduction_pensioner: true
benefits_premiums: 0
council_tax_reduction_premiums: 0
households:
household:
members: [person1]
region: LONDON
output:
state_pension_age_date: 20210331
council_tax_reduction_applicable_amount: 9_939.8
171 changes: 171 additions & 0 deletions policyengine_uk/tests/test_housing_benefit_pension_age_cohorts.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,3 +309,174 @@ def test_other_members_do_not_change_the_cohort(units, others, year):
plain.calculate(variable, year),
atol=0.005,
)


# SI 2021/188 reg 1(2), NI SR 2021/70 reg 1(2), and SI 2021/29 reg 1(2)
# introduce the cohort split for FY2021-22. Annual 2021 represents that fiscal
# year; it does not claim that these provisions commenced on 1 January 2021.
COHORT_VARIABLES = (
"housing_benefit_attained_pension_age_before_cutoff",
"council_tax_reduction_attained_pension_age_before_cutoff",
)


def cutoff_situation(year, people=None, region="LONDON", component=False):
"""Build public situations; component flags isolate allowances, not eligibility."""
if people is None:
people = {"person1": {"age": {year: 40}, "is_male": {year: True}}}
benunit = {"members": list(people)}
if component:
benunit.update(
{
"housing_benefit_eligible": {year: True},
"housing_benefit_pension_age_regulations_apply": {year: True},
"council_tax_reduction_pensioner": {year: True},
"benefits_premiums": {year: 0},
"council_tax_reduction_premiums": {year: 0},
}
)
return {
"people": people,
"benunits": {"benunit": benunit},
"households": {
"household": {"members": list(people), "region": {year: region}}
},
}


@pytest.mark.parametrize("variable", COHORT_VARIABLES, ids=["hb", "ctr"])
@pytest.mark.parametrize("year", [2015, 2020, 2021])
def test_pre2021_cutoff_is_neutral_without_entitlement_overrides(variable, year):
"""A neutral historical cohort flag does not award pensioner eligibility."""
sim = Simulation(situation=cutoff_situation(year))
assert sim.calculate("state_pension_age_date", year)[0] > 20210401
assert bool(sim.calculate(variable, year)[0]) is (year < 2021)


@pytest.mark.parametrize("region", ["LONDON", "SCOTLAND", "WALES", "NORTHERN_IRELAND"])
@pytest.mark.parametrize("year", [2015, 2020])
def test_pre2021_neutral_cohort_does_not_depend_on_region(year, region):
"""The CTR helper is neutral; this does not extend English monetary rules."""
sim = Simulation(situation=cutoff_situation(year, region=region))
for variable in COHORT_VARIABLES:
assert bool(sim.calculate(variable, year)[0]) is True


@pytest.mark.parametrize("year", [2021, 2025])
@pytest.mark.parametrize(
"birth, attainment, expected",
[
(19550331, 20210331, True),
(19550401, 20210401, False),
(19550402, 20210402, False),
],
)
def test_post2021_exact_attainment_boundary_is_preserved(
year, birth, attainment, expected
):
"""These men attain 66 on their actual 1955 birth-date anniversary."""
people = {
"person1": {
"age": {year: year - 1955},
"date_of_birth": {year: birth},
"is_male": {year: True},
}
}
sim = Simulation(situation=cutoff_situation(year, people))
assert sim.calculate("state_pension_age_date", year)[0] == attainment
for variable in COHORT_VARIABLES:
assert bool(sim.calculate(variable, year)[0]) is expected


@pytest.mark.parametrize("year", [2021, 2025])
@pytest.mark.parametrize("qualifying_role", ["partner", "other"])
def test_post2021_only_claimant_or_partner_can_supply_earlier_attainment(
year, qualifying_role
):
"""An earlier-attaining partner counts; an unrelated member does not."""
people = {}
for name, birth, role in [
("person1", 19550402, True),
("person2", 19550331, qualifying_role == "partner"),
]:
people[name] = {
"age": {year: year - 1955},
"date_of_birth": {year: birth},
"is_male": {year: True},
"is_claimant_or_partner": {year: role},
"is_council_tax_reduction_applicant_or_partner": {year: role},
}
sim = Simulation(situation=cutoff_situation(year, people))
for variable in COHORT_VARIABLES:
assert bool(sim.calculate(variable, year)[0]) is (qualifying_role == "partner")


@pytest.mark.parametrize("year", [2021, 2025])
def test_post2021_cutoff_parameter_remains_reformable(year):
"""Moving the cutoff one day includes an otherwise identical 1 April cohort."""
from policyengine_uk.model_api import Scenario

people = {
"person1": {
"age": {year: year - 1955},
"date_of_birth": {year: 19550401},
"is_male": {year: True},
}
}
situation = cutoff_situation(year, people)
baseline = Simulation(situation=situation)
reformed = Simulation(
situation=situation,
scenario=Scenario(
parameter_changes={
"gov.dwp.housing_benefit.allowances.pension_age_cutoff": {
str(year): 20210402
}
}
),
)
for variable in COHORT_VARIABLES:
assert bool(reformed.calculate(variable, year)[0]) is True
assert bool(baseline.calculate(variable, year)[0]) is False


@pytest.mark.parametrize("year", [2015, 2020])
def test_omitted_age_component_keeps_existing_higher_allowance(year):
"""No age/DOB inputs: isolate the component, not natural pensioner entitlement."""
sim = Simulation(
situation=cutoff_situation(year, people={"person1": {}}, component=True)
)
for variable in COHORT_VARIABLES:
assert bool(sim.calculate(variable, year)[0]) is True
higher = sim.tax_benefit_system.parameters(
str(year)
).gov.dwp.housing_benefit.allowances.single.aged
# 2015 uses the existing model rate symbolically; historical rate repair is #2207.
for variable in [
"housing_benefit_applicable_amount",
"council_tax_reduction_applicable_amount",
]:
assert sim.calculate(variable, year)[0] == pytest.approx(higher * 52, abs=0.01)


@pytest.mark.parametrize("region", ["SCOTLAND", "WALES", "NORTHERN_IRELAND"])
def test_nonenglish_ctr_component_keeps_existing_higher_route(region):
"""England's cutoff does not introduce a new monetary gate elsewhere."""
year = 2021
sim = Simulation(situation=cutoff_situation(year, region=region, component=True))
assert bool(sim.calculate(COHORT_VARIABLES[1], year)[0]) is False
higher = sim.tax_benefit_system.parameters(
str(year)
).gov.dwp.housing_benefit.allowances.single.aged
assert sim.calculate("council_tax_reduction_applicable_amount", year)[
0
] == pytest.approx(higher * 52, abs=0.01)


@pytest.mark.parametrize("variable", COHORT_VARIABLES, ids=["hb", "ctr"])
def test_explicit_historical_cohort_input_remains_authoritative(variable):
year = 2020
situation = cutoff_situation(year)
situation["benunits"]["benunit"][variable] = {year: False}
sim = Simulation(situation=situation)
assert bool(sim.calculate(variable, year)[0]) is False
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,25 @@ class housing_benefit_attained_pension_age_before_cutoff(Variable):
"did get the lower one. Other members of the benefit unit do not "
"count. Before the "
"cutoff took effect there was a single pension-age allowance, the "
"higher one, so this is true for every unit then."
"higher one, so this is true for every unit then. Annual 2021 "
"represents FY2021-22 under the model fiscal-year convention; the "
"cohort split commenced on 1 April 2021. This flag does not decide "
"pension-age eligibility."
)
definition_period = YEAR
reference = (
"https://www.legislation.gov.uk/uksi/2006/214/schedule/3",
"https://www.legislation.gov.uk/uksi/2021/188/regulation/2/made",
"https://www.legislation.gov.uk/nisr/2006/406/schedule/4",
"https://www.legislation.gov.uk/uksi/2021/188/regulation/1/made",
"https://www.legislation.gov.uk/nisr/2021/70/regulation/1/made",
"https://www.legislation.gov.uk/nisr/2021/70/regulation/2/made",
)

def formula(benunit, period, parameters):
return np.ones(benunit.count, dtype=bool)

def formula_2021(benunit, period, parameters):
cutoff = parameters(
period
).gov.dwp.housing_benefit.allowances.pension_age_cutoff
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,22 @@ class council_tax_reduction_attained_pension_age_before_cutoff(Variable):
"pension-age personal allowance; other members of the household do "
"not count. Before the cutoff took effect there was a single "
"pension-age allowance, the higher one, so this is true for every unit "
"then."
"then. Annual 2021 represents FY2021-22 under the model fiscal-year "
"convention: the English amendment applies to financial years "
"beginning on or after 1 April 2021. This flag does not decide "
"pension-age eligibility."
)
definition_period = YEAR
reference = "https://www.legislation.gov.uk/uksi/2012/2885/schedule/2"
reference = (
"https://www.legislation.gov.uk/uksi/2012/2885/schedule/2",
"https://www.legislation.gov.uk/uksi/2021/29/regulation/1/made",
"https://www.legislation.gov.uk/uksi/2021/29/regulation/7/made",
)

def formula(benunit, period, parameters):
return np.ones(benunit.count, dtype=bool)

def formula_2021(benunit, period, parameters):
cutoff = parameters(
period
).gov.dwp.housing_benefit.allowances.pension_age_cutoff
Expand Down
Loading