diff --git a/policyengine_uk/entities.py b/policyengine_uk/entities.py index d83ee53b2..d68f16bd2 100644 --- a/policyengine_uk/entities.py +++ b/policyengine_uk/entities.py @@ -43,4 +43,19 @@ is_person=True, ) -entities = [Household, BenUnit, Person] +Firm = build_entity( + key="firm", + plural="firms", + label="Firm", + doc="A business entity that may be subject to VAT and other business taxes.", + roles=[ + { + "key": "owner", + "plural": "owners", + "label": "Owner", + "doc": "A person who owns or has an interest in the firm.", + } + ], +) + +entities = [Household, BenUnit, Person, Firm] diff --git a/policyengine_uk/parameters/gov/hmrc/vat/deregistration_threshold.yaml b/policyengine_uk/parameters/gov/hmrc/vat/deregistration_threshold.yaml new file mode 100644 index 000000000..eb7642b1b --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/deregistration_threshold.yaml @@ -0,0 +1,10 @@ +description: VAT deregistration threshold - firms can deregister if turnover falls below this +values: + 2017-04-01: 83000 + 2024-04-01: 88000 +metadata: + unit: currency-GBP + reference: + - title: HMRC VAT registration thresholds + href: https://www.gov.uk/vat-registration-thresholds + label: VAT deregistration threshold \ No newline at end of file diff --git a/policyengine_uk/parameters/gov/hmrc/vat/registration_threshold.yaml b/policyengine_uk/parameters/gov/hmrc/vat/registration_threshold.yaml new file mode 100644 index 000000000..0d43a234e --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/registration_threshold.yaml @@ -0,0 +1,10 @@ +description: VAT registration threshold - firms with taxable turnover above this must register for VAT +values: + 2017-04-01: 85000 + 2024-04-01: 90000 +metadata: + unit: currency-GBP + reference: + - title: HMRC VAT registration thresholds + href: https://www.gov.uk/vat-registration-thresholds + label: VAT registration threshold \ No newline at end of file diff --git a/policyengine_uk/parameters/gov/hmrc/vat/zero_rate.yaml b/policyengine_uk/parameters/gov/hmrc/vat/zero_rate.yaml new file mode 100644 index 000000000..9a0d22a52 --- /dev/null +++ b/policyengine_uk/parameters/gov/hmrc/vat/zero_rate.yaml @@ -0,0 +1,9 @@ +description: Zero-rated VAT (0% rate) applies to essential items like most food, children's clothes, books +values: + 1973-04-01: 0.0 +metadata: + unit: /1 + reference: + - title: HMRC VAT rates + href: https://www.gov.uk/vat-rates + label: VAT zero rate \ No newline at end of file diff --git a/policyengine_uk/tax_benefit_system.py b/policyengine_uk/tax_benefit_system.py index e76cddbd9..c2c4e95f4 100644 --- a/policyengine_uk/tax_benefit_system.py +++ b/policyengine_uk/tax_benefit_system.py @@ -14,7 +14,7 @@ from policyengine_core.variables import Variable # PolicyEngine UK imports -from policyengine_uk.entities import BenUnit, Household, Person +from policyengine_uk.entities import BenUnit, Household, Person, Firm from policyengine_uk.parameters.gov.contrib.create_private_pension_uprating import ( add_private_pension_uprating_factor, ) @@ -108,16 +108,17 @@ def __init__(self): self.variables = {} # Create copies of entity classes to avoid modifying originals - person, benunit, household = ( + person, benunit, household, firm = ( copy.copy(Person), copy.copy(BenUnit), copy.copy(Household), + copy.copy(Firm), ) # Set up entities - self.entities = [person, benunit, household] + self.entities = [person, benunit, household, firm] self.person_entity = person - self.group_entities = [benunit, household] + self.group_entities = [benunit, household, firm] self.group_entity_keys = [entity.key for entity in self.group_entities] # Link entities to this tax-benefit system diff --git a/policyengine_uk/tests/entities/test_firm_entity.py b/policyengine_uk/tests/entities/test_firm_entity.py new file mode 100644 index 000000000..270858211 --- /dev/null +++ b/policyengine_uk/tests/entities/test_firm_entity.py @@ -0,0 +1,66 @@ +import pytest +from policyengine_uk import CountryTaxBenefitSystem + + +def test_firm_entity_exists(): + """Test that the Firm entity is defined in the tax-benefit system.""" + system = CountryTaxBenefitSystem() + + # Find Firm entity in the list + firm_entity = None + for entity in system.entities: + if entity.key == "firm": + firm_entity = entity + break + + assert firm_entity is not None, "Firm entity not found" + assert firm_entity.key == "firm" + assert firm_entity.plural == "firms" + assert firm_entity.label == "Firm" + + +def test_firm_entity_has_owner_role(): + """Test that the Firm entity has an owner role linking to Person.""" + system = CountryTaxBenefitSystem() + + # Find Firm entity + firm_entity = None + for entity in system.entities: + if entity.key == "firm": + firm_entity = entity + break + + assert firm_entity is not None, "Firm entity not found" + + # Check that the entity has roles + assert len(firm_entity.roles) > 0 + + # Find the owner role + owner_role = None + for role in firm_entity.roles: + if role.key == "owner": + owner_role = role + break + + assert owner_role is not None + assert owner_role.plural == "owners" + assert owner_role.label == "Owner" + + +def test_firm_entity_documentation(): + """Test that the Firm entity has proper documentation.""" + system = CountryTaxBenefitSystem() + + # Find Firm entity + firm_entity = None + for entity in system.entities: + if entity.key == "firm": + firm_entity = entity + break + + assert firm_entity is not None, "Firm entity not found" + assert firm_entity.doc is not None + assert ( + "business" in firm_entity.doc.lower() + or "firm" in firm_entity.doc.lower() + ) diff --git a/policyengine_uk/tests/policy/firm_vat/test_firm_vat.py b/policyengine_uk/tests/policy/firm_vat/test_firm_vat.py new file mode 100644 index 000000000..51a26b540 --- /dev/null +++ b/policyengine_uk/tests/policy/firm_vat/test_firm_vat.py @@ -0,0 +1,113 @@ +import pytest +import numpy as np +from policyengine_uk import CountryTaxBenefitSystem + + +def test_firm_turnover_variable(): + """Test that firm_turnover variable exists and works correctly.""" + system = CountryTaxBenefitSystem() + + # Check variable exists + assert "firm_turnover" in system.variables + + variable = system.variables["firm_turnover"] + assert variable.value_type == float + assert variable.entity.key == "firm" + assert variable.label == "Firm turnover" + + +def test_firm_vat_registered_variable(): + """Test that firm_vat_registered variable exists and calculates correctly.""" + system = CountryTaxBenefitSystem() + + # Check variable exists + assert "firm_vat_registered" in system.variables + + variable = system.variables["firm_vat_registered"] + assert variable.value_type == bool + assert variable.entity.key == "firm" + + +def test_vat_registration_threshold(): + """Test VAT registration threshold of £90,000.""" + from policyengine_uk.simulation import Simulation + + # Test firm below threshold + sim_below = Simulation( + situation={ + "firms": {"test_firm": {"firm_turnover": {"2024": 85_000}}}, + "people": {"p": {}}, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"]}}, + } + ) + vat_registered = sim_below.calculate("firm_vat_registered", "2024") + assert vat_registered[0] == False + + # Test firm above threshold + sim_above = Simulation( + situation={ + "firms": {"test_firm": {"firm_turnover": {"2024": 95_000}}}, + "people": {"p": {}}, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"]}}, + } + ) + vat_registered = sim_above.calculate("firm_vat_registered", "2024") + assert vat_registered[0] == True + + +def test_firm_vat_on_sales(): + """Test VAT calculation on firm sales.""" + from policyengine_uk.simulation import Simulation + + simulation = Simulation( + situation={ + "firms": { + "firm_1": { + "firm_turnover": {"2024": 100_000}, + "firm_standard_rated_supplies": {"2024": 80_000}, + "firm_reduced_rated_supplies": {"2024": 10_000}, + "firm_zero_rated_supplies": {"2024": 10_000}, + } + }, + "people": {"p": {}}, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"]}}, + } + ) + + vat_on_sales = simulation.calculate("firm_vat_on_sales", "2024") + # Standard rate 20% on £80,000 = £16,000 + # Reduced rate 5% on £10,000 = £500 + # Zero rate 0% on £10,000 = £0 + # Total = £16,500 + expected = 16_500 + assert np.isclose(vat_on_sales[0], expected, rtol=0.01) + + +def test_firm_net_vat_liability(): + """Test net VAT liability calculation (output VAT - input VAT).""" + from policyengine_uk.simulation import Simulation + + simulation = Simulation( + situation={ + "firms": { + "firm_1": { + "firm_turnover": {"2024": 100_000}, + "firm_standard_rated_supplies": {"2024": 100_000}, + "firm_vat_on_purchases": {"2024": 5_000}, + } + }, + "people": {"p": {}}, + "benunits": {"b": {"members": ["p"]}}, + "households": {"h": {"members": ["p"]}}, + } + ) + + net_vat = simulation.calculate("firm_net_vat_liability", "2024") + # Output VAT: 20% of £100,000 = £20,000 + # Input VAT: £5,000 + # Net liability: £20,000 - £5,000 = £15,000 + expected = 15_000 + assert np.isclose(net_vat[0], expected, rtol=0.01) diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_net_vat_liability.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_net_vat_liability.py new file mode 100644 index 000000000..1f464b23f --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_net_vat_liability.py @@ -0,0 +1,20 @@ +from policyengine_uk.model_api import * + + +class firm_net_vat_liability(Variable): + value_type = float + entity = Firm + label = "Net VAT liability" + definition_period = YEAR + unit = GBP + documentation = "Net VAT liability (output VAT minus input VAT)" + + def formula(firm, period, parameters): + vat_registered = firm("firm_vat_registered", period) + output_vat = firm("firm_vat_on_sales", period) + input_vat = firm("firm_vat_on_purchases", period) + + net_vat = output_vat - input_vat + + # Only registered firms pay/reclaim VAT + return net_vat * vat_registered diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_reduced_rated_supplies.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_reduced_rated_supplies.py new file mode 100644 index 000000000..94c7fdf57 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_reduced_rated_supplies.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class firm_reduced_rated_supplies(Variable): + value_type = float + entity = Firm + label = "Reduced-rated supplies" + definition_period = YEAR + unit = GBP + documentation = "Value of firm's supplies subject to reduced VAT rate" diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_standard_rated_supplies.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_standard_rated_supplies.py new file mode 100644 index 000000000..33a970534 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_standard_rated_supplies.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class firm_standard_rated_supplies(Variable): + value_type = float + entity = Firm + label = "Standard-rated supplies" + definition_period = YEAR + unit = GBP + documentation = "Value of firm's supplies subject to standard VAT rate" diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_turnover.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_turnover.py new file mode 100644 index 000000000..0ce040e85 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_turnover.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class firm_turnover(Variable): + value_type = float + entity = Firm + label = "Firm turnover" + definition_period = YEAR + unit = GBP + documentation = "Annual turnover of the firm from all business activities" diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_purchases.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_purchases.py new file mode 100644 index 000000000..5cfb645eb --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_purchases.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class firm_vat_on_purchases(Variable): + value_type = float + entity = Firm + label = "VAT on purchases" + definition_period = YEAR + unit = GBP + documentation = "Total VAT paid on firm's purchases (input VAT)" diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_sales.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_sales.py new file mode 100644 index 000000000..104f10df7 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_on_sales.py @@ -0,0 +1,30 @@ +from policyengine_uk.model_api import * + + +class firm_vat_on_sales(Variable): + value_type = float + entity = Firm + label = "VAT on sales" + definition_period = YEAR + unit = GBP + documentation = "Total VAT charged on firm's sales (output VAT)" + + def formula(firm, period, parameters): + vat_registered = firm("firm_vat_registered", period) + + if not vat_registered.any(): + return firm.empty_array() + + vat_params = parameters(period).gov.hmrc.vat + + standard_supplies = firm("firm_standard_rated_supplies", period) + reduced_supplies = firm("firm_reduced_rated_supplies", period) + zero_supplies = firm("firm_zero_rated_supplies", period) + + standard_vat = standard_supplies * vat_params.standard_rate + reduced_vat = reduced_supplies * vat_params.reduced_rate + zero_vat = zero_supplies * vat_params.zero_rate + + total_vat = standard_vat + reduced_vat + zero_vat + + return total_vat * vat_registered diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_registered.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_registered.py new file mode 100644 index 000000000..f022c2083 --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_vat_registered.py @@ -0,0 +1,14 @@ +from policyengine_uk.model_api import * + + +class firm_vat_registered(Variable): + value_type = bool + entity = Firm + label = "Firm is VAT registered" + definition_period = YEAR + documentation = "Whether the firm is registered for VAT" + + def formula(firm, period, parameters): + turnover = firm("firm_turnover", period) + threshold = parameters(period).gov.hmrc.vat.registration_threshold + return turnover > threshold diff --git a/policyengine_uk/variables/gov/hmrc/vat/firm/firm_zero_rated_supplies.py b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_zero_rated_supplies.py new file mode 100644 index 000000000..6c457c0fe --- /dev/null +++ b/policyengine_uk/variables/gov/hmrc/vat/firm/firm_zero_rated_supplies.py @@ -0,0 +1,10 @@ +from policyengine_uk.model_api import * + + +class firm_zero_rated_supplies(Variable): + value_type = float + entity = Firm + label = "Zero-rated supplies" + definition_period = YEAR + unit = GBP + documentation = "Value of firm's supplies subject to zero VAT rate"