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.
@@ -0,0 +1,955 @@
1
+ """Tests for ConsumptionService."""
2
+
3
+ import shutil
4
+ import tempfile
5
+ from datetime import date, datetime, timedelta
6
+ from unittest.mock import AsyncMock, Mock, patch
7
+
8
+ import pytest
9
+ import pytest_asyncio
10
+
11
+ from edata.connectors.datadis import DatadisConnector
12
+ from edata.models.consumption import Consumption, ConsumptionAggregated
13
+ from edata.services.consumption import ConsumptionService
14
+
15
+
16
+ class TestConsumptionService:
17
+ """Test suite for ConsumptionService."""
18
+
19
+ @pytest.fixture
20
+ def temp_dir(self):
21
+ """Create a temporary directory for tests."""
22
+ temp_dir = tempfile.mkdtemp()
23
+ yield temp_dir
24
+ shutil.rmtree(temp_dir)
25
+
26
+ @pytest.fixture
27
+ def mock_datadis_connector(self):
28
+ """Mock DatadisConnector for testing."""
29
+ with patch(
30
+ "edata.services.consumption.DatadisConnector"
31
+ ) as mock_connector_class:
32
+ mock_connector = Mock(spec=DatadisConnector)
33
+ mock_connector_class.return_value = mock_connector
34
+ yield mock_connector, mock_connector_class
35
+
36
+ @pytest.fixture
37
+ def mock_database_service(self):
38
+ """Mock DatabaseService for testing."""
39
+ with patch("edata.services.consumption.get_database_service") as mock_get_db:
40
+ mock_db = Mock()
41
+ # Hacer que los métodos async retornen AsyncMock
42
+ mock_db.get_consumptions = AsyncMock(return_value=[])
43
+ mock_db.save_consumption = AsyncMock(return_value=Mock())
44
+ mock_db.get_latest_consumption = AsyncMock(return_value=None)
45
+ mock_get_db.return_value = mock_db
46
+ yield mock_db
47
+
48
+ @pytest_asyncio.fixture
49
+ async def consumption_service(
50
+ self, temp_dir, mock_datadis_connector, mock_database_service
51
+ ):
52
+ """Create a ConsumptionService instance for testing."""
53
+ mock_connector, mock_connector_class = mock_datadis_connector
54
+ return ConsumptionService(
55
+ datadis_connector=mock_connector,
56
+ storage_dir=temp_dir,
57
+ )
58
+
59
+ @pytest.fixture
60
+ def sample_consumptions(self):
61
+ """Sample consumption data for testing."""
62
+ return [
63
+ Consumption(
64
+ datetime=datetime(2024, 6, 15, 10, 0),
65
+ delta_h=1.0,
66
+ value_kwh=0.5,
67
+ surplus_kwh=0.0,
68
+ real=True,
69
+ ),
70
+ Consumption(
71
+ datetime=datetime(2024, 6, 15, 11, 0),
72
+ delta_h=1.0,
73
+ value_kwh=0.7,
74
+ surplus_kwh=0.0,
75
+ real=True,
76
+ ),
77
+ Consumption(
78
+ datetime=datetime(2024, 6, 15, 12, 0),
79
+ delta_h=1.0,
80
+ value_kwh=0.6,
81
+ surplus_kwh=0.0,
82
+ real=True,
83
+ ),
84
+ ]
85
+
86
+ @pytest.mark.asyncio
87
+ async def test_initialization(
88
+ self, temp_dir, mock_datadis_connector, mock_database_service
89
+ ):
90
+ """Test ConsumptionService initialization."""
91
+ mock_connector, mock_connector_class = mock_datadis_connector
92
+
93
+ service = ConsumptionService(
94
+ datadis_connector=mock_connector,
95
+ storage_dir=temp_dir,
96
+ )
97
+
98
+ # Verify service stores the connector and storage directory
99
+ assert service._datadis == mock_connector
100
+ assert service._storage_dir == temp_dir
101
+
102
+ # Verify database service is obtained lazily by calling _get_db_service
103
+ db_service = await service._get_db_service()
104
+ assert db_service is mock_database_service
105
+
106
+ @pytest.mark.asyncio
107
+ async def test_update_consumptions_success(
108
+ self,
109
+ consumption_service,
110
+ mock_datadis_connector,
111
+ mock_database_service,
112
+ sample_consumptions,
113
+ ):
114
+ """Test successful consumption update."""
115
+ mock_connector, mock_connector_class = mock_datadis_connector
116
+ cups = "ES1234567890123456789"
117
+ distributor_code = "123"
118
+ start_date = datetime(2024, 6, 15, 0, 0)
119
+ end_date = datetime(2024, 6, 15, 23, 59)
120
+
121
+ # Mock datadis connector response (now returns Pydantic models)
122
+ mock_connector.get_consumption_data.return_value = sample_consumptions
123
+
124
+ # Mock database service responses - no existing consumptions
125
+ mock_database_service.get_consumptions.return_value = []
126
+
127
+ # Execute update
128
+ result = await consumption_service.update_consumptions(
129
+ cups=cups,
130
+ distributor_code=distributor_code,
131
+ start_date=start_date,
132
+ end_date=end_date,
133
+ )
134
+
135
+ # Verify datadis connector was called correctly
136
+ mock_connector.get_consumption_data.assert_called_once_with(
137
+ cups=cups,
138
+ distributor_code=distributor_code,
139
+ start_date=start_date,
140
+ end_date=end_date,
141
+ measurement_type="0",
142
+ point_type=5,
143
+ authorized_nif=None,
144
+ )
145
+
146
+ # Verify database service was called for each consumption
147
+ assert mock_database_service.save_consumption.call_count == len(
148
+ sample_consumptions
149
+ )
150
+
151
+ # Verify result structure
152
+ assert result["success"] is True
153
+ assert result["cups"] == cups
154
+ assert result["period"]["start"] == start_date.isoformat()
155
+ assert result["period"]["end"] == end_date.isoformat()
156
+ assert result["stats"]["fetched"] == len(sample_consumptions)
157
+ assert result["stats"]["saved"] == len(sample_consumptions)
158
+ assert result["stats"]["updated"] == 0
159
+
160
+ @pytest.mark.asyncio
161
+ async def test_update_consumptions_with_existing_data(
162
+ self,
163
+ consumption_service,
164
+ mock_datadis_connector,
165
+ mock_database_service,
166
+ sample_consumptions,
167
+ ):
168
+ """Test consumption update with some existing data."""
169
+ mock_connector, mock_connector_class = mock_datadis_connector
170
+ cups = "ES1234567890123456789"
171
+ distributor_code = "123"
172
+ start_date = datetime(2024, 6, 15, 0, 0)
173
+ end_date = datetime(2024, 6, 15, 23, 59)
174
+
175
+ # Mock datadis connector response (now returns Pydantic models)
176
+ mock_connector.get_consumption_data.return_value = sample_consumptions
177
+
178
+ # Mock get_latest_consumption to return an existing consumption before the start date
179
+ mock_latest = Mock()
180
+ mock_latest.datetime = datetime(2024, 6, 14, 23, 0) # Day before start_date
181
+ mock_database_service.get_latest_consumption.return_value = mock_latest
182
+
183
+ # Mock database service responses - first consumption exists, others don't
184
+ def mock_get_consumptions(cups, start_date, end_date):
185
+ if start_date == sample_consumptions[0].datetime:
186
+ return [Mock()] # Existing consumption
187
+ return [] # No existing consumption
188
+
189
+ mock_database_service.get_consumptions.side_effect = mock_get_consumptions
190
+
191
+ # Execute update
192
+ result = await consumption_service.update_consumptions(
193
+ cups=cups,
194
+ distributor_code=distributor_code,
195
+ start_date=start_date,
196
+ end_date=end_date,
197
+ )
198
+
199
+ # Verify result
200
+ assert result["success"] is True
201
+ assert result["stats"]["fetched"] == len(sample_consumptions)
202
+ assert result["stats"]["saved"] == 2 # Two new consumptions
203
+ assert result["stats"]["updated"] == 1 # One updated consumption
204
+
205
+ @pytest.mark.asyncio
206
+ async def test_update_consumptions_with_optional_parameters(
207
+ self,
208
+ consumption_service,
209
+ mock_datadis_connector,
210
+ mock_database_service,
211
+ sample_consumptions,
212
+ ):
213
+ """Test consumption update with optional parameters."""
214
+ mock_connector, mock_connector_class = mock_datadis_connector
215
+ cups = "ES1234567890123456789"
216
+ distributor_code = "123"
217
+ start_date = datetime(2024, 6, 15, 0, 0)
218
+ end_date = datetime(2024, 6, 15, 23, 59)
219
+ measurement_type = "1"
220
+ point_type = 3
221
+ authorized_nif = "12345678A"
222
+
223
+ # Mock datadis connector response (now returns Pydantic models)
224
+ mock_connector.get_consumption_data.return_value = sample_consumptions
225
+ mock_database_service.get_consumptions.return_value = []
226
+
227
+ # Execute update with optional parameters
228
+ result = await consumption_service.update_consumptions(
229
+ cups=cups,
230
+ distributor_code=distributor_code,
231
+ start_date=start_date,
232
+ end_date=end_date,
233
+ measurement_type=measurement_type,
234
+ point_type=point_type,
235
+ authorized_nif=authorized_nif,
236
+ )
237
+
238
+ # Verify datadis connector was called with optional parameters
239
+ mock_connector.get_consumption_data.assert_called_once_with(
240
+ cups=cups,
241
+ distributor_code=distributor_code,
242
+ start_date=start_date,
243
+ end_date=end_date,
244
+ measurement_type=measurement_type,
245
+ point_type=point_type,
246
+ authorized_nif=authorized_nif,
247
+ )
248
+
249
+ assert result["success"] is True
250
+
251
+ @pytest.mark.asyncio
252
+ async def test_update_consumptions_error_handling(
253
+ self, consumption_service, mock_datadis_connector, mock_database_service
254
+ ):
255
+ """Test consumption update error handling."""
256
+ mock_connector, mock_connector_class = mock_datadis_connector
257
+ cups = "ES1234567890123456789"
258
+ distributor_code = "123"
259
+ start_date = datetime(2024, 6, 15, 0, 0)
260
+ end_date = datetime(2024, 6, 15, 23, 59)
261
+
262
+ # Mock datadis connector to raise an exception
263
+ error_message = "API connection failed"
264
+ mock_connector.get_consumption_data.side_effect = Exception(error_message)
265
+
266
+ # Mock database service to return None for get_latest_consumption (no existing data)
267
+ mock_database_service.get_latest_consumption.return_value = None
268
+
269
+ # Execute update
270
+ result = await consumption_service.update_consumptions(
271
+ cups=cups,
272
+ distributor_code=distributor_code,
273
+ start_date=start_date,
274
+ end_date=end_date,
275
+ )
276
+
277
+ # Verify error result
278
+ assert result["success"] is False
279
+ assert result["cups"] == cups
280
+ assert result["error"] == error_message
281
+ assert result["period"]["start"] == start_date.isoformat()
282
+ assert result["period"]["end"] == end_date.isoformat()
283
+
284
+ # Verify database service was not called
285
+ mock_database_service.save_consumption.assert_not_called()
286
+
287
+ @pytest.mark.asyncio
288
+ async def test_update_consumptions_with_force_full_update(
289
+ self,
290
+ consumption_service,
291
+ mock_datadis_connector,
292
+ mock_database_service,
293
+ sample_consumptions,
294
+ ):
295
+ """Test consumption update with force_full_update=True ignores existing data."""
296
+ mock_connector, mock_connector_class = mock_datadis_connector
297
+ cups = "ES1234567890123456789"
298
+ distributor_code = "123"
299
+ start_date = datetime(2024, 6, 15, 0, 0)
300
+ end_date = datetime(2024, 6, 15, 23, 59)
301
+
302
+ # Mock datadis connector response
303
+ mock_connector.get_consumption_data.return_value = sample_consumptions
304
+
305
+ # Mock get_latest_consumption to return existing data
306
+ mock_latest = Mock()
307
+ mock_latest.datetime = datetime(
308
+ 2024, 6, 15, 12, 0
309
+ ) # Within the requested range
310
+ mock_database_service.get_latest_consumption.return_value = mock_latest
311
+
312
+ # Mock database service responses - no existing consumptions
313
+ mock_database_service.get_consumptions.return_value = []
314
+
315
+ # Execute update with force_full_update=True
316
+ result = await consumption_service.update_consumptions(
317
+ cups=cups,
318
+ distributor_code=distributor_code,
319
+ start_date=start_date,
320
+ end_date=end_date,
321
+ force_full_update=True,
322
+ )
323
+
324
+ # Verify datadis connector was called with original start_date (not optimized)
325
+ mock_connector.get_consumption_data.assert_called_once_with(
326
+ cups=cups,
327
+ distributor_code=distributor_code,
328
+ start_date=start_date, # Should use original start_date, not optimized
329
+ end_date=end_date,
330
+ measurement_type="0",
331
+ point_type=5,
332
+ authorized_nif=None,
333
+ )
334
+
335
+ # Verify result
336
+ assert result["success"] is True
337
+ assert result["stats"]["fetched"] == len(sample_consumptions)
338
+
339
+ @pytest.mark.asyncio
340
+ async def test_update_consumptions_incremental_optimization(
341
+ self,
342
+ consumption_service,
343
+ mock_datadis_connector,
344
+ mock_database_service,
345
+ sample_consumptions,
346
+ ):
347
+ """Test that consumption update optimizes by starting from last consumption date."""
348
+ mock_connector, mock_connector_class = mock_datadis_connector
349
+ cups = "ES1234567890123456789"
350
+ distributor_code = "123"
351
+ start_date = datetime(2024, 6, 15, 0, 0)
352
+ end_date = datetime(2024, 6, 15, 23, 59)
353
+
354
+ # Mock datadis connector response
355
+ mock_connector.get_consumption_data.return_value = sample_consumptions
356
+
357
+ # Mock get_latest_consumption to return existing data
358
+ mock_latest = Mock()
359
+ mock_latest.datetime = datetime(2024, 6, 15, 8, 0) # 8 AM on same day
360
+ mock_database_service.get_latest_consumption.return_value = mock_latest
361
+
362
+ # Mock database service responses - no existing consumptions for the new range
363
+ mock_database_service.get_consumptions.return_value = []
364
+
365
+ # Execute update
366
+ result = await consumption_service.update_consumptions(
367
+ cups=cups,
368
+ distributor_code=distributor_code,
369
+ start_date=start_date,
370
+ end_date=end_date,
371
+ )
372
+
373
+ # Verify datadis connector was called with optimized start_date (9 AM)
374
+ expected_optimized_start = datetime(2024, 6, 15, 9, 0) # last + 1 hour
375
+ mock_connector.get_consumption_data.assert_called_once_with(
376
+ cups=cups,
377
+ distributor_code=distributor_code,
378
+ start_date=expected_optimized_start, # Should be optimized
379
+ end_date=end_date,
380
+ measurement_type="0",
381
+ point_type=5,
382
+ authorized_nif=None,
383
+ )
384
+
385
+ # Verify result includes message about optimization
386
+ assert result["success"] is True
387
+ assert "message" in result
388
+ assert "missing data" in result["message"]
389
+
390
+ @pytest.mark.asyncio
391
+ async def test_update_consumptions_up_to_date(
392
+ self,
393
+ consumption_service,
394
+ mock_datadis_connector,
395
+ mock_database_service,
396
+ ):
397
+ """Test consumption update when data is already up to date."""
398
+ mock_connector, mock_connector_class = mock_datadis_connector
399
+ cups = "ES1234567890123456789"
400
+ distributor_code = "123"
401
+ start_date = datetime(2024, 6, 15, 0, 0)
402
+ end_date = datetime(2024, 6, 15, 23, 59)
403
+
404
+ # Mock get_latest_consumption to return data beyond end_date
405
+ mock_latest = Mock()
406
+ mock_latest.datetime = datetime(2024, 6, 16, 1, 0) # After end_date
407
+ mock_database_service.get_latest_consumption.return_value = mock_latest
408
+
409
+ # Execute update
410
+ result = await consumption_service.update_consumptions(
411
+ cups=cups,
412
+ distributor_code=distributor_code,
413
+ start_date=start_date,
414
+ end_date=end_date,
415
+ )
416
+
417
+ # Verify datadis connector was NOT called (data is up to date)
418
+ mock_connector.get_consumption_data.assert_not_called()
419
+
420
+ # Verify result indicates no new data needed
421
+ assert result["success"] is True
422
+ assert result["stats"]["fetched"] == 0
423
+ assert result["stats"]["skipped"] == "up_to_date"
424
+ assert "up to date" in result["message"]
425
+
426
+ @pytest.mark.asyncio
427
+ async def test_update_consumption_range_by_months_single_month(
428
+ self,
429
+ consumption_service,
430
+ mock_datadis_connector,
431
+ mock_database_service,
432
+ sample_consumptions,
433
+ ):
434
+ """Test consumption range update for a single month."""
435
+ mock_connector, mock_connector_class = mock_datadis_connector
436
+ cups = "ES1234567890123456789"
437
+ distributor_code = "123"
438
+ start_date = datetime(2024, 6, 1, 0, 0)
439
+ end_date = datetime(2024, 6, 30, 23, 59)
440
+
441
+ # Mock datadis connector response (now returns Pydantic models)
442
+ mock_connector.get_consumption_data.return_value = sample_consumptions
443
+ mock_database_service.get_consumptions.return_value = []
444
+
445
+ # Execute range update
446
+ result = await consumption_service.update_consumption_range_by_months(
447
+ cups=cups,
448
+ distributor_code=distributor_code,
449
+ start_date=start_date,
450
+ end_date=end_date,
451
+ )
452
+
453
+ # Verify result structure
454
+ assert result["success"] is True
455
+ assert result["cups"] == cups
456
+ assert result["months_processed"] == 1
457
+ assert result["total_stats"]["consumptions_fetched"] == len(sample_consumptions)
458
+ assert result["total_stats"]["consumptions_saved"] == len(sample_consumptions)
459
+ assert result["total_stats"]["consumptions_updated"] == 0
460
+ assert len(result["monthly_results"]) == 1
461
+
462
+ # Verify monthly result
463
+ monthly_result = result["monthly_results"][0]
464
+ assert monthly_result["month"] == "2024-06"
465
+ assert monthly_result["consumption"]["success"] is True
466
+
467
+ @pytest.mark.asyncio
468
+ async def test_update_consumption_range_by_months_multiple_months(
469
+ self,
470
+ consumption_service,
471
+ mock_datadis_connector,
472
+ mock_database_service,
473
+ sample_consumptions,
474
+ ):
475
+ """Test consumption range update for multiple months."""
476
+ mock_connector, mock_connector_class = mock_datadis_connector
477
+ cups = "ES1234567890123456789"
478
+ distributor_code = "123"
479
+ start_date = datetime(2024, 5, 15, 0, 0)
480
+ end_date = datetime(2024, 7, 15, 23, 59)
481
+
482
+ # Mock datadis connector response (now returns Pydantic models)
483
+ mock_connector.get_consumption_data.return_value = sample_consumptions
484
+ mock_database_service.get_consumptions.return_value = []
485
+
486
+ # Execute range update
487
+ result = await consumption_service.update_consumption_range_by_months(
488
+ cups=cups,
489
+ distributor_code=distributor_code,
490
+ start_date=start_date,
491
+ end_date=end_date,
492
+ )
493
+
494
+ # Should process 3 months: May (partial), June (full), July (partial)
495
+ assert result["months_processed"] == 3
496
+ assert len(result["monthly_results"]) == 3
497
+
498
+ # Verify month identifiers
499
+ months = [r["month"] for r in result["monthly_results"]]
500
+ assert "2024-05" in months
501
+ assert "2024-06" in months
502
+ assert "2024-07" in months
503
+
504
+ # Verify total stats
505
+ expected_total_fetched = len(sample_consumptions) * 3
506
+ assert result["total_stats"]["consumptions_fetched"] == expected_total_fetched
507
+
508
+ @pytest.mark.asyncio
509
+ async def test_update_consumption_range_by_months_with_errors(
510
+ self,
511
+ consumption_service,
512
+ mock_datadis_connector,
513
+ mock_database_service,
514
+ sample_consumptions,
515
+ ):
516
+ """Test consumption range update with some months failing."""
517
+ mock_connector, mock_connector_class = mock_datadis_connector
518
+ cups = "ES1234567890123456789"
519
+ distributor_code = "123"
520
+ start_date = datetime(2024, 6, 1, 0, 0)
521
+ end_date = datetime(2024, 8, 31, 23, 59)
522
+
523
+ # Mock datadis connector to fail on second call
524
+ call_count = 0
525
+
526
+ def mock_get_consumption_data(*args, **kwargs):
527
+ nonlocal call_count
528
+ call_count += 1
529
+ if call_count == 2: # Second month fails
530
+ raise Exception("API rate limit exceeded")
531
+ return sample_consumptions
532
+
533
+ mock_connector.get_consumption_data.side_effect = mock_get_consumption_data
534
+ mock_database_service.get_consumptions.return_value = []
535
+
536
+ # Execute range update
537
+ result = await consumption_service.update_consumption_range_by_months(
538
+ cups=cups,
539
+ distributor_code=distributor_code,
540
+ start_date=start_date,
541
+ end_date=end_date,
542
+ )
543
+
544
+ # Should process 3 months but with one failure
545
+ assert result["months_processed"] == 3
546
+ assert result["success"] is False # Overall failure due to one failed month
547
+
548
+ # Check individual month results
549
+ successful_months = [
550
+ r for r in result["monthly_results"] if r["consumption"]["success"]
551
+ ]
552
+ failed_months = [
553
+ r for r in result["monthly_results"] if not r["consumption"]["success"]
554
+ ]
555
+
556
+ assert len(successful_months) == 2
557
+ assert len(failed_months) == 1
558
+
559
+ @pytest.mark.asyncio
560
+ async def test_update_consumption_range_year_boundary(
561
+ self,
562
+ consumption_service,
563
+ mock_datadis_connector,
564
+ mock_database_service,
565
+ sample_consumptions,
566
+ ):
567
+ """Test consumption range update across year boundary."""
568
+ mock_connector, mock_connector_class = mock_datadis_connector
569
+ cups = "ES1234567890123456789"
570
+ distributor_code = "123"
571
+ start_date = datetime(2023, 12, 1, 0, 0)
572
+ end_date = datetime(2024, 2, 28, 23, 59)
573
+
574
+ # Mock datadis connector response (now returns Pydantic models)
575
+ mock_connector.get_consumption_data.return_value = sample_consumptions
576
+ mock_database_service.get_consumptions.return_value = []
577
+
578
+ # Execute range update
579
+ result = await consumption_service.update_consumption_range_by_months(
580
+ cups=cups,
581
+ distributor_code=distributor_code,
582
+ start_date=start_date,
583
+ end_date=end_date,
584
+ )
585
+
586
+ # Should process 3 months: December 2023, January 2024, February 2024
587
+ assert result["months_processed"] == 3
588
+
589
+ # Verify month identifiers
590
+ months = [r["month"] for r in result["monthly_results"]]
591
+ assert "2023-12" in months
592
+ assert "2024-01" in months
593
+ assert "2024-02" in months
594
+
595
+ @pytest.mark.asyncio
596
+ async def test_get_stored_consumptions_no_filters(
597
+ self, consumption_service, mock_database_service, sample_consumptions
598
+ ):
599
+ """Test getting stored consumptions without date filters."""
600
+ cups = "ES1234567890123456789"
601
+
602
+ # Mock database service response
603
+ mock_database_service.get_consumptions.return_value = sample_consumptions
604
+
605
+ # Execute get stored consumptions
606
+ result = await consumption_service.get_stored_consumptions(cups)
607
+
608
+ # Verify database service was called correctly
609
+ mock_database_service.get_consumptions.assert_called_once_with(cups, None, None)
610
+
611
+ # Verify result
612
+ assert result == sample_consumptions
613
+
614
+ @pytest.mark.asyncio
615
+ async def test_get_stored_consumptions_with_filters(
616
+ self, consumption_service, mock_database_service, sample_consumptions
617
+ ):
618
+ """Test getting stored consumptions with date filters."""
619
+ cups = "ES1234567890123456789"
620
+ start_date = datetime(2024, 6, 15, 0, 0)
621
+ end_date = datetime(2024, 6, 15, 23, 59)
622
+
623
+ # Mock database service response
624
+ filtered_consumptions = sample_consumptions[:2] # Return first two
625
+ mock_database_service.get_consumptions.return_value = filtered_consumptions
626
+
627
+ # Execute get stored consumptions with filters
628
+ result = await consumption_service.get_stored_consumptions(
629
+ cups=cups, start_date=start_date, end_date=end_date
630
+ )
631
+
632
+ # Verify database service was called correctly
633
+ mock_database_service.get_consumptions.assert_called_once_with(
634
+ cups, start_date, end_date
635
+ )
636
+
637
+ # Verify result
638
+ assert result == filtered_consumptions
639
+
640
+ @pytest.mark.asyncio
641
+ async def test_initialization_default_parameters(
642
+ self, temp_dir, mock_datadis_connector, mock_database_service
643
+ ):
644
+ """Test ConsumptionService initialization with default parameters."""
645
+ mock_connector, mock_connector_class = mock_datadis_connector
646
+
647
+ service = ConsumptionService(datadis_connector=mock_connector)
648
+
649
+ # Verify service stores the connector with default storage directory
650
+ assert service._datadis == mock_connector
651
+ assert service._storage_dir is None
652
+
653
+ @patch("edata.services.consumption._LOGGER")
654
+ @pytest.mark.asyncio
655
+ async def test_logging_during_operations(
656
+ self,
657
+ mock_logger,
658
+ consumption_service,
659
+ mock_datadis_connector,
660
+ mock_database_service,
661
+ sample_consumptions,
662
+ ):
663
+ """Test that appropriate logging occurs during operations."""
664
+ mock_connector, mock_connector_class = mock_datadis_connector
665
+ cups = "ES1234567890123456789"
666
+ distributor_code = "123"
667
+ start_date = datetime(2024, 6, 15, 0, 0)
668
+ end_date = datetime(2024, 6, 15, 23, 59)
669
+
670
+ # Mock datadis connector response (now returns Pydantic models)
671
+ mock_connector.get_consumption_data.return_value = sample_consumptions
672
+ mock_database_service.get_consumptions.return_value = []
673
+
674
+ # Execute update
675
+ await consumption_service.update_consumptions(
676
+ cups=cups,
677
+ distributor_code=distributor_code,
678
+ start_date=start_date,
679
+ end_date=end_date,
680
+ )
681
+
682
+ # Verify logging calls
683
+ assert mock_logger.info.call_count >= 2 # Start and completion logs
684
+
685
+ # Verify log messages contain expected information
686
+ log_calls = [call.args[0] for call in mock_logger.info.call_args_list]
687
+ assert any("Updating consumptions" in msg for msg in log_calls)
688
+ assert any("Consumption update completed" in msg for msg in log_calls)
689
+
690
+ @pytest.fixture
691
+ def sample_db_consumptions(self):
692
+ """Sample database consumption data for aggregation testing."""
693
+ from edata.services.database import ConsumptionModel as DbConsumption
694
+
695
+ # Use Monday (weekday 0) instead of Saturday for proper tariff testing
696
+ base_date = datetime(2024, 6, 17) # Monday, June 17, 2024
697
+
698
+ # Create 48 hours of hourly data (2 days: Monday and Tuesday)
699
+ db_consumptions = []
700
+ for hour in range(48):
701
+ dt = base_date + timedelta(hours=hour)
702
+ # Vary consumption by hour to test tariff periods
703
+ if 10 <= dt.hour <= 13 or 18 <= dt.hour <= 21: # P1 hours
704
+ kwh = 1.5
705
+ elif dt.hour in [8, 9, 14, 15, 16, 17, 22, 23]: # P2 hours
706
+ kwh = 1.0
707
+ else: # P3 hours
708
+ kwh = 0.5
709
+
710
+ db_cons = Mock(spec=DbConsumption)
711
+ db_cons.datetime = dt
712
+ db_cons.delta_h = 1.0
713
+ db_cons.value_kwh = kwh
714
+ db_cons.surplus_kwh = (
715
+ 0.1 if hour % 10 == 0 else 0.0
716
+ ) # Some surplus every 10 hours
717
+ db_cons.real = True
718
+
719
+ db_consumptions.append(db_cons)
720
+
721
+ return db_consumptions
722
+
723
+ @pytest.mark.asyncio
724
+ async def test_get_daily_consumptions(
725
+ self,
726
+ consumption_service,
727
+ mock_datadis_connector,
728
+ mock_database_service,
729
+ sample_db_consumptions,
730
+ ):
731
+ """Test daily consumption aggregation."""
732
+ mock_connector, mock_connector_class = mock_datadis_connector
733
+ cups = "ES1234567890123456789"
734
+ start_date = datetime(2024, 6, 17, 0, 0) # Monday
735
+ end_date = datetime(2024, 6, 18, 23, 59) # Tuesday
736
+
737
+ # Mock database service to return sample data
738
+ mock_database_service.get_consumptions.return_value = sample_db_consumptions
739
+
740
+ # Execute daily aggregation
741
+ daily_consumptions = await consumption_service.get_daily_consumptions(
742
+ cups=cups, start_date=start_date, end_date=end_date
743
+ )
744
+
745
+ # Verify database service was called correctly
746
+ mock_database_service.get_consumptions.assert_called_once_with(
747
+ cups, start_date, end_date
748
+ )
749
+
750
+ # Should have 2 days of data
751
+ assert len(daily_consumptions) == 2
752
+
753
+ # Verify first day aggregation
754
+ day1 = daily_consumptions[0]
755
+ assert isinstance(day1, ConsumptionAggregated)
756
+ assert day1.datetime.date() == date(2024, 6, 17) # Monday
757
+ assert day1.delta_h == 24.0 # 24 hours
758
+
759
+ # Verify total consumption (should be sum of all hourly values)
760
+ expected_day1_total = (8 * 1.5) + (8 * 1.0) + (8 * 0.5) # P1 + P2 + P3 hours
761
+ assert day1.value_kwh == expected_day1_total
762
+
763
+ # Verify P1 consumption (hours 10-13, 18-21)
764
+ expected_p1 = 8 * 1.5 # 8 P1 hours at 1.5 kWh each
765
+ assert day1.value_p1_kwh == expected_p1
766
+
767
+ # Verify some surplus was recorded
768
+ assert day1.surplus_kwh > 0
769
+
770
+ # Verify second day
771
+ day2 = daily_consumptions[1]
772
+ assert day2.datetime.date() == date(2024, 6, 18) # Tuesday
773
+ assert day2.delta_h == 24.0
774
+
775
+ @pytest.mark.asyncio
776
+ async def test_get_monthly_consumptions(
777
+ self,
778
+ consumption_service,
779
+ mock_datadis_connector,
780
+ mock_database_service,
781
+ sample_db_consumptions,
782
+ ):
783
+ """Test monthly consumption aggregation."""
784
+ mock_connector, mock_connector_class = mock_datadis_connector
785
+ cups = "ES1234567890123456789"
786
+ start_date = datetime(2024, 6, 17, 0, 0) # Monday
787
+ end_date = datetime(2024, 6, 18, 23, 59) # Tuesday
788
+
789
+ # Mock database service to return sample data
790
+ mock_database_service.get_consumptions.return_value = sample_db_consumptions
791
+
792
+ # Execute monthly aggregation
793
+ monthly_consumptions = await consumption_service.get_monthly_consumptions(
794
+ cups=cups, start_date=start_date, end_date=end_date
795
+ )
796
+
797
+ # Verify database service was called correctly
798
+ mock_database_service.get_consumptions.assert_called_once_with(
799
+ cups, start_date, end_date
800
+ )
801
+
802
+ # Should have 1 month of data (both days in same month)
803
+ assert len(monthly_consumptions) == 1
804
+
805
+ # Verify monthly aggregation
806
+ month = monthly_consumptions[0]
807
+ assert isinstance(month, ConsumptionAggregated)
808
+ assert month.datetime.replace(day=1).date() == date(2024, 6, 1)
809
+ assert month.delta_h == 48.0 # 48 hours total
810
+
811
+ # Verify total consumption (should be sum of both days)
812
+ expected_total = 2 * ((8 * 1.5) + (8 * 1.0) + (8 * 0.5))
813
+ assert month.value_kwh == expected_total
814
+
815
+ # Verify P1 consumption
816
+ expected_p1 = 2 * (8 * 1.5) # 2 days * 8 P1 hours * 1.5 kWh
817
+ assert month.value_p1_kwh == expected_p1
818
+
819
+ @pytest.mark.asyncio
820
+ async def test_get_monthly_consumptions_with_cycle_start_day(
821
+ self, consumption_service, mock_datadis_connector, mock_database_service
822
+ ):
823
+ """Test monthly consumption aggregation with custom cycle start day."""
824
+ mock_connector, mock_connector_class = mock_datadis_connector
825
+ cups = "ES1234567890123456789"
826
+ start_date = datetime(2024, 6, 1, 0, 0)
827
+ end_date = datetime(2024, 6, 30, 23, 59)
828
+
829
+ # Create sample data spanning across billing cycle boundary
830
+ from edata.services.database import ConsumptionModel as DbConsumption
831
+
832
+ db_consumptions = []
833
+
834
+ # Data on June 14th (before cycle start)
835
+ dt1 = datetime(2024, 6, 14, 12, 0)
836
+ db_cons1 = Mock(spec=DbConsumption)
837
+ db_cons1.datetime = dt1
838
+ db_cons1.delta_h = 1.0
839
+ db_cons1.value_kwh = 2.0
840
+ db_cons1.surplus_kwh = 0.0
841
+ db_cons1.real = True
842
+ db_consumptions.append(db_cons1)
843
+
844
+ # Data on June 16th (after cycle start)
845
+ dt2 = datetime(2024, 6, 16, 12, 0)
846
+ db_cons2 = Mock(spec=DbConsumption)
847
+ db_cons2.datetime = dt2
848
+ db_cons2.delta_h = 1.0
849
+ db_cons2.value_kwh = 3.0
850
+ db_cons2.surplus_kwh = 0.0
851
+ db_cons2.real = True
852
+ db_consumptions.append(db_cons2)
853
+
854
+ mock_database_service.get_consumptions.return_value = db_consumptions
855
+
856
+ # Execute with cycle start day = 15
857
+ monthly_consumptions = await consumption_service.get_monthly_consumptions(
858
+ cups=cups, start_date=start_date, end_date=end_date, cycle_start_day=15
859
+ )
860
+
861
+ # Should have 2 months (May billing period and June billing period)
862
+ assert len(monthly_consumptions) == 2
863
+
864
+ # Verify the months
865
+ months = sorted([m.datetime for m in monthly_consumptions])
866
+ assert months[0].month == 5 # May billing period (for June 14th data)
867
+ assert months[1].month == 6 # June billing period (for June 16th data)
868
+
869
+ @pytest.mark.asyncio
870
+ async def test_get_daily_consumptions_empty_data(
871
+ self, consumption_service, mock_datadis_connector, mock_database_service
872
+ ):
873
+ """Test daily consumption aggregation with no data."""
874
+ mock_connector, mock_connector_class = mock_datadis_connector
875
+ cups = "ES1234567890123456789"
876
+ start_date = datetime(2024, 6, 17, 0, 0) # Monday
877
+ end_date = datetime(2024, 6, 17, 23, 59) # Monday
878
+
879
+ # Mock database service to return empty data
880
+ mock_database_service.get_consumptions.return_value = []
881
+
882
+ # Execute daily aggregation
883
+ daily_consumptions = await consumption_service.get_daily_consumptions(
884
+ cups=cups, start_date=start_date, end_date=end_date
885
+ )
886
+
887
+ # Should return empty list
888
+ assert len(daily_consumptions) == 0
889
+
890
+ @pytest.mark.asyncio
891
+ async def test_get_monthly_consumptions_empty_data(
892
+ self, consumption_service, mock_datadis_connector, mock_database_service
893
+ ):
894
+ """Test monthly consumption aggregation with no data."""
895
+ mock_connector, mock_connector_class = mock_datadis_connector
896
+ cups = "ES1234567890123456789"
897
+ start_date = datetime(2024, 6, 15, 0, 0)
898
+ end_date = datetime(2024, 6, 15, 23, 59)
899
+
900
+ # Mock database service to return empty data
901
+ mock_database_service.get_consumptions.return_value = []
902
+
903
+ # Execute monthly aggregation
904
+ monthly_consumptions = await consumption_service.get_monthly_consumptions(
905
+ cups=cups, start_date=start_date, end_date=end_date
906
+ )
907
+
908
+ # Should return empty list
909
+ assert len(monthly_consumptions) == 0
910
+
911
+ @pytest.mark.asyncio
912
+ @patch("edata.services.consumption.get_pvpc_tariff")
913
+ async def test_tariff_calculation_in_aggregations(
914
+ self,
915
+ mock_get_pvpc_tariff,
916
+ consumption_service,
917
+ mock_datadis_connector,
918
+ mock_database_service,
919
+ ):
920
+ """Test that tariff calculation is used correctly in aggregations."""
921
+ mock_connector, mock_connector_class = mock_datadis_connector
922
+ cups = "ES1234567890123456789"
923
+ start_date = datetime(2024, 6, 17, 0, 0) # Monday
924
+ end_date = datetime(2024, 6, 17, 23, 59) # Monday
925
+
926
+ # Create single consumption data
927
+ from edata.services.database import ConsumptionModel as DbConsumption
928
+
929
+ db_cons = Mock(spec=DbConsumption)
930
+ db_cons.datetime = datetime(2024, 6, 17, 12, 0) # Monday noon
931
+ db_cons.delta_h = 1.0
932
+ db_cons.value_kwh = 2.0
933
+ db_cons.surplus_kwh = 0.1
934
+ db_cons.real = True
935
+
936
+ mock_database_service.get_consumptions.return_value = [db_cons]
937
+
938
+ # Mock tariff calculation to return P2
939
+ mock_get_pvpc_tariff.return_value = "p2"
940
+
941
+ # Execute daily aggregation with await
942
+ daily_consumptions = await consumption_service.get_daily_consumptions(
943
+ cups=cups, start_date=start_date, end_date=end_date
944
+ )
945
+
946
+ # Verify tariff function was called
947
+ mock_get_pvpc_tariff.assert_called_with(datetime(2024, 6, 17, 12, 0))
948
+
949
+ # Verify P2 values were set correctly
950
+ assert len(daily_consumptions) == 1
951
+ day = daily_consumptions[0]
952
+ assert day.value_p2_kwh == 2.0
953
+ assert day.surplus_p2_kwh == 0.1
954
+ assert day.value_p1_kwh == 0.0
955
+ assert day.value_p3_kwh == 0.0