e-data 2.0.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- e_data-2.0.0b1.dist-info/METADATA +214 -0
- e_data-2.0.0b1.dist-info/RECORD +21 -0
- e_data-2.0.0b1.dist-info/WHEEL +5 -0
- e_data-2.0.0b1.dist-info/licenses/LICENSE +674 -0
- e_data-2.0.0b1.dist-info/top_level.txt +1 -0
- edata/__init__.py +0 -0
- edata/const.py +56 -0
- edata/helpers.py +312 -0
- edata/tests/__init__.py +0 -0
- edata/tests/connectors/__init__.py +0 -0
- edata/tests/connectors/test_datadis_connector.py +178 -0
- edata/tests/connectors/test_redata_connector.py +29 -0
- edata/tests/services/__init__.py +0 -0
- edata/tests/services/test_billing_service.py +1169 -0
- edata/tests/services/test_consumption_service.py +955 -0
- edata/tests/services/test_contract_service.py +216 -0
- edata/tests/services/test_database_service.py +415 -0
- edata/tests/services/test_maximeter_service.py +83 -0
- edata/tests/services/test_supply_service.py +251 -0
- edata/tests/test_helpers.py +612 -0
- edata/utils.py +164 -0
|
@@ -0,0 +1,1169 @@
|
|
|
1
|
+
"""Tests for BillingService."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from unittest.mock import AsyncMock, Mock, patch
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
import pytest_asyncio
|
|
10
|
+
|
|
11
|
+
from edata.models.pricing import PricingData, PricingRules
|
|
12
|
+
from edata.services.billing import BillingService
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestBillingService:
|
|
16
|
+
"""Test suite for BillingService."""
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def temp_dir(self):
|
|
20
|
+
"""Create a temporary directory for tests."""
|
|
21
|
+
temp_dir = tempfile.mkdtemp()
|
|
22
|
+
yield temp_dir
|
|
23
|
+
shutil.rmtree(temp_dir)
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def mock_redata_connector(self):
|
|
27
|
+
"""Mock REDataConnector for testing."""
|
|
28
|
+
with patch("edata.services.billing.REDataConnector") as mock_connector_class:
|
|
29
|
+
mock_connector = Mock()
|
|
30
|
+
mock_connector_class.return_value = mock_connector
|
|
31
|
+
yield mock_connector, mock_connector_class
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def mock_database_service(self):
|
|
35
|
+
"""Mock DatabaseService for testing."""
|
|
36
|
+
with patch("edata.services.billing.get_database_service") as mock_get_db:
|
|
37
|
+
mock_db = Mock()
|
|
38
|
+
# Hacer que los métodos async retornen AsyncMock
|
|
39
|
+
mock_db.get_pvpc_prices = AsyncMock(return_value=[])
|
|
40
|
+
mock_db.save_pvpc_price = AsyncMock(return_value=Mock())
|
|
41
|
+
mock_db.get_billing = AsyncMock(return_value=[])
|
|
42
|
+
mock_db.save_billing = AsyncMock(return_value=Mock())
|
|
43
|
+
mock_db.get_consumptions = AsyncMock(return_value=[])
|
|
44
|
+
mock_db.get_contracts = AsyncMock(return_value=[])
|
|
45
|
+
mock_db.generate_pricing_config_hash = Mock(return_value="test_hash")
|
|
46
|
+
mock_db.get_latest_pvpc_price = AsyncMock(return_value=None)
|
|
47
|
+
mock_db.get_latest_billing = AsyncMock(return_value=None)
|
|
48
|
+
mock_get_db.return_value = mock_db
|
|
49
|
+
yield mock_db
|
|
50
|
+
|
|
51
|
+
@pytest_asyncio.fixture
|
|
52
|
+
async def billing_service(
|
|
53
|
+
self, temp_dir, mock_redata_connector, mock_database_service
|
|
54
|
+
):
|
|
55
|
+
"""Create a BillingService instance for testing."""
|
|
56
|
+
return BillingService(storage_dir=temp_dir)
|
|
57
|
+
|
|
58
|
+
@pytest.fixture
|
|
59
|
+
def sample_pvpc_prices(self):
|
|
60
|
+
"""Sample PVPC price data for testing."""
|
|
61
|
+
return [
|
|
62
|
+
PricingData(
|
|
63
|
+
datetime=datetime(2024, 6, 17, 10, 0),
|
|
64
|
+
value_eur_kwh=0.12345,
|
|
65
|
+
delta_h=1.0,
|
|
66
|
+
),
|
|
67
|
+
PricingData(
|
|
68
|
+
datetime=datetime(2024, 6, 17, 11, 0),
|
|
69
|
+
value_eur_kwh=0.13456,
|
|
70
|
+
delta_h=1.0,
|
|
71
|
+
),
|
|
72
|
+
PricingData(
|
|
73
|
+
datetime=datetime(2024, 6, 17, 12, 0),
|
|
74
|
+
value_eur_kwh=0.14567,
|
|
75
|
+
delta_h=1.0,
|
|
76
|
+
),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
@pytest.fixture
|
|
80
|
+
def sample_pricing_rules_pvpc(self):
|
|
81
|
+
"""Sample pricing rules for PVPC configuration."""
|
|
82
|
+
return PricingRules(
|
|
83
|
+
p1_kw_year_eur=30.67,
|
|
84
|
+
p2_kw_year_eur=1.42,
|
|
85
|
+
p1_kwh_eur=None, # PVPC
|
|
86
|
+
p2_kwh_eur=None, # PVPC
|
|
87
|
+
p3_kwh_eur=None, # PVPC
|
|
88
|
+
surplus_p1_kwh_eur=0.05,
|
|
89
|
+
surplus_p2_kwh_eur=0.04,
|
|
90
|
+
surplus_p3_kwh_eur=0.03,
|
|
91
|
+
meter_month_eur=0.81,
|
|
92
|
+
market_kw_year_eur=3.11,
|
|
93
|
+
electricity_tax=1.05113,
|
|
94
|
+
iva_tax=1.21,
|
|
95
|
+
energy_formula="electricity_tax * iva_tax * kwh_eur * kwh",
|
|
96
|
+
power_formula="electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24",
|
|
97
|
+
others_formula="iva_tax * meter_month_eur / 30 / 24",
|
|
98
|
+
surplus_formula="electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur",
|
|
99
|
+
main_formula="energy_term + power_term + others_term",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
@pytest.fixture
|
|
103
|
+
def sample_pricing_rules_custom(self):
|
|
104
|
+
"""Sample pricing rules for custom pricing configuration."""
|
|
105
|
+
return PricingRules(
|
|
106
|
+
p1_kw_year_eur=30.67,
|
|
107
|
+
p2_kw_year_eur=1.42,
|
|
108
|
+
p1_kwh_eur=0.15, # Custom prices
|
|
109
|
+
p2_kwh_eur=0.12,
|
|
110
|
+
p3_kwh_eur=0.08,
|
|
111
|
+
surplus_p1_kwh_eur=0.05,
|
|
112
|
+
surplus_p2_kwh_eur=0.04,
|
|
113
|
+
surplus_p3_kwh_eur=0.03,
|
|
114
|
+
meter_month_eur=0.81,
|
|
115
|
+
market_kw_year_eur=3.11,
|
|
116
|
+
electricity_tax=1.05113,
|
|
117
|
+
iva_tax=1.21,
|
|
118
|
+
energy_formula="electricity_tax * iva_tax * kwh_eur * kwh",
|
|
119
|
+
power_formula="electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24",
|
|
120
|
+
others_formula="iva_tax * meter_month_eur / 30 / 24",
|
|
121
|
+
surplus_formula="electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur",
|
|
122
|
+
main_formula="energy_term + power_term + others_term",
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_initialization(
|
|
127
|
+
self, temp_dir, mock_redata_connector, mock_database_service
|
|
128
|
+
):
|
|
129
|
+
"""Test BillingService initialization."""
|
|
130
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
131
|
+
|
|
132
|
+
service = BillingService(storage_dir=temp_dir)
|
|
133
|
+
|
|
134
|
+
# Verify REDataConnector was initialized
|
|
135
|
+
mock_connector_class.assert_called_once()
|
|
136
|
+
|
|
137
|
+
# Verify database service is obtained lazily by calling _get_db_service
|
|
138
|
+
db_service = await service._get_db_service()
|
|
139
|
+
assert db_service is mock_database_service
|
|
140
|
+
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_update_pvpc_prices_success(
|
|
143
|
+
self,
|
|
144
|
+
billing_service,
|
|
145
|
+
mock_redata_connector,
|
|
146
|
+
mock_database_service,
|
|
147
|
+
sample_pvpc_prices,
|
|
148
|
+
):
|
|
149
|
+
"""Test successful PVPC price update."""
|
|
150
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
151
|
+
start_date = datetime(2024, 6, 17, 0, 0)
|
|
152
|
+
end_date = datetime(2024, 6, 17, 23, 59)
|
|
153
|
+
|
|
154
|
+
# Mock REData connector response
|
|
155
|
+
mock_connector.get_realtime_prices = AsyncMock(return_value=sample_pvpc_prices)
|
|
156
|
+
|
|
157
|
+
# Mock database service responses - no existing prices
|
|
158
|
+
mock_database_service.get_pvpc_prices.return_value = []
|
|
159
|
+
|
|
160
|
+
# Execute PVPC update
|
|
161
|
+
result = await billing_service.update_pvpc_prices(
|
|
162
|
+
start_date=start_date, end_date=end_date, is_ceuta_melilla=False
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Verify REData connector was called correctly
|
|
166
|
+
mock_connector.get_realtime_prices.assert_called_once_with(
|
|
167
|
+
dt_from=start_date, dt_to=end_date, is_ceuta_melilla=False
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Verify database service was called for each price
|
|
171
|
+
assert mock_database_service.save_pvpc_price.call_count == len(
|
|
172
|
+
sample_pvpc_prices
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Verify result structure
|
|
176
|
+
assert result["success"] is True
|
|
177
|
+
assert result["region"] == "Peninsula"
|
|
178
|
+
assert result["geo_id"] == 8741
|
|
179
|
+
assert result["stats"]["fetched"] == len(sample_pvpc_prices)
|
|
180
|
+
assert result["stats"]["saved"] == len(sample_pvpc_prices)
|
|
181
|
+
assert result["stats"]["updated"] == 0
|
|
182
|
+
|
|
183
|
+
@patch("edata.utils.get_pvpc_tariff")
|
|
184
|
+
def test_get_custom_prices_success(
|
|
185
|
+
self,
|
|
186
|
+
mock_get_pvpc_tariff,
|
|
187
|
+
billing_service,
|
|
188
|
+
mock_redata_connector,
|
|
189
|
+
mock_database_service,
|
|
190
|
+
sample_pricing_rules_custom,
|
|
191
|
+
):
|
|
192
|
+
"""Test successful custom price calculation."""
|
|
193
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
194
|
+
start_date = datetime(2024, 6, 17, 10, 0) # Monday 10 AM
|
|
195
|
+
end_date = datetime(2024, 6, 17, 13, 0) # Monday 1 PM (3 hours)
|
|
196
|
+
|
|
197
|
+
# Mock tariff calculation to cycle through periods
|
|
198
|
+
mock_get_pvpc_tariff.side_effect = ["p1", "p1", "p1"] # All P1 hours
|
|
199
|
+
|
|
200
|
+
# Execute custom price calculation (not async)
|
|
201
|
+
result = billing_service.get_custom_prices(
|
|
202
|
+
pricing_rules=sample_pricing_rules_custom,
|
|
203
|
+
start_date=start_date,
|
|
204
|
+
end_date=end_date,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Verify tariff function was called for each hour
|
|
208
|
+
assert mock_get_pvpc_tariff.call_count == 3
|
|
209
|
+
|
|
210
|
+
# Verify result structure
|
|
211
|
+
assert len(result) == 3
|
|
212
|
+
assert all(isinstance(price, PricingData) for price in result)
|
|
213
|
+
assert all(
|
|
214
|
+
price.value_eur_kwh == sample_pricing_rules_custom.p1_kwh_eur
|
|
215
|
+
for price in result
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@pytest.mark.asyncio
|
|
219
|
+
async def test_get_stored_pvpc_prices(
|
|
220
|
+
self, billing_service, mock_redata_connector, mock_database_service
|
|
221
|
+
):
|
|
222
|
+
"""Test getting stored PVPC prices from database."""
|
|
223
|
+
start_date = datetime(2024, 6, 17, 0, 0)
|
|
224
|
+
end_date = datetime(2024, 6, 17, 23, 59)
|
|
225
|
+
geo_id = 8741
|
|
226
|
+
|
|
227
|
+
# Mock database service response
|
|
228
|
+
mock_prices = [Mock(), Mock(), Mock()]
|
|
229
|
+
mock_database_service.get_pvpc_prices.return_value = mock_prices
|
|
230
|
+
|
|
231
|
+
# Execute get stored prices
|
|
232
|
+
result = await billing_service.get_stored_pvpc_prices(
|
|
233
|
+
start_date=start_date, end_date=end_date, geo_id=geo_id
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Verify database service was called correctly
|
|
237
|
+
mock_database_service.get_pvpc_prices.assert_called_once_with(
|
|
238
|
+
start_date, end_date, geo_id
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Verify result
|
|
242
|
+
assert result == mock_prices
|
|
243
|
+
|
|
244
|
+
@pytest.mark.asyncio
|
|
245
|
+
async def test_get_prices_pvpc(
|
|
246
|
+
self,
|
|
247
|
+
billing_service,
|
|
248
|
+
mock_redata_connector,
|
|
249
|
+
mock_database_service,
|
|
250
|
+
sample_pricing_rules_pvpc,
|
|
251
|
+
):
|
|
252
|
+
"""Test automatic price retrieval with PVPC configuration."""
|
|
253
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
254
|
+
start_date = datetime(2024, 6, 17, 0, 0)
|
|
255
|
+
end_date = datetime(2024, 6, 17, 23, 59)
|
|
256
|
+
|
|
257
|
+
# Mock stored PVPC prices
|
|
258
|
+
mock_pvpc_prices = [
|
|
259
|
+
Mock(
|
|
260
|
+
datetime=datetime(2024, 6, 17, 10, 0), value_eur_kwh=0.15, delta_h=1.0
|
|
261
|
+
),
|
|
262
|
+
Mock(
|
|
263
|
+
datetime=datetime(2024, 6, 17, 11, 0), value_eur_kwh=0.16, delta_h=1.0
|
|
264
|
+
),
|
|
265
|
+
]
|
|
266
|
+
mock_database_service.get_pvpc_prices.return_value = mock_pvpc_prices
|
|
267
|
+
|
|
268
|
+
# Execute automatic price retrieval with PVPC rules
|
|
269
|
+
result = await billing_service.get_prices(
|
|
270
|
+
pricing_rules=sample_pricing_rules_pvpc,
|
|
271
|
+
start_date=start_date,
|
|
272
|
+
end_date=end_date,
|
|
273
|
+
is_ceuta_melilla=False,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Should call PVPC retrieval
|
|
277
|
+
mock_database_service.get_pvpc_prices.assert_called_once()
|
|
278
|
+
assert len(result) == 2
|
|
279
|
+
assert all(isinstance(price, PricingData) for price in result)
|
|
280
|
+
|
|
281
|
+
@patch("edata.utils.get_pvpc_tariff")
|
|
282
|
+
@pytest.mark.asyncio
|
|
283
|
+
async def test_get_prices_custom(
|
|
284
|
+
self,
|
|
285
|
+
mock_get_pvpc_tariff,
|
|
286
|
+
billing_service,
|
|
287
|
+
mock_redata_connector,
|
|
288
|
+
mock_database_service,
|
|
289
|
+
sample_pricing_rules_custom,
|
|
290
|
+
):
|
|
291
|
+
"""Test automatic price retrieval with custom configuration."""
|
|
292
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
293
|
+
start_date = datetime(2024, 6, 17, 10, 0)
|
|
294
|
+
end_date = datetime(2024, 6, 17, 11, 0)
|
|
295
|
+
|
|
296
|
+
# Mock tariff calculation
|
|
297
|
+
mock_get_pvpc_tariff.return_value = "p1"
|
|
298
|
+
|
|
299
|
+
# Execute automatic price retrieval with custom rules
|
|
300
|
+
result = await billing_service.get_prices(
|
|
301
|
+
pricing_rules=sample_pricing_rules_custom,
|
|
302
|
+
start_date=start_date,
|
|
303
|
+
end_date=end_date,
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
# Should call custom calculation (not database)
|
|
307
|
+
mock_database_service.get_pvpc_prices.assert_not_called()
|
|
308
|
+
assert len(result) == 1
|
|
309
|
+
assert isinstance(result[0], PricingData)
|
|
310
|
+
assert result[0].value_eur_kwh == sample_pricing_rules_custom.p1_kwh_eur
|
|
311
|
+
|
|
312
|
+
@pytest.mark.asyncio
|
|
313
|
+
async def test_get_prices_pvpc_no_data(
|
|
314
|
+
self,
|
|
315
|
+
billing_service,
|
|
316
|
+
mock_redata_connector,
|
|
317
|
+
mock_database_service,
|
|
318
|
+
sample_pricing_rules_pvpc,
|
|
319
|
+
):
|
|
320
|
+
"""Test automatic price retrieval with PVPC configuration but no data."""
|
|
321
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
322
|
+
start_date = datetime(2024, 6, 17, 0, 0)
|
|
323
|
+
end_date = datetime(2024, 6, 17, 23, 59)
|
|
324
|
+
|
|
325
|
+
# Mock no PVPC prices available
|
|
326
|
+
mock_database_service.get_pvpc_prices.return_value = []
|
|
327
|
+
|
|
328
|
+
# Execute automatic price retrieval with PVPC rules
|
|
329
|
+
result = await billing_service.get_prices(
|
|
330
|
+
pricing_rules=sample_pricing_rules_pvpc,
|
|
331
|
+
start_date=start_date,
|
|
332
|
+
end_date=end_date,
|
|
333
|
+
is_ceuta_melilla=False,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Should return None when no data available
|
|
337
|
+
assert result is None
|
|
338
|
+
mock_database_service.get_pvpc_prices.assert_called_once()
|
|
339
|
+
|
|
340
|
+
@pytest.mark.asyncio
|
|
341
|
+
async def test_get_prices_custom_no_prices_defined(
|
|
342
|
+
self, billing_service, mock_redata_connector, mock_database_service
|
|
343
|
+
):
|
|
344
|
+
"""Test automatic price retrieval with custom configuration but no prices defined."""
|
|
345
|
+
from edata.models.pricing import PricingRules
|
|
346
|
+
|
|
347
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
348
|
+
start_date = datetime(2024, 6, 17, 10, 0)
|
|
349
|
+
end_date = datetime(2024, 6, 17, 11, 0)
|
|
350
|
+
|
|
351
|
+
# Create pricing rules with no energy prices defined
|
|
352
|
+
empty_pricing_rules = PricingRules(
|
|
353
|
+
p1_kw_year_eur=30.67,
|
|
354
|
+
p2_kw_year_eur=1.42,
|
|
355
|
+
p1_kwh_eur=None, # No custom prices
|
|
356
|
+
p2_kwh_eur=None,
|
|
357
|
+
p3_kwh_eur=None,
|
|
358
|
+
surplus_p1_kwh_eur=0.05,
|
|
359
|
+
surplus_p2_kwh_eur=0.04,
|
|
360
|
+
surplus_p3_kwh_eur=0.03,
|
|
361
|
+
meter_month_eur=0.81,
|
|
362
|
+
market_kw_year_eur=3.11,
|
|
363
|
+
electricity_tax=1.05113,
|
|
364
|
+
iva_tax=1.21,
|
|
365
|
+
energy_formula="electricity_tax * iva_tax * kwh_eur * kwh",
|
|
366
|
+
power_formula="electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24",
|
|
367
|
+
others_formula="iva_tax * meter_month_eur / 30 / 24",
|
|
368
|
+
surplus_formula="electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur",
|
|
369
|
+
main_formula="energy_term + power_term + others_term",
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Mock empty PVPC prices since rules indicate PVPC usage
|
|
373
|
+
mock_database_service.get_pvpc_prices.return_value = []
|
|
374
|
+
|
|
375
|
+
# Execute automatic price retrieval with empty custom rules
|
|
376
|
+
result = await billing_service.get_prices(
|
|
377
|
+
pricing_rules=empty_pricing_rules, start_date=start_date, end_date=end_date
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Should return None when no PVPC prices available
|
|
381
|
+
assert result is None
|
|
382
|
+
# Should have tried to get PVPC prices since no custom prices are defined
|
|
383
|
+
mock_database_service.get_pvpc_prices.assert_called_once_with(
|
|
384
|
+
start_date, end_date, 8741
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
@pytest.mark.asyncio
|
|
388
|
+
@patch("edata.utils.get_pvpc_tariff")
|
|
389
|
+
async def test_get_cost_calculation(
|
|
390
|
+
self,
|
|
391
|
+
mock_get_pvpc_tariff,
|
|
392
|
+
billing_service,
|
|
393
|
+
mock_redata_connector,
|
|
394
|
+
mock_database_service,
|
|
395
|
+
sample_pricing_rules_custom,
|
|
396
|
+
):
|
|
397
|
+
"""Test cost calculation functionality."""
|
|
398
|
+
from datetime import datetime
|
|
399
|
+
|
|
400
|
+
from edata.models.pricing import PricingAggregated
|
|
401
|
+
|
|
402
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
403
|
+
cups = "ES0123456789012345AB"
|
|
404
|
+
start_date = datetime(2024, 6, 17, 10, 0)
|
|
405
|
+
end_date = datetime(2024, 6, 17, 12, 0) # 2 hours
|
|
406
|
+
|
|
407
|
+
# Mock no existing billing data initially
|
|
408
|
+
mock_database_service.get_billing.return_value = []
|
|
409
|
+
|
|
410
|
+
# Mock the pricing config hash generation
|
|
411
|
+
mock_database_service.generate_pricing_config_hash.return_value = (
|
|
412
|
+
"test_hash_12345678"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Mock consumption data
|
|
416
|
+
mock_consumptions = [
|
|
417
|
+
type(
|
|
418
|
+
"MockConsumption",
|
|
419
|
+
(),
|
|
420
|
+
{
|
|
421
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
422
|
+
"value_kwh": 0.5,
|
|
423
|
+
"surplus_kwh": 0.0,
|
|
424
|
+
},
|
|
425
|
+
)(),
|
|
426
|
+
type(
|
|
427
|
+
"MockConsumption",
|
|
428
|
+
(),
|
|
429
|
+
{
|
|
430
|
+
"datetime": datetime(2024, 6, 17, 11, 0),
|
|
431
|
+
"value_kwh": 0.6,
|
|
432
|
+
"surplus_kwh": 0.0,
|
|
433
|
+
},
|
|
434
|
+
)(),
|
|
435
|
+
]
|
|
436
|
+
mock_database_service.get_consumptions.return_value = mock_consumptions
|
|
437
|
+
|
|
438
|
+
# Mock contract data
|
|
439
|
+
mock_contracts = [
|
|
440
|
+
type(
|
|
441
|
+
"MockContract",
|
|
442
|
+
(),
|
|
443
|
+
{
|
|
444
|
+
"power_p1": 4.0,
|
|
445
|
+
"power_p2": 4.0,
|
|
446
|
+
"date_start": datetime(2024, 6, 17, 0, 0),
|
|
447
|
+
"date_end": datetime(2024, 6, 18, 0, 0),
|
|
448
|
+
},
|
|
449
|
+
)()
|
|
450
|
+
]
|
|
451
|
+
mock_database_service.get_contracts.return_value = mock_contracts
|
|
452
|
+
|
|
453
|
+
# Mock the save_billing method to return a success response
|
|
454
|
+
mock_database_service.save_billing.return_value = type("MockBilling", (), {})()
|
|
455
|
+
|
|
456
|
+
# Mock billing data after calculation
|
|
457
|
+
mock_billing_results = [
|
|
458
|
+
type(
|
|
459
|
+
"MockBilling",
|
|
460
|
+
(),
|
|
461
|
+
{
|
|
462
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
463
|
+
"total_eur": 0.05,
|
|
464
|
+
"energy_term": 0.03,
|
|
465
|
+
"power_term": 0.015,
|
|
466
|
+
"others_term": 0.005,
|
|
467
|
+
"surplus_term": 0.0,
|
|
468
|
+
},
|
|
469
|
+
)(),
|
|
470
|
+
type(
|
|
471
|
+
"MockBilling",
|
|
472
|
+
(),
|
|
473
|
+
{
|
|
474
|
+
"datetime": datetime(2024, 6, 17, 11, 0),
|
|
475
|
+
"total_eur": 0.06,
|
|
476
|
+
"energy_term": 0.036,
|
|
477
|
+
"power_term": 0.015,
|
|
478
|
+
"others_term": 0.005,
|
|
479
|
+
"surplus_term": 0.0,
|
|
480
|
+
},
|
|
481
|
+
)(),
|
|
482
|
+
]
|
|
483
|
+
|
|
484
|
+
# Configure get_billing to return empty first, then billing results after update_missing_costs
|
|
485
|
+
mock_database_service.get_billing.side_effect = [
|
|
486
|
+
[],
|
|
487
|
+
mock_billing_results,
|
|
488
|
+
mock_billing_results,
|
|
489
|
+
]
|
|
490
|
+
|
|
491
|
+
# Mock tariff calculation - need one call per hour in the data
|
|
492
|
+
mock_get_pvpc_tariff.return_value = (
|
|
493
|
+
"p1" # Use return_value instead of side_effect
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
# Execute cost calculation
|
|
497
|
+
result = await billing_service.get_cost(
|
|
498
|
+
cups=cups,
|
|
499
|
+
pricing_rules=sample_pricing_rules_custom,
|
|
500
|
+
start_date=start_date,
|
|
501
|
+
end_date=end_date,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# Validate result aggregation from mocked billing data
|
|
505
|
+
assert isinstance(result, PricingAggregated)
|
|
506
|
+
assert result.datetime == start_date
|
|
507
|
+
assert result.value_eur == 0.11 # 0.05 + 0.06
|
|
508
|
+
assert result.energy_term == 0.066 # 0.03 + 0.036
|
|
509
|
+
assert result.power_term == 0.03 # 0.015 + 0.015
|
|
510
|
+
assert result.others_term == 0.01 # 0.005 + 0.005
|
|
511
|
+
assert result.surplus_term == 0.0
|
|
512
|
+
assert result.delta_h == 2 # 2 billing records
|
|
513
|
+
|
|
514
|
+
# Verify database calls
|
|
515
|
+
mock_database_service.get_consumptions.assert_called_once_with(
|
|
516
|
+
cups, start_date, end_date
|
|
517
|
+
)
|
|
518
|
+
mock_database_service.get_contracts.assert_called_once_with(cups)
|
|
519
|
+
|
|
520
|
+
@pytest.mark.asyncio
|
|
521
|
+
async def test_get_cost_no_consumption_data(
|
|
522
|
+
self,
|
|
523
|
+
billing_service,
|
|
524
|
+
mock_redata_connector,
|
|
525
|
+
mock_database_service,
|
|
526
|
+
sample_pricing_rules_custom,
|
|
527
|
+
):
|
|
528
|
+
"""Test cost calculation with no consumption data."""
|
|
529
|
+
from datetime import datetime
|
|
530
|
+
|
|
531
|
+
from edata.models.pricing import PricingAggregated
|
|
532
|
+
|
|
533
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
534
|
+
cups = "ES0123456789012345AB"
|
|
535
|
+
start_date = datetime(2024, 6, 17, 10, 0)
|
|
536
|
+
end_date = datetime(2024, 6, 17, 12, 0)
|
|
537
|
+
|
|
538
|
+
# Mock no existing billing data initially
|
|
539
|
+
mock_database_service.get_billing.return_value = []
|
|
540
|
+
|
|
541
|
+
# Mock the pricing config hash generation
|
|
542
|
+
mock_database_service.generate_pricing_config_hash.return_value = (
|
|
543
|
+
"test_hash_12345678"
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Mock no consumption data
|
|
547
|
+
mock_database_service.get_consumptions.return_value = []
|
|
548
|
+
|
|
549
|
+
# Execute cost calculation
|
|
550
|
+
result = await billing_service.get_cost(
|
|
551
|
+
cups=cups,
|
|
552
|
+
pricing_rules=sample_pricing_rules_custom,
|
|
553
|
+
start_date=start_date,
|
|
554
|
+
end_date=end_date,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Should return default values when update_missing_costs fails
|
|
558
|
+
assert isinstance(result, PricingAggregated)
|
|
559
|
+
assert result.value_eur == 0.0
|
|
560
|
+
assert result.energy_term == 0.0
|
|
561
|
+
assert result.power_term == 0.0
|
|
562
|
+
assert result.others_term == 0.0
|
|
563
|
+
assert result.surplus_term == 0.0
|
|
564
|
+
assert result.delta_h == 2.0 # (end_date - start_date).total_seconds() / 3600
|
|
565
|
+
|
|
566
|
+
@pytest.mark.asyncio
|
|
567
|
+
async def test_get_cost_no_pricing_data(
|
|
568
|
+
self, billing_service, mock_redata_connector, mock_database_service
|
|
569
|
+
):
|
|
570
|
+
"""Test cost calculation with no pricing data available."""
|
|
571
|
+
from datetime import datetime
|
|
572
|
+
|
|
573
|
+
from edata.models.pricing import PricingAggregated, PricingRules
|
|
574
|
+
|
|
575
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
576
|
+
cups = "ES0123456789012345AB"
|
|
577
|
+
start_date = datetime(2024, 6, 17, 10, 0)
|
|
578
|
+
end_date = datetime(2024, 6, 17, 12, 0)
|
|
579
|
+
|
|
580
|
+
# Mock no existing billing data initially
|
|
581
|
+
mock_database_service.get_billing.return_value = []
|
|
582
|
+
|
|
583
|
+
# Mock the pricing config hash generation
|
|
584
|
+
mock_database_service.generate_pricing_config_hash.return_value = (
|
|
585
|
+
"test_hash_12345678"
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
# Mock consumption data present
|
|
589
|
+
mock_consumptions = [
|
|
590
|
+
type(
|
|
591
|
+
"MockConsumption",
|
|
592
|
+
(),
|
|
593
|
+
{
|
|
594
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
595
|
+
"value_kwh": 0.5,
|
|
596
|
+
"surplus_kwh": 0.0,
|
|
597
|
+
},
|
|
598
|
+
)()
|
|
599
|
+
]
|
|
600
|
+
mock_database_service.get_consumptions.return_value = mock_consumptions
|
|
601
|
+
|
|
602
|
+
# Mock contract data present
|
|
603
|
+
mock_contracts = [
|
|
604
|
+
type(
|
|
605
|
+
"MockContract",
|
|
606
|
+
(),
|
|
607
|
+
{
|
|
608
|
+
"power_p1": 4.0,
|
|
609
|
+
"power_p2": 4.0,
|
|
610
|
+
"date_start": datetime(2024, 6, 17, 0, 0),
|
|
611
|
+
"date_end": datetime(2024, 6, 18, 0, 0),
|
|
612
|
+
},
|
|
613
|
+
)()
|
|
614
|
+
]
|
|
615
|
+
mock_database_service.get_contracts.return_value = mock_contracts
|
|
616
|
+
|
|
617
|
+
# Mock no PVPC prices available
|
|
618
|
+
mock_database_service.get_pvpc_prices.return_value = []
|
|
619
|
+
|
|
620
|
+
# Create PVPC pricing rules
|
|
621
|
+
pvpc_pricing_rules = PricingRules(
|
|
622
|
+
p1_kw_year_eur=30.67,
|
|
623
|
+
p2_kw_year_eur=1.42,
|
|
624
|
+
p1_kwh_eur=None, # PVPC
|
|
625
|
+
p2_kwh_eur=None,
|
|
626
|
+
p3_kwh_eur=None,
|
|
627
|
+
surplus_p1_kwh_eur=0.05,
|
|
628
|
+
surplus_p2_kwh_eur=0.04,
|
|
629
|
+
surplus_p3_kwh_eur=0.03,
|
|
630
|
+
meter_month_eur=0.81,
|
|
631
|
+
market_kw_year_eur=3.11,
|
|
632
|
+
electricity_tax=1.05113,
|
|
633
|
+
iva_tax=1.21,
|
|
634
|
+
energy_formula="electricity_tax * iva_tax * kwh_eur * kwh",
|
|
635
|
+
power_formula="electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24",
|
|
636
|
+
others_formula="iva_tax * meter_month_eur / 30 / 24",
|
|
637
|
+
surplus_formula="electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur",
|
|
638
|
+
main_formula="energy_term + power_term + others_term",
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Execute cost calculation
|
|
642
|
+
result = await billing_service.get_cost(
|
|
643
|
+
cups=cups,
|
|
644
|
+
pricing_rules=pvpc_pricing_rules,
|
|
645
|
+
start_date=start_date,
|
|
646
|
+
end_date=end_date,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# Verify result when no pricing data available
|
|
650
|
+
assert isinstance(result, PricingAggregated)
|
|
651
|
+
assert result.datetime == start_date
|
|
652
|
+
assert result.value_eur == 0.0
|
|
653
|
+
assert result.energy_term == 0.0
|
|
654
|
+
assert result.power_term == 0.0
|
|
655
|
+
assert result.others_term == 0.0
|
|
656
|
+
assert result.surplus_term == 0.0
|
|
657
|
+
|
|
658
|
+
@pytest.mark.asyncio
|
|
659
|
+
async def test_jinja2_formula_evaluation(
|
|
660
|
+
self, billing_service, mock_redata_connector, mock_database_service
|
|
661
|
+
):
|
|
662
|
+
"""Test Jinja2 formula evaluation with predictable values."""
|
|
663
|
+
from datetime import datetime
|
|
664
|
+
|
|
665
|
+
from edata.models.pricing import PricingAggregated, PricingRules
|
|
666
|
+
|
|
667
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
668
|
+
cups = "ES0123456789012345AB"
|
|
669
|
+
start_date = datetime(2024, 6, 17, 10, 0) # P1 period (Monday 10:00)
|
|
670
|
+
end_date = datetime(2024, 6, 17, 11, 0) # 1 hour
|
|
671
|
+
|
|
672
|
+
# Mock no existing billing data initially
|
|
673
|
+
mock_database_service.get_billing.return_value = []
|
|
674
|
+
|
|
675
|
+
# Mock the pricing config hash generation
|
|
676
|
+
mock_database_service.generate_pricing_config_hash.return_value = (
|
|
677
|
+
"test_hash_12345678"
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
# Mock predictable consumption data: 1 kWh consumed, 0.5 kWh surplus
|
|
681
|
+
mock_consumptions = [
|
|
682
|
+
type(
|
|
683
|
+
"MockConsumption",
|
|
684
|
+
(),
|
|
685
|
+
{
|
|
686
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
687
|
+
"value_kwh": 1.0,
|
|
688
|
+
"surplus_kwh": 0.5,
|
|
689
|
+
},
|
|
690
|
+
)()
|
|
691
|
+
]
|
|
692
|
+
mock_database_service.get_consumptions.return_value = mock_consumptions
|
|
693
|
+
|
|
694
|
+
# Mock predictable contract data: 5kW P1, 3kW P2
|
|
695
|
+
mock_contracts = [
|
|
696
|
+
type(
|
|
697
|
+
"MockContract",
|
|
698
|
+
(),
|
|
699
|
+
{
|
|
700
|
+
"power_p1": 5.0,
|
|
701
|
+
"power_p2": 3.0,
|
|
702
|
+
"date_start": datetime(2024, 6, 17, 0, 0),
|
|
703
|
+
"date_end": datetime(2024, 6, 18, 0, 0),
|
|
704
|
+
},
|
|
705
|
+
)()
|
|
706
|
+
]
|
|
707
|
+
mock_database_service.get_contracts.return_value = mock_contracts
|
|
708
|
+
|
|
709
|
+
# Mock predictable PVPC prices: 0.10 €/kWh
|
|
710
|
+
mock_pvpc_prices = [
|
|
711
|
+
type(
|
|
712
|
+
"MockPVPCPrice",
|
|
713
|
+
(),
|
|
714
|
+
{
|
|
715
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
716
|
+
"value_eur_kwh": 0.10,
|
|
717
|
+
"delta_h": 1.0,
|
|
718
|
+
},
|
|
719
|
+
)()
|
|
720
|
+
]
|
|
721
|
+
mock_database_service.get_pvpc_prices.return_value = mock_pvpc_prices
|
|
722
|
+
|
|
723
|
+
# Mock the save_billing method to return a success response
|
|
724
|
+
mock_database_service.save_billing.return_value = type("MockBilling", (), {})()
|
|
725
|
+
|
|
726
|
+
# Mock billing result after calculation (with predictable values)
|
|
727
|
+
# Energy term: 1.05 * 1.21 * 0.10 * 1.0 = 0.12705
|
|
728
|
+
expected_energy_term = 1.05 * 1.21 * 0.10 * 1.0
|
|
729
|
+
# Power term: 1.05 * 1.21 * (5 * (40 + 4) + 3 * 20) / 365 / 24
|
|
730
|
+
expected_power_term = 1.05 * 1.21 * (5 * (40 + 4) + 3 * 20) / 365 / 24
|
|
731
|
+
# Others term: 1.21 * 3.0 / 30 / 24
|
|
732
|
+
expected_others_term = 1.21 * 3.0 / 30 / 24
|
|
733
|
+
# Surplus term: 1.05 * 1.21 * 0.5 * 0.06
|
|
734
|
+
expected_surplus_term = 1.05 * 1.21 * 0.5 * 0.06
|
|
735
|
+
# Total: energy + power + others - surplus
|
|
736
|
+
expected_total = (
|
|
737
|
+
expected_energy_term
|
|
738
|
+
+ expected_power_term
|
|
739
|
+
+ expected_others_term
|
|
740
|
+
- expected_surplus_term
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
mock_billing_result = [
|
|
744
|
+
type(
|
|
745
|
+
"MockBilling",
|
|
746
|
+
(),
|
|
747
|
+
{
|
|
748
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
749
|
+
"total_eur": expected_total,
|
|
750
|
+
"energy_term": expected_energy_term,
|
|
751
|
+
"power_term": expected_power_term,
|
|
752
|
+
"others_term": expected_others_term,
|
|
753
|
+
"surplus_term": expected_surplus_term,
|
|
754
|
+
},
|
|
755
|
+
)()
|
|
756
|
+
]
|
|
757
|
+
|
|
758
|
+
# Configure get_billing to return empty first, then billing results after update_missing_costs
|
|
759
|
+
mock_database_service.get_billing.side_effect = [
|
|
760
|
+
[],
|
|
761
|
+
mock_billing_result,
|
|
762
|
+
mock_billing_result,
|
|
763
|
+
]
|
|
764
|
+
|
|
765
|
+
# Create PVPC pricing rules with simplified formulas for testing
|
|
766
|
+
test_pricing_rules = PricingRules(
|
|
767
|
+
p1_kw_year_eur=40.0, # €40/kW/year
|
|
768
|
+
p2_kw_year_eur=20.0, # €20/kW/year
|
|
769
|
+
p1_kwh_eur=None, # Use PVPC (0.10 €/kWh)
|
|
770
|
+
p2_kwh_eur=None,
|
|
771
|
+
p3_kwh_eur=None,
|
|
772
|
+
surplus_p1_kwh_eur=0.06, # €0.06/kWh surplus in P1
|
|
773
|
+
surplus_p2_kwh_eur=0.04, # €0.04/kWh surplus in P2
|
|
774
|
+
surplus_p3_kwh_eur=0.02, # €0.02/kWh surplus in P3
|
|
775
|
+
meter_month_eur=3.0, # €3/month meter
|
|
776
|
+
market_kw_year_eur=4.0, # €4/kW/year market
|
|
777
|
+
electricity_tax=1.05, # 5% electricity tax
|
|
778
|
+
iva_tax=1.21, # 21% IVA
|
|
779
|
+
# Simplified formulas for predictable calculation
|
|
780
|
+
energy_formula="electricity_tax * iva_tax * kwh_eur * kwh",
|
|
781
|
+
power_formula="electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24",
|
|
782
|
+
others_formula="iva_tax * meter_month_eur / 30 / 24",
|
|
783
|
+
surplus_formula="electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur",
|
|
784
|
+
main_formula="energy_term + power_term + others_term - surplus_term",
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
# Execute cost calculation
|
|
788
|
+
result = await billing_service.get_cost(
|
|
789
|
+
cups=cups,
|
|
790
|
+
pricing_rules=test_pricing_rules,
|
|
791
|
+
start_date=start_date,
|
|
792
|
+
end_date=end_date,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
# Verify the result matches our mocked billing data
|
|
796
|
+
assert isinstance(result, PricingAggregated)
|
|
797
|
+
assert result.datetime == start_date
|
|
798
|
+
assert result.delta_h == 1 # 1 billing record
|
|
799
|
+
|
|
800
|
+
# Verify that the aggregated values match our expected calculations
|
|
801
|
+
assert round(result.energy_term, 4) == round(expected_energy_term, 4)
|
|
802
|
+
assert round(result.power_term, 4) == round(expected_power_term, 4)
|
|
803
|
+
assert round(result.others_term, 4) == round(expected_others_term, 4)
|
|
804
|
+
assert round(result.surplus_term, 4) == round(expected_surplus_term, 4)
|
|
805
|
+
assert round(result.value_eur, 4) == round(expected_total, 4)
|
|
806
|
+
assert round(result.value_eur, 5) == round(expected_total, 5)
|
|
807
|
+
|
|
808
|
+
@pytest.mark.asyncio
|
|
809
|
+
async def test_update_missing_costs(
|
|
810
|
+
self, billing_service, mock_redata_connector, mock_database_service
|
|
811
|
+
):
|
|
812
|
+
"""Test update_missing_costs method."""
|
|
813
|
+
from datetime import datetime
|
|
814
|
+
|
|
815
|
+
from edata.models.pricing import PricingRules
|
|
816
|
+
|
|
817
|
+
mock_connector, mock_connector_class = mock_redata_connector
|
|
818
|
+
cups = "ES0123456789012345AB"
|
|
819
|
+
start_date = datetime(2024, 6, 17, 10, 0)
|
|
820
|
+
end_date = datetime(2024, 6, 17, 12, 0)
|
|
821
|
+
|
|
822
|
+
# Mock consumption data
|
|
823
|
+
mock_consumptions = [
|
|
824
|
+
type(
|
|
825
|
+
"MockConsumption",
|
|
826
|
+
(),
|
|
827
|
+
{
|
|
828
|
+
"datetime": datetime(2024, 6, 17, 10, 0),
|
|
829
|
+
"value_kwh": 0.5,
|
|
830
|
+
"surplus_kwh": 0.0,
|
|
831
|
+
},
|
|
832
|
+
)(),
|
|
833
|
+
type(
|
|
834
|
+
"MockConsumption",
|
|
835
|
+
(),
|
|
836
|
+
{
|
|
837
|
+
"datetime": datetime(2024, 6, 17, 11, 0),
|
|
838
|
+
"value_kwh": 0.7,
|
|
839
|
+
"surplus_kwh": 0.1,
|
|
840
|
+
},
|
|
841
|
+
)(),
|
|
842
|
+
]
|
|
843
|
+
mock_database_service.get_consumptions.return_value = mock_consumptions
|
|
844
|
+
|
|
845
|
+
# Mock contract data
|
|
846
|
+
mock_contracts = [
|
|
847
|
+
type(
|
|
848
|
+
"MockContract",
|
|
849
|
+
(),
|
|
850
|
+
{
|
|
851
|
+
"power_p1": 4.0,
|
|
852
|
+
"power_p2": 4.0,
|
|
853
|
+
"date_start": datetime(2024, 6, 17, 0, 0),
|
|
854
|
+
"date_end": datetime(2024, 6, 18, 0, 0),
|
|
855
|
+
},
|
|
856
|
+
)()
|
|
857
|
+
]
|
|
858
|
+
mock_database_service.get_contracts.return_value = mock_contracts
|
|
859
|
+
|
|
860
|
+
# Mock no existing billing records
|
|
861
|
+
mock_database_service.get_billing.return_value = []
|
|
862
|
+
|
|
863
|
+
# Mock successful billing save
|
|
864
|
+
mock_billing_record = type("MockBilling", (), {"id": 1})()
|
|
865
|
+
mock_database_service.save_billing.return_value = mock_billing_record
|
|
866
|
+
|
|
867
|
+
# Mock hash generation
|
|
868
|
+
mock_database_service.generate_pricing_config_hash.return_value = (
|
|
869
|
+
"test_hash_123"
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Create custom pricing rules (no PVPC)
|
|
873
|
+
custom_pricing_rules = PricingRules(
|
|
874
|
+
p1_kw_year_eur=30.0,
|
|
875
|
+
p2_kw_year_eur=20.0,
|
|
876
|
+
p1_kwh_eur=0.15, # Custom prices - no PVPC
|
|
877
|
+
p2_kwh_eur=0.12,
|
|
878
|
+
p3_kwh_eur=0.10,
|
|
879
|
+
surplus_p1_kwh_eur=0.06,
|
|
880
|
+
surplus_p2_kwh_eur=0.04,
|
|
881
|
+
surplus_p3_kwh_eur=0.02,
|
|
882
|
+
meter_month_eur=3.0,
|
|
883
|
+
market_kw_year_eur=4.0,
|
|
884
|
+
electricity_tax=1.05,
|
|
885
|
+
iva_tax=1.21,
|
|
886
|
+
energy_formula="electricity_tax * iva_tax * kwh_eur * kwh",
|
|
887
|
+
power_formula="electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24",
|
|
888
|
+
others_formula="iva_tax * meter_month_eur / 30 / 24",
|
|
889
|
+
surplus_formula="electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur",
|
|
890
|
+
main_formula="energy_term + power_term + others_term - surplus_term",
|
|
891
|
+
)
|
|
892
|
+
|
|
893
|
+
# Execute update_missing_costs
|
|
894
|
+
result = await billing_service.update_missing_costs(
|
|
895
|
+
cups=cups,
|
|
896
|
+
pricing_rules=custom_pricing_rules,
|
|
897
|
+
start_date=start_date,
|
|
898
|
+
end_date=end_date,
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
# Verify successful result
|
|
902
|
+
assert result["success"] is True
|
|
903
|
+
assert result["cups"] == cups
|
|
904
|
+
assert result["pricing_config_hash"] == "test_hash_123"
|
|
905
|
+
|
|
906
|
+
# Verify statistics
|
|
907
|
+
stats = result["stats"]
|
|
908
|
+
assert stats["total_consumptions"] == 2
|
|
909
|
+
assert stats["processed"] > 0 # Should have processed some records
|
|
910
|
+
|
|
911
|
+
# Verify database methods were called
|
|
912
|
+
mock_database_service.get_consumptions.assert_called_once_with(
|
|
913
|
+
cups, start_date, end_date
|
|
914
|
+
)
|
|
915
|
+
mock_database_service.get_contracts.assert_called_once_with(cups)
|
|
916
|
+
mock_database_service.get_billing.assert_called_once()
|
|
917
|
+
mock_database_service.generate_pricing_config_hash.assert_called_once()
|
|
918
|
+
|
|
919
|
+
# Verify save_billing was called (at least once)
|
|
920
|
+
assert mock_database_service.save_billing.call_count > 0
|
|
921
|
+
|
|
922
|
+
@pytest.mark.asyncio
|
|
923
|
+
async def test_get_daily_costs_with_existing_data(
|
|
924
|
+
self, billing_service, mock_database_service, sample_pricing_rules_custom
|
|
925
|
+
):
|
|
926
|
+
"""Test get_daily_costs with existing billing data."""
|
|
927
|
+
from edata.models.database import BillingModel
|
|
928
|
+
|
|
929
|
+
# Create mock billing records for 2 days
|
|
930
|
+
base_date = datetime(2024, 1, 1, 0, 0, 0)
|
|
931
|
+
mock_billing_records = []
|
|
932
|
+
|
|
933
|
+
# Create 48 hours of billing data (2 days)
|
|
934
|
+
for hour in range(48):
|
|
935
|
+
record = BillingModel(
|
|
936
|
+
datetime=base_date + timedelta(hours=hour),
|
|
937
|
+
cups="ES0012345678901234567890AB",
|
|
938
|
+
pricing_config_hash="test_hash",
|
|
939
|
+
total_eur=1.5 + (hour * 0.1), # Varying costs
|
|
940
|
+
energy_term=1.0 + (hour * 0.05),
|
|
941
|
+
power_term=0.3,
|
|
942
|
+
others_term=0.1,
|
|
943
|
+
surplus_term=0.1 + (hour * 0.05),
|
|
944
|
+
)
|
|
945
|
+
mock_billing_records.append(record)
|
|
946
|
+
|
|
947
|
+
# Setup mocks
|
|
948
|
+
mock_database_service.generate_pricing_config_hash.return_value = "test_hash"
|
|
949
|
+
mock_database_service.get_billing.return_value = mock_billing_records
|
|
950
|
+
|
|
951
|
+
# Test parameters
|
|
952
|
+
cups = "ES0012345678901234567890AB"
|
|
953
|
+
start_date = datetime(2024, 1, 1)
|
|
954
|
+
end_date = datetime(2024, 1, 2, 23, 59, 59)
|
|
955
|
+
|
|
956
|
+
# Call method
|
|
957
|
+
result = await billing_service.get_daily_costs(
|
|
958
|
+
cups, sample_pricing_rules_custom, start_date, end_date
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
# Assertions
|
|
962
|
+
assert len(result) == 2 # Two days
|
|
963
|
+
from edata.models.pricing import PricingAggregated
|
|
964
|
+
|
|
965
|
+
assert all(isinstance(item, PricingAggregated) for item in result)
|
|
966
|
+
|
|
967
|
+
# Check first day
|
|
968
|
+
first_day = result[0]
|
|
969
|
+
assert first_day.datetime.date() == datetime(2024, 1, 1).date()
|
|
970
|
+
assert first_day.delta_h == 24 # 24 hours
|
|
971
|
+
assert first_day.value_eur > 0
|
|
972
|
+
|
|
973
|
+
# Check second day
|
|
974
|
+
second_day = result[1]
|
|
975
|
+
assert second_day.datetime.date() == datetime(2024, 1, 2).date()
|
|
976
|
+
assert second_day.delta_h == 24 # 24 hours
|
|
977
|
+
assert (
|
|
978
|
+
second_day.value_eur > first_day.value_eur
|
|
979
|
+
) # Should be higher due to increasing pattern
|
|
980
|
+
|
|
981
|
+
@pytest.mark.asyncio
|
|
982
|
+
async def test_get_daily_costs_without_existing_data(
|
|
983
|
+
self, billing_service, mock_database_service, sample_pricing_rules_custom
|
|
984
|
+
):
|
|
985
|
+
"""Test get_daily_costs when no billing data exists."""
|
|
986
|
+
# Setup mocks - no existing data
|
|
987
|
+
mock_database_service.generate_pricing_config_hash.return_value = "test_hash"
|
|
988
|
+
mock_database_service.get_billing.side_effect = [
|
|
989
|
+
[],
|
|
990
|
+
[],
|
|
991
|
+
] # First call empty, second still empty
|
|
992
|
+
|
|
993
|
+
# Mock update_missing_costs to fail
|
|
994
|
+
with patch.object(billing_service, "update_missing_costs") as mock_update:
|
|
995
|
+
mock_update.return_value = {"success": False, "error": "Test error"}
|
|
996
|
+
|
|
997
|
+
# Test parameters
|
|
998
|
+
cups = "ES0012345678901234567890AB"
|
|
999
|
+
start_date = datetime(2024, 1, 1)
|
|
1000
|
+
end_date = datetime(2024, 1, 1, 23, 59, 59)
|
|
1001
|
+
|
|
1002
|
+
# Call method
|
|
1003
|
+
result = await billing_service.get_daily_costs(
|
|
1004
|
+
cups, sample_pricing_rules_custom, start_date, end_date
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
# Assertions
|
|
1008
|
+
assert result == [] # Should return empty list when update fails
|
|
1009
|
+
mock_update.assert_called_once()
|
|
1010
|
+
|
|
1011
|
+
@pytest.mark.asyncio
|
|
1012
|
+
async def test_get_monthly_costs_with_existing_data(
|
|
1013
|
+
self, billing_service, mock_database_service, sample_pricing_rules_custom
|
|
1014
|
+
):
|
|
1015
|
+
"""Test get_monthly_costs with existing billing data."""
|
|
1016
|
+
from edata.models.database import BillingModel
|
|
1017
|
+
|
|
1018
|
+
# Create mock billing records for 2 days in same month
|
|
1019
|
+
base_date = datetime(2024, 1, 1, 0, 0, 0)
|
|
1020
|
+
mock_billing_records = []
|
|
1021
|
+
|
|
1022
|
+
# Create 48 hours of billing data (2 days)
|
|
1023
|
+
for hour in range(48):
|
|
1024
|
+
record = BillingModel(
|
|
1025
|
+
datetime=base_date + timedelta(hours=hour),
|
|
1026
|
+
cups="ES0012345678901234567890AB",
|
|
1027
|
+
pricing_config_hash="test_hash",
|
|
1028
|
+
total_eur=1.5 + (hour * 0.1), # Varying costs
|
|
1029
|
+
energy_term=1.0 + (hour * 0.05),
|
|
1030
|
+
power_term=0.3,
|
|
1031
|
+
others_term=0.1,
|
|
1032
|
+
surplus_term=0.1 + (hour * 0.05),
|
|
1033
|
+
)
|
|
1034
|
+
mock_billing_records.append(record)
|
|
1035
|
+
|
|
1036
|
+
# Setup mocks
|
|
1037
|
+
mock_database_service.generate_pricing_config_hash.return_value = "test_hash"
|
|
1038
|
+
mock_database_service.get_billing.return_value = mock_billing_records
|
|
1039
|
+
|
|
1040
|
+
# Test parameters
|
|
1041
|
+
cups = "ES0012345678901234567890AB"
|
|
1042
|
+
start_date = datetime(2024, 1, 1)
|
|
1043
|
+
end_date = datetime(2024, 1, 31, 23, 59, 59)
|
|
1044
|
+
|
|
1045
|
+
# Call method
|
|
1046
|
+
result = await billing_service.get_monthly_costs(
|
|
1047
|
+
cups, sample_pricing_rules_custom, start_date, end_date
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
# Assertions
|
|
1051
|
+
assert len(result) == 1 # One month
|
|
1052
|
+
from edata.models.pricing import PricingAggregated
|
|
1053
|
+
|
|
1054
|
+
assert all(isinstance(item, PricingAggregated) for item in result)
|
|
1055
|
+
|
|
1056
|
+
# Check month
|
|
1057
|
+
month_data = result[0]
|
|
1058
|
+
assert month_data.datetime.date() == datetime(2024, 1, 1).date()
|
|
1059
|
+
assert month_data.delta_h == 48 # 48 hours from mock data
|
|
1060
|
+
assert month_data.value_eur > 0
|
|
1061
|
+
|
|
1062
|
+
@pytest.mark.asyncio
|
|
1063
|
+
async def test_get_monthly_costs_multiple_months(
|
|
1064
|
+
self, billing_service, mock_database_service, sample_pricing_rules_custom
|
|
1065
|
+
):
|
|
1066
|
+
"""Test get_monthly_costs with data spanning multiple months."""
|
|
1067
|
+
from edata.models.database import BillingModel
|
|
1068
|
+
|
|
1069
|
+
# Create billing records spanning two months
|
|
1070
|
+
records = []
|
|
1071
|
+
|
|
1072
|
+
# January data (24 hours)
|
|
1073
|
+
for hour in range(24):
|
|
1074
|
+
record = BillingModel(
|
|
1075
|
+
datetime=datetime(2024, 1, 15) + timedelta(hours=hour),
|
|
1076
|
+
cups="ES0012345678901234567890AB",
|
|
1077
|
+
pricing_config_hash="test_hash",
|
|
1078
|
+
total_eur=1.0,
|
|
1079
|
+
energy_term=0.7,
|
|
1080
|
+
power_term=0.2,
|
|
1081
|
+
others_term=0.1,
|
|
1082
|
+
surplus_term=0.0,
|
|
1083
|
+
)
|
|
1084
|
+
records.append(record)
|
|
1085
|
+
|
|
1086
|
+
# February data (24 hours)
|
|
1087
|
+
for hour in range(24):
|
|
1088
|
+
record = BillingModel(
|
|
1089
|
+
datetime=datetime(2024, 2, 15) + timedelta(hours=hour),
|
|
1090
|
+
cups="ES0012345678901234567890AB",
|
|
1091
|
+
pricing_config_hash="test_hash",
|
|
1092
|
+
total_eur=1.2,
|
|
1093
|
+
energy_term=0.8,
|
|
1094
|
+
power_term=0.3,
|
|
1095
|
+
others_term=0.1,
|
|
1096
|
+
surplus_term=0.0,
|
|
1097
|
+
)
|
|
1098
|
+
records.append(record)
|
|
1099
|
+
|
|
1100
|
+
# Setup mocks
|
|
1101
|
+
mock_database_service.generate_pricing_config_hash.return_value = "test_hash"
|
|
1102
|
+
mock_database_service.get_billing.return_value = records
|
|
1103
|
+
|
|
1104
|
+
# Test parameters
|
|
1105
|
+
cups = "ES0012345678901234567890AB"
|
|
1106
|
+
start_date = datetime(2024, 1, 1)
|
|
1107
|
+
end_date = datetime(2024, 2, 28, 23, 59, 59)
|
|
1108
|
+
|
|
1109
|
+
# Call method
|
|
1110
|
+
result = await billing_service.get_monthly_costs(
|
|
1111
|
+
cups, sample_pricing_rules_custom, start_date, end_date
|
|
1112
|
+
)
|
|
1113
|
+
|
|
1114
|
+
# Assertions
|
|
1115
|
+
assert len(result) == 2 # Two months
|
|
1116
|
+
|
|
1117
|
+
# Check January
|
|
1118
|
+
jan_data = result[0]
|
|
1119
|
+
assert jan_data.datetime.date() == datetime(2024, 1, 1).date()
|
|
1120
|
+
assert jan_data.delta_h == 24
|
|
1121
|
+
assert jan_data.value_eur == 24.0 # 24 hours * 1.0 EUR
|
|
1122
|
+
|
|
1123
|
+
# Check February
|
|
1124
|
+
feb_data = result[1]
|
|
1125
|
+
assert feb_data.datetime.date() == datetime(2024, 2, 1).date()
|
|
1126
|
+
assert feb_data.delta_h == 24
|
|
1127
|
+
assert feb_data.value_eur == 28.8 # 24 hours * 1.2 EUR
|
|
1128
|
+
|
|
1129
|
+
@pytest.mark.asyncio
|
|
1130
|
+
async def test_get_daily_costs_error_handling(
|
|
1131
|
+
self, billing_service, mock_database_service, sample_pricing_rules_custom
|
|
1132
|
+
):
|
|
1133
|
+
"""Test error handling in get_daily_costs."""
|
|
1134
|
+
# Setup mocks to raise exception
|
|
1135
|
+
mock_database_service.generate_pricing_config_hash.side_effect = Exception(
|
|
1136
|
+
"Database error"
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
# Test parameters
|
|
1140
|
+
cups = "ES0012345678901234567890AB"
|
|
1141
|
+
start_date = datetime(2024, 1, 1)
|
|
1142
|
+
end_date = datetime(2024, 1, 1, 23, 59, 59)
|
|
1143
|
+
|
|
1144
|
+
# Call method and expect exception
|
|
1145
|
+
with pytest.raises(Exception, match="Database error"):
|
|
1146
|
+
await billing_service.get_daily_costs(
|
|
1147
|
+
cups, sample_pricing_rules_custom, start_date, end_date
|
|
1148
|
+
)
|
|
1149
|
+
|
|
1150
|
+
@pytest.mark.asyncio
|
|
1151
|
+
async def test_get_monthly_costs_error_handling(
|
|
1152
|
+
self, billing_service, mock_database_service, sample_pricing_rules_custom
|
|
1153
|
+
):
|
|
1154
|
+
"""Test error handling in get_monthly_costs."""
|
|
1155
|
+
# Setup mocks to raise exception
|
|
1156
|
+
mock_database_service.generate_pricing_config_hash.side_effect = Exception(
|
|
1157
|
+
"Database error"
|
|
1158
|
+
)
|
|
1159
|
+
|
|
1160
|
+
# Test parameters
|
|
1161
|
+
cups = "ES0012345678901234567890AB"
|
|
1162
|
+
start_date = datetime(2024, 1, 1)
|
|
1163
|
+
end_date = datetime(2024, 1, 31, 23, 59, 59)
|
|
1164
|
+
|
|
1165
|
+
# Call method and expect exception
|
|
1166
|
+
with pytest.raises(Exception, match="Database error"):
|
|
1167
|
+
await billing_service.get_monthly_costs(
|
|
1168
|
+
cups, sample_pricing_rules_custom, start_date, end_date
|
|
1169
|
+
)
|