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/2145.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Based TV licence charges on licensable viewing and corrected the HBAI free-licence value.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions policyengine_uk/tests/test_tv_licence_metadata.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from policyengine_uk.model_api import *
from policyengine_uk.utils.excise import fiscal_year_segments


class tv_licence_discount(Variable):
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading