diff --git a/changelog.d/2145.fixed.md b/changelog.d/2145.fixed.md new file mode 100644 index 0000000000..8552054381 --- /dev/null +++ b/changelog.d/2145.fixed.md @@ -0,0 +1 @@ +- Based TV licence charges on licensable viewing and corrected the HBAI free-licence value. diff --git a/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/free_tv_licence_value.yaml b/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/free_tv_licence_value.yaml new file mode 100644 index 0000000000..f1dbfc9544 --- /dev/null +++ b/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/free_tv_licence_value.yaml @@ -0,0 +1,101 @@ +- name: Case 1, BBC iPlayer use without television ownership. + period: 2026 + input: + household_owns_tv: false + tv_licence_required: true + tv_licence_free_licence_share: 1 + would_evade_tv_licence_fee: false + output: + free_tv_licence_value: 180 + +- name: Case 2, television ownership without licensable use. + period: 2026 + input: + household_owns_tv: true + tv_licence_required: false + tv_licence_free_licence_share: 1 + would_evade_tv_licence_fee: false + output: + free_tv_licence_value: 0 + +- name: Case 3, full free licence value enters HBAI income in kind. + period: 2026 + input: + tv_licence_required: true + tv_licence_free_licence_share: 1 + would_evade_tv_licence_fee: false + output: + free_tv_licence_value: 180 + +- name: Case 4, blind discount is not a free licence in HBAI income. + period: 2026 + input: + tv_licence_required: true + tv_licence_free_licence_share: 0 + is_blind: true + would_evade_tv_licence_fee: false + output: + tv_licence_discount: 0.5 + free_tv_licence_value: 0 + +- name: Case 5, a partial-year free licence contributes its proportional value. + period: 2020 + absolute_error_margin: 0.01 + input: + tv_licence_required: true + tv_licence_free_licence_share: 0.3205479452 + would_evade_tv_licence_fee: false + output: + free_tv_licence_value: 50.49 + +- name: Case 6, an over-75 household on Pension Credit gets a full free licence. + period: 2026 + input: + age: 80 + pension_credit: 1 + tv_licence_required: true + would_evade_tv_licence_fee: false + output: + tv_licence_free_licence_share: 1 + free_tv_licence_value: 180 + +- name: Case 7, age alone does not qualify after the Pension Credit condition begins. + period: 2026 + input: + age: 80 + pension_credit: 0 + tv_licence_required: true + would_evade_tv_licence_fee: false + output: + tv_licence_free_licence_share: 0 + free_tv_licence_value: 0 + +- name: Case 8, the 2020 transition determines both free value and paid liability. + period: 2020 + absolute_error_margin: 0.01 + input: + age: 80 + pension_credit: 0 + tv_licence_required: true + is_blind: false + would_evade_tv_licence_fee: false + output: + tv_licence_free_licence_share: 0.3205479452 + tv_licence_discount: 0.3205479452 + free_tv_licence_value: 50.49 + tv_licence: 107.01 + +- name: Case 9, the blind discount reduces only the paid part of the 2020 licence. + period: 2020 + absolute_error_margin: 0.01 + input: + age: 80 + pension_credit: 0 + tv_licence_required: true + is_blind: true + would_evade_tv_licence_fee: false + output: + tv_licence_free_licence_share: 0.3205479452 + tv_licence_discount: 0.6602739726 + free_tv_licence_value: 50.49 + tv_licence: 53.51 diff --git a/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml b/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml index 4b62332a7c..7a33fb64d0 100644 --- a/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml +++ b/policyengine_uk/tests/policy/baseline/gov/dcms/bbc/tv-licence/tv_licence.yaml @@ -42,3 +42,44 @@ would_evade_tv_licence_fee: false output: tv_licence: 0 + +- name: TV licence required for BBC iPlayer without television ownership. + period: 2026 + input: + household_owns_tv: false + tv_licence_required: true + tv_licence_discount: 0 + would_evade_tv_licence_fee: false + output: + tv_licence: 180 + +- name: Television ownership alone does not require a TV licence. + period: 2026 + input: + household_owns_tv: true + tv_licence_required: false + tv_licence_discount: 0 + would_evade_tv_licence_fee: false + output: + tv_licence: 0 + +- name: Television ownership remains the fallback for existing inputs. + period: 2026 + input: + household_owns_tv: false + tv_licence_discount: 0 + would_evade_tv_licence_fee: false + output: + tv_licence: 0 + +- name: Blind discount applies only after a partial-year free licence ends. + period: 2020 + absolute_error_margin: 0.01 + input: + tv_licence_required: true + tv_licence_free_licence_share: 0.3205479452 + is_blind: true + would_evade_tv_licence_fee: false + output: + tv_licence_discount: 0.6602739726 + tv_licence: 53.51 diff --git a/policyengine_uk/tests/test_tv_licence_metadata.py b/policyengine_uk/tests/test_tv_licence_metadata.py new file mode 100644 index 0000000000..6c90645b81 --- /dev/null +++ b/policyengine_uk/tests/test_tv_licence_metadata.py @@ -0,0 +1,8 @@ +from policyengine_uk import CountryTaxBenefitSystem +from policyengine_uk.model_api import GBP + + +def test_free_tv_licence_value_uses_currency_units(): + system = CountryTaxBenefitSystem() + + assert system.variables["free_tv_licence_value"].unit == GBP diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/free_tv_licence_value.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/free_tv_licence_value.py index 6626fc9842..9e725b692d 100644 --- a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/free_tv_licence_value.py +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/free_tv_licence_value.py @@ -3,18 +3,29 @@ class free_tv_licence_value(Variable): label = "free TV licence value" + documentation = ( + "Value included in HBAI income for the share of the year covered by " + "the complete age-based exemption. The blind discount is excluded." + ) entity = Household definition_period = YEAR value_type = float - unit = "/1" + unit = GBP reference = ( + "https://www.legislation.gov.uk/ukpga/2003/21/section/363", "https://www.legislation.gov.uk/ukpga/2003/21/section/365A", - "https://www.tvlicensing.co.uk/reducedfee", + "https://www.legislation.gov.uk/uksi/2016/704/regulation/9/made", + "https://www.legislation.gov.uk/uksi/2026/104/made", + "https://www.gov.uk/free-discount-tv-licence", + "https://www.gov.uk/government/statistics/households-below-average-income-for-financial-years-ending-1995-to-2025/households-below-average-income-background-information-and-methodology-report-fye-2025", ) def formula(household, period, parameters): - owns_tv = household("household_owns_tv", period) - discount = household("tv_licence_discount", period) + licence_required = household("tv_licence_required", period) + free_licence_share = household( + "tv_licence_free_licence_share", + period, + ) would_evade = household("would_evade_tv_licence_fee", period) fee = parameters(period).gov.dcms.bbc.tv_licence.colour - return (owns_tv & ~would_evade) * fee * discount + return (licence_required & ~would_evade) * fee * free_licence_share diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence.py index ef0fb57f54..ad6c0eb9e7 100644 --- a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence.py +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence.py @@ -8,12 +8,15 @@ class tv_licence(Variable): definition_period = YEAR value_type = float unit = GBP - reference = "https://www.legislation.gov.uk/ukpga/2003/21/section/363" + reference = ( + "https://www.legislation.gov.uk/ukpga/2003/21/section/363", + "https://www.legislation.gov.uk/uksi/2016/704/regulation/9/made", + ) category = TAX def formula(household, period, parameters): - owns_tv = household("household_owns_tv", period) + licence_required = household("tv_licence_required", period) discount = household("tv_licence_discount", period) would_evade = household("would_evade_tv_licence_fee", period) fee = parameters(period).gov.dcms.bbc.tv_licence.colour - return (owns_tv & ~would_evade) * fee * (1 - discount) + return (licence_required & ~would_evade) * fee * (1 - discount) 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 655b1eca18..c220d08179 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,5 +1,4 @@ from policyengine_uk.model_api import * -from policyengine_uk.utils.excise import fiscal_year_segments class tv_licence_discount(Variable): @@ -17,24 +16,14 @@ class tv_licence_discount(Variable): def formula(household, period, parameters): person = household.members tv_licence = parameters(period).gov.dcms.bbc.tv_licence - - # Aged discount - claims_pc = add(household, period, ["pension_credit"]) > 0 - 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 - ) + free_licence_share = household( + "tv_licence_free_licence_share", + period, + ) # Blind discount is_blind = person("is_blind", period) has_blind = household.any(is_blind) blind_discount = has_blind * tv_licence.discount.blind.discount - return max_(aged_discount, blind_discount) + return free_licence_share + (1 - free_licence_share) * blind_discount diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_free_licence_share.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_free_licence_share.py new file mode 100644 index 0000000000..5be64444cc --- /dev/null +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_free_licence_share.py @@ -0,0 +1,34 @@ +from policyengine_uk.model_api import * +from policyengine_uk.utils.excise import fiscal_year_segments + + +class tv_licence_free_licence_share(Variable): + label = "Share of the year covered by a free TV licence" + documentation = ( + "Share of the year for which the household qualifies for the complete " + "age-based TV licence exemption. This excludes the blind discount." + ) + entity = Household + definition_period = YEAR + value_type = float + unit = "/1" + reference = ( + "https://www.legislation.gov.uk/ukpga/2003/21/section/365A", + "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): + person = household.members + claims_pc = add(household, period, ["pension_credit"]) > 0 + free_licence_share = 0 + for aged_rules, share in fiscal_year_segments( + parameters.gov.dcms.bbc.tv_licence.discount.aged, + period.start.year, + ): + has_aged = household.any(person("age", period) >= aged_rules.min_age) + meets_pc_requirement = not_(aged_rules.must_claim_pc) | claims_pc + free_licence_share += ( + has_aged * meets_pc_requirement * aged_rules.discount * share + ) + return free_licence_share diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_required.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_required.py new file mode 100644 index 0000000000..f7acf05b1d --- /dev/null +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/tv_licence_required.py @@ -0,0 +1,22 @@ +from policyengine_uk.model_api import * + + +class tv_licence_required(Variable): + label = "TV licence required" + documentation = ( + "Whether anyone in the household watches or records live television " + "or uses BBC iPlayer. When this is not provided directly, television " + "ownership is used as a backward-compatible proxy. That proxy can " + "overstate liability for owners who use only non-BBC on-demand services " + "and understate it for non-owners who watch live television or BBC iPlayer." + ) + entity = Household + definition_period = YEAR + value_type = bool + reference = ( + "https://www.legislation.gov.uk/ukpga/2003/21/section/363", + "https://www.legislation.gov.uk/uksi/2016/704/regulation/9/made", + ) + + def formula(household, period, parameters): + return household("household_owns_tv", period) diff --git a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/would_evade_tv_licence_fee.py b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/would_evade_tv_licence_fee.py index 61be081395..7239d88489 100644 --- a/policyengine_uk/variables/gov/dcms/bbc/tv_licence/would_evade_tv_licence_fee.py +++ b/policyengine_uk/variables/gov/dcms/bbc/tv_licence/would_evade_tv_licence_fee.py @@ -5,7 +5,8 @@ class would_evade_tv_licence_fee(Variable): label = "Would evade TV licence fee" documentation = ( "Whether this household would unlawfully evade the TV licence fee. " - "Generated stochastically in the dataset using evasion rates." + "Generated stochastically in the dataset using evasion rates. The " + "realised result does not change in response to reforms of the fee." ) entity = Household definition_period = YEAR