Skip to content

Commit 5537f19

Browse files
committed
Add firm-level VAT implementation
- Create Firm entity with owner role - Add VAT registration thresholds (£90k) and rates - Implement VAT calculation variables for firms - Add comprehensive tests for firm entity and VAT logic Fixes #1320
1 parent 2ec96f1 commit 5537f19

15 files changed

Lines changed: 343 additions & 5 deletions

policyengine_uk/entities.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,19 @@
4343
is_person=True,
4444
)
4545

46-
entities = [Household, BenUnit, Person]
46+
Firm = build_entity(
47+
key="firm",
48+
plural="firms",
49+
label="Firm",
50+
doc="A business entity that may be subject to VAT and other business taxes.",
51+
roles=[
52+
{
53+
"key": "owner",
54+
"plural": "owners",
55+
"label": "Owner",
56+
"doc": "A person who owns or has an interest in the firm.",
57+
}
58+
],
59+
)
60+
61+
entities = [Household, BenUnit, Person, Firm]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description: VAT deregistration threshold - firms can deregister if turnover falls below this
2+
values:
3+
2017-04-01: 83000
4+
2024-04-01: 88000
5+
metadata:
6+
unit: currency-GBP
7+
reference:
8+
- title: HMRC VAT registration thresholds
9+
href: https://www.gov.uk/vat-registration-thresholds
10+
label: VAT deregistration threshold
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
description: VAT registration threshold - firms with taxable turnover above this must register for VAT
2+
values:
3+
2017-04-01: 85000
4+
2024-04-01: 90000
5+
metadata:
6+
unit: currency-GBP
7+
reference:
8+
- title: HMRC VAT registration thresholds
9+
href: https://www.gov.uk/vat-registration-thresholds
10+
label: VAT registration threshold
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
description: Zero-rated VAT (0% rate) applies to essential items like most food, children's clothes, books
2+
values:
3+
1973-04-01: 0.0
4+
metadata:
5+
unit: /1
6+
reference:
7+
- title: HMRC VAT rates
8+
href: https://www.gov.uk/vat-rates
9+
label: VAT zero rate

policyengine_uk/tax_benefit_system.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from policyengine_core.variables import Variable
1515

1616
# PolicyEngine UK imports
17-
from policyengine_uk.entities import BenUnit, Household, Person
17+
from policyengine_uk.entities import BenUnit, Household, Person, Firm
1818
from policyengine_uk.parameters.gov.contrib.create_private_pension_uprating import (
1919
add_private_pension_uprating_factor,
2020
)
@@ -108,16 +108,17 @@ def __init__(self):
108108
self.variables = {}
109109

