From 62f83d334af3e98eb99c57e7f5c94f9bc1931ecb Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:12:15 +0400 Subject: [PATCH 1/3] Fix 2020 TV licence concession transition --- changelog.d/2151.fixed.md | 1 + .../discount/aged/must_claim_pc.yaml | 21 ++++++---- .../gov/dcms/bbc/tv_licence_discount.yaml | 39 ++++++++++++++++++- .../tests/test_tv_licence_transition.py | 32 +++++++++++++++ .../bbc/tv_licence/tv_licence_discount.py | 11 ++++-- 5 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 changelog.d/2151.fixed.md create mode 100644 policyengine_uk/tests/test_tv_licence_transition.py diff --git a/changelog.d/2151.fixed.md b/changelog.d/2151.fixed.md new file mode 100644 index 0000000000..4db286614e --- /dev/null +++ b/changelog.d/2151.fixed.md @@ -0,0 +1 @@ +- Corrected the 2020 over-75 TV licence Pension Credit transition to start on 1 August and annualise the split fiscal year. diff --git a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml index 6f2b4749d9..be366e434e 100644 --- a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml +++ b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml @@ -1,10 +1,17 @@ -description: Whether aged individuals must claim Pension Credit in order to qualify for a free TV licence. +description: Share of the fiscal year during which aged individuals must receive Pension Credit to qualify for a free TV licence. values: - 2003-01-01: false - 2020-04-01: true + 2003-01-01: + value: 0 + 2020-08-01: + value: 1 + reference: + - title: Government and BBC joint statement on delaying the TV licence fee for over 75s + href: https://www.gov.uk/government/news/bbc-and-governments-joint-statement-on-delaying-the-tv-licence-fee-for-over-75s metadata: - unit: bool - label: Aged TV Licence Pension Credit requirement + fiscal_year_blend: true + unit: /1 + period: year + label: Aged TV Licence Pension Credit requirement share reference: - - title: "Free and reduced fee TV Licences | TV Licensing" - href: https://www.tvlicensing.co.uk/reducedfee \ No newline at end of file + - title: Get a free or discounted TV licence + href: https://www.gov.uk/free-discount-tv-licence diff --git a/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv_licence_discount.yaml b/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv_licence_discount.yaml index 35a1b110ba..e3f117baa7 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv_licence_discount.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv_licence_discount.yaml @@ -41,4 +41,41 @@ pension_credit: 1 is_blind: true output: - tv_licence_discount: 1 \ No newline at end of file + tv_licence_discount: 1 + +- name: All aged households qualify before the Pension Credit condition. + period: 2019 + input: + age: 75 + pension_credit: 0 + is_blind: false + output: + tv_licence_discount: 1 + +- name: The 2020-21 discount reflects the 1 August transition. + period: 2020 + absolute_error_margin: 0.000001 + input: + age: 75 + pension_credit: 0 + is_blind: false + output: + tv_licence_discount: 0.3205479452 + +- name: Pension Credit recipients retain the full 2020-21 discount. + period: 2020 + input: + age: 75 + pension_credit: 1 + is_blind: false + output: + tv_licence_discount: 1 + +- name: The Pension Credit condition applies for all of 2021-22. + period: 2021 + input: + age: 75 + pension_credit: 0 + is_blind: false + output: + tv_licence_discount: 0 diff --git a/policyengine_uk/tests/test_tv_licence_transition.py b/policyengine_uk/tests/test_tv_licence_transition.py new file mode 100644 index 0000000000..9f3595f5df --- /dev/null +++ b/policyengine_uk/tests/test_tv_licence_transition.py @@ -0,0 +1,32 @@ +"""Tests for the 2020 over-75 TV licence concession transition.""" + +from pathlib import Path + +import pytest +from policyengine_core.parameters import ParameterNode + +import policyengine_uk +from policyengine_uk import CountryTaxBenefitSystem + + +def _requirement(parameter_root): + return parameter_root.gov.dcms.bbc.tv_licence.discount.aged.must_claim_pc + + +def test_pension_credit_condition_uses_the_statutory_start_date(): + """The BBC deferred the condition from 1 June to 1 August 2020.""" + parameters = ParameterNode( + directory_path=str(Path(policyengine_uk.__file__).parent / "parameters") + ) + requirement = _requirement(parameters) + + assert requirement("2020-07-31") == 0 + assert requirement("2020-08-01") == 1 + + +def test_pension_credit_condition_is_annualised_across_2020_21(): + """The fiscal year has 117 unrestricted and 248 restricted days.""" + system = CountryTaxBenefitSystem() + requirement = _requirement(system.get_parameters_at_instant("2020")) + + assert requirement == pytest.approx(248 / 365) diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py index ffc499db15..1c8335ee60 100644 --- a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py @@ -9,7 +9,8 @@ class tv_licence_discount(Variable): unit = "/1" reference = ( "https://www.legislation.gov.uk/ukpga/2003/21/section/365A", - "https://www.tvlicensing.co.uk/reducedfee", + "https://www.gov.uk/government/news/bbc-and-governments-joint-statement-on-delaying-the-tv-licence-fee-for-over-75s", + "https://www.gov.uk/free-discount-tv-licence", ) def formula(household, period, parameters): @@ -20,9 +21,11 @@ def formula(household, period, parameters): aged = person("age", period) >= tv_licence.discount.aged.min_age has_aged = household.any(aged) claims_pc = add(household, period, ["pension_credit"]) > 0 - meets_pc_requirement = not_(tv_licence.discount.aged.must_claim_pc) | claims_pc - eligible_for_aged_discount = has_aged & meets_pc_requirement - aged_discount = eligible_for_aged_discount * tv_licence.discount.aged.discount + pc_requirement_share = tv_licence.discount.aged.must_claim_pc + aged_discount_share = where(claims_pc, 1, 1 - pc_requirement_share) + aged_discount = ( + has_aged * aged_discount_share * tv_licence.discount.aged.discount + ) # Blind discount is_blind = person("is_blind", period) From 26e9ed561af43bdcb32ee7ff8980c39042e76dd4 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 7 Oct 2026 01:47:27 +0400 Subject: [PATCH 2/3] Fix issues from review: preserve Boolean TV licence rule --- changelog.d/2151.fixed.md | 2 +- .../discount/aged/must_claim_pc.yaml | 13 ++++----- .../tests/test_tv_licence_transition.py | 28 +++++++++++++++---- .../bbc/tv_licence/tv_licence_discount.py | 19 ++++++++----- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/changelog.d/2151.fixed.md b/changelog.d/2151.fixed.md index 4db286614e..5e300b1805 100644 --- a/changelog.d/2151.fixed.md +++ b/changelog.d/2151.fixed.md @@ -1 +1 @@ -- Corrected the 2020 over-75 TV licence Pension Credit transition to start on 1 August and annualise the split fiscal year. +- Corrected the 2020 over-75 TV licence Pension Credit transition to start on 1 August while preserving its Boolean eligibility rule. diff --git a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml index be366e434e..a3c9f3b21b 100644 --- a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml +++ b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/must_claim_pc.yaml @@ -1,17 +1,16 @@ -description: Share of the fiscal year during which aged individuals must receive Pension Credit to qualify for a free TV licence. +description: Whether aged individuals must receive Pension Credit to qualify for a free TV licence. values: - 2003-01-01: - value: 0 + 2003-01-01: false 2020-08-01: - value: 1 + value: true reference: - title: Government and BBC joint statement on delaying the TV licence fee for over 75s href: https://www.gov.uk/government/news/bbc-and-governments-joint-statement-on-delaying-the-tv-licence-fee-for-over-75s metadata: - fiscal_year_blend: true - unit: /1 + preserve_calendar_dates: true + unit: bool period: year - label: Aged TV Licence Pension Credit requirement share + label: Aged TV Licence Pension Credit requirement reference: - title: Get a free or discounted TV licence href: https://www.gov.uk/free-discount-tv-licence diff --git a/policyengine_uk/tests/test_tv_licence_transition.py b/policyengine_uk/tests/test_tv_licence_transition.py index 9f3595f5df..08bcf520a7 100644 --- a/policyengine_uk/tests/test_tv_licence_transition.py +++ b/policyengine_uk/tests/test_tv_licence_transition.py @@ -7,6 +7,7 @@ import policyengine_uk from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.utils.excise import fiscal_year_segments def _requirement(parameter_root): @@ -20,13 +21,30 @@ def test_pension_credit_condition_uses_the_statutory_start_date(): ) requirement = _requirement(parameters) - assert requirement("2020-07-31") == 0 - assert requirement("2020-08-01") == 1 + assert requirement("2020-07-31") is False + assert requirement("2020-08-01") is True -def test_pension_credit_condition_is_annualised_across_2020_21(): +def test_pension_credit_condition_keeps_its_date_after_processing(): + """Processing preserves the 1 August Boolean policy transition.""" + system = CountryTaxBenefitSystem() + requirement = _requirement(system.parameters) + + assert requirement("2020-07-31") is False + assert requirement("2020-08-01") is True + + +def test_pension_credit_condition_segments_the_2020_21_fiscal_year(): """The fiscal year has 117 unrestricted and 248 restricted days.""" system = CountryTaxBenefitSystem() - requirement = _requirement(system.get_parameters_at_instant("2020")) + aged_rules = system.parameters.gov.dcms.bbc.tv_licence.discount.aged + segments = list(fiscal_year_segments(aged_rules, 2020)) + + unrestricted_share = sum( + share for rules, share in segments if not rules.must_claim_pc + ) + restricted_share = sum(share for rules, share in segments if rules.must_claim_pc) - assert requirement == pytest.approx(248 / 365) + assert unrestricted_share == pytest.approx(117 / 365) + assert restricted_share == pytest.approx(248 / 365) + assert all(isinstance(rules.must_claim_pc, bool) for rules, _ in segments) diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py index 1c8335ee60..655b1eca18 100644 --- a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_discount.py @@ -1,4 +1,5 @@ from policyengine_uk.model_api import * +from policyengine_uk.utils.excise import fiscal_year_segments class tv_licence_discount(Variable): @@ -18,14 +19,18 @@ def formula(household, period, parameters): tv_licence = parameters(period).gov.dcms.bbc.tv_licence # Aged discount - aged = person("age", period) >= tv_licence.discount.aged.min_age - has_aged = household.any(aged) claims_pc = add(household, period, ["pension_credit"]) > 0 - pc_requirement_share = tv_licence.discount.aged.must_claim_pc - aged_discount_share = where(claims_pc, 1, 1 - pc_requirement_share) - aged_discount = ( - has_aged * aged_discount_share * tv_licence.discount.aged.discount - ) + aged_discount = 0 + for aged_rules, share in fiscal_year_segments( + parameters.gov.dcms.bbc.tv_licence.discount.aged, + period.start.year, + ): + aged = person("age", period) >= aged_rules.min_age + has_aged = household.any(aged) + meets_pc_requirement = not_(aged_rules.must_claim_pc) | claims_pc + aged_discount += ( + has_aged * meets_pc_requirement * aged_rules.discount * share + ) # Blind discount is_blind = person("is_blind", period) From 01b02c3577e0f43c7e816b845208c8f0d58c2ca5 Mon Sep 17 00:00:00 2001 From: Anthony Volk <14987227+anth-volk@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:15:59 +0400 Subject: [PATCH 3/3] Fix issues from review: preserve TV licence rule dates --- .../tv_licence/discount/aged/discount.yaml | 3 +- .../bbc/tv_licence/discount/aged/min_age.yaml | 1 + .../tests/test_tv_licence_transition.py | 33 ++++++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/discount.yaml b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/discount.yaml index 618d617809..019ba291b7 100644 --- a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/discount.yaml +++ b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/discount.yaml @@ -2,8 +2,9 @@ description: Percentage discount for qualifying aged households. values: 2003-01-01: 1 metadata: + preserve_calendar_dates: true unit: /1 label: Aged TV Licence discount reference: - title: "Free and reduced fee TV Licences | TV Licensing" - href: https://www.tvlicensing.co.uk/reducedfee \ No newline at end of file + href: https://www.tvlicensing.co.uk/reducedfee diff --git a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/min_age.yaml b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/min_age.yaml index 1125466471..ce4b410ea1 100644 --- a/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/min_age.yaml +++ b/policyengine_uk/parameters/gov/dcms/bbc/tv_licence/discount/aged/min_age.yaml @@ -2,6 +2,7 @@ description: Minimum age required to qualify for a TV licence discount. values: 2003-01-01: 75 metadata: + preserve_calendar_dates: true unit: year label: Aged TV Licence discounted minimum age reference: diff --git a/policyengine_uk/tests/test_tv_licence_transition.py b/policyengine_uk/tests/test_tv_licence_transition.py index 08bcf520a7..881ac67d51 100644 --- a/policyengine_uk/tests/test_tv_licence_transition.py +++ b/policyengine_uk/tests/test_tv_licence_transition.py @@ -6,7 +6,7 @@ from policyengine_core.parameters import ParameterNode import policyengine_uk -from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk import CountryTaxBenefitSystem, Simulation from policyengine_uk.utils.excise import fiscal_year_segments @@ -48,3 +48,34 @@ def test_pension_credit_condition_segments_the_2020_21_fiscal_year(): assert unrestricted_share == pytest.approx(117 / 365) assert restricted_share == pytest.approx(248 / 365) assert all(isinstance(rules.must_claim_pc, bool) for rules, _ in segments) + + +def test_year_keyed_minimum_age_reform_applies_to_one_fiscal_year(): + """A bare-year age reform must not be split across adjacent years.""" + years = (2024, 2025, 2026) + situation = { + "people": { + "person": { + "age": {str(year): 72 for year in years}, + }, + }, + "benunits": { + "benunit": { + "members": ["person"], + "pension_credit": {str(year): 1 for year in years}, + }, + }, + "households": { + "household": { + "members": ["person"], + }, + }, + } + reform = { + "gov.dcms.bbc.tv_licence.discount.aged.min_age": {"2025": 70}, + } + simulation = Simulation(situation=situation, reform=reform) + + discounts = [simulation.calculate("tv_licence_discount", year)[0] for year in years] + + assert discounts == pytest.approx([0, 1, 0])