110110
# Create copies of entity classes to avoid modifying originals
111-
person, benunit, household = (
111+
person, benunit, household, firm = (
112112
copy.copy(Person),
113113
copy.copy(BenUnit),
114114
copy.copy(Household),
115+
copy.copy(Firm),
115116
)
116117

117118
# Set up entities
118-
self.entities = [person, benunit, household]
119+
self.entities = [person, benunit, household, firm]
119120
self.person_entity = person
120-
self.group_entities = [benunit, household]
121+
self.group_entities = [benunit, household, firm]
121122
self.group_entity_keys = [entity.key for entity in self.group_entities]
122123

123124
# Link entities to this tax-benefit system
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import pytest
2+
from policyengine_uk import CountryTaxBenefitSystem
3+
4+
5+
def test_firm_entity_exists():
6+
"""Test that the Firm entity is defined in the tax-benefit system."""
7+
system = CountryTaxBenefitSystem()
8+
9+
# Find Firm entity in the list
10+
firm_entity = None
11+
for entity in system.entities:
12+
if entity.key == "firm":
13+
firm_entity = entity
14+
break
15+
16+
assert firm_entity is not None, "Firm entity not found"
17+
assert firm_entity.key == "firm"
18+
assert firm_entity.plural == "firms"
19+
assert firm_entity.label == "Firm"
20+
21+
22+
def test_firm_entity_has_owner_role():
23+
"""Test that the Firm entity has an owner role linking to Person."""
24+
system = CountryTaxBenefitSystem()
25+
26+
# Find Firm entity
27+
firm_entity = None
28+
for entity in system.entities:
29+
if entity.key == "firm":
30+
firm_entity = entity
31+
break
32+
33+
assert firm_entity is not None, "Firm entity not found"
34+
35+
# Check that the entity has roles
36+
assert len(firm_entity.roles) > 0
37+
38+
# Find the owner role
39+
owner_role = None
40+
for role in firm_entity.roles:
41+
if role.key == "owner":
42+
owner_role = role
43+
break
44+
45+
assert owner_role is not None
46+
assert owner_role.plural == "owners"
47+
assert owner_role.label == "Owner"
48+
49+
50+
def test_firm_entity_documentation():
51+
"""Test that the Firm entity has proper documentation."""
52+
system = CountryTaxBenefitSystem()
53+
54+
# Find Firm entity
55+
firm_entity = None
56+
for entity in system.entities:
57+
if entity.key == "firm":
58+
firm_entity = entity
59+
break
60+
61+
assert firm_entity is not None, "Firm entity not found"
62+
assert firm_entity.doc is not None
63+
assert (
64+
"business" in firm_entity.doc.lower()
65+
or "firm" in firm_entity.doc.lower()
66+
)
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import pytest
2+
import numpy as np
3+
from policyengine_uk import CountryTaxBenefitSystem
4+
5+
6+
def test_firm_turnover_variable():
7+
"""Test that firm_turnover variable exists and works correctly."""
8+
system = CountryTaxBenefitSystem()
9+
10+
# Check variable exists
11+
assert "firm_turnover" in system.variables
12+
13+
variable = system.variables["firm_turnover"]
14+
assert variable.value_type == float
15+
assert variable.entity.key == "firm"
16+
assert variable.label == "Firm turnover"
17+
18+
19+
def test_firm_vat_registered_variable():
20+
"""Test that firm_vat_registered variable exists and calculates correctly."""
21+
system = CountryTaxBenefitSystem()
22+
23+
# Check variable exists
24+
assert "firm_vat_registered" in system.variables
25+
26+
variable = system.variables["firm_vat_registered"]
27+
assert variable.value_type == bool
28+
assert variable.entity.key == "firm"
29+
30+
31+
def test_vat_registration_threshold():
32+
"""Test VAT registration threshold of £90,000."""
33+
from policyengine_uk.simulation import Simulation
34+
35+
# Test firm below threshold
36+
sim_below = Simulation(
37+
situation={
38+
"firms": {"test_firm": {"firm_turnover": {"2024": 85_000}}},
39+
"people": {"p": {}},
40+
"benunits": {"b": {"members": ["p"]}},
41+
"households": {"h": {"members": ["p"]}},
42+
}
43+
)
44+
vat_registered = sim_below.calculate("firm_vat_registered", "2024")
45+
assert vat_registered[0] == False
46+
47+
# Test firm above threshold
48+
sim_above = Simulation(
49+
situation={
50+
"firms": {"test_firm": {"firm_turnover": {"2024": 95_000}}},
51+
"people": {"p": {}},
52+
"benunits": {"b": {"members": ["p"]}},
53+
"households": {"h": {"members": ["p"]}},
54+
}
55+
)
56+
vat_registered = sim_above.calculate("firm_vat_registered", "2024")
57+
assert vat_registered[0] == True
58+
59+
60+
def test_firm_vat_on_sales():
61+
"""Test VAT calculation on firm sales."""
62+
from policyengine_uk.simulation import Simulation
63+
64+
simulation = Simulation(
65+
situation={
66+
"firms": {
67+
"firm_1": {
68+
"firm_turnover": {"2024": 100_000},
69+
"firm_standard_rated_supplies": {"2024": 80_000},
70+
"firm_reduced_rated_supplies": {"2024": 10_000},
71+
"firm_zero_rated_supplies": {"2024": 10_000},
72+
}
73+
},
74+
"people": {"p": {}},
75+
"benunits": {"b": {"members": ["p"]}},
76+
"households": {"h": {"members": ["p"]}},
77+
}
78+
)
79+
80+
vat_on_sales = simulation.calculate("firm_vat_on_sales", "2024")
81+
# Standard rate 20% on £80,000 = £16,000
82+
# Reduced rate 5% on £10,000 = £500
83+
# Zero rate 0% on £10,000 = £0
84+
# Total = £16,500
85+
expected = 16_500
86+
assert np.isclose(vat_on_sales[0], expected, rtol=0.01)
87+
88+
89+
def test_firm_net_vat_liability():
90+
"""Test net VAT liability calculation (output VAT - input VAT)."""
91+
from policyengine_uk.simulation import Simulation
92+
93+
simulation = Simulation(
94+
situation={
95+
"firms": {
96+
"firm_1": {
97+
"firm_turnover": {"2024": 100_000},
98+
"firm_standard_rated_supplies": {"2024": 100_000},
99+
"firm_vat_on_purchases": {"2024": 5_000},
100+
}
101+
},
102+
"people": {"p": {}},
103+
"benunits": {"b": {"members": ["p"]}},
104+
"households": {"h": {"members": ["p"]}},
105+
}
106+
)
107+
108+
net_vat = simulation.calculate("firm_net_vat_liability", "2024")
109+
# Output VAT: 20% of £100,000 = £20,000
110+
# Input VAT: £5,000
111+
# Net liability: £20,000 - £5,000 = £15,000
112+
expected = 15_000
113+
assert np.isclose(net_vat[0], expected, rtol=0.01)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from policyengine_uk.model_api import *
2+
3+
4+
class firm_net_vat_liability(Variable):
5+
value_type = float
6+
entity = Firm
7+
label = "Net VAT liability"
8+
definition_period = YEAR
9+
unit = GBP
10+
documentation = "Net VAT liability (output VAT minus input VAT)"
11+
12+
def formula(firm, period, parameters):
13+
vat_registered = firm("firm_vat_registered", period)
14+
output_vat = firm("firm_vat_on_sales", period)
15+
input_vat = firm("firm_vat_on_purchases", period)
16+
17+
net_vat = output_vat - input_vat
18+
19+
# Only registered firms pay/reclaim VAT
20+
return net_vat * vat_registered
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from policyengine_uk.model_api import *
2+
3+
4+
class firm_reduced_rated_supplies(Variable):
5+
value_type = float
6+
entity = Firm
7+
label = "Reduced-rated supplies"
8+
definition_period = YEAR
9+
unit = GBP
10+
documentation = "Value of firm's supplies subject to reduced VAT rate"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from policyengine_uk.model_api import *
2+
3+
4+
class firm_standard_rated_supplies(Variable):
5+
value_type = float
6+
entity = Firm
7+
label = "Standard-rated supplies"
8+
definition_period = YEAR
9+
unit = GBP
10+
documentation = "Value of firm's supplies subject to standard VAT rate"

0 commit comments

Comments
 (0)