1use std::sync::Arc;
2use uuid::Uuid;
3
4use crate::application::ports::{EnergyBillUploadRepository, EnergyCampaignRepository};
5use crate::domain::entities::{CampaignStatus, EnergyBillUpload};
6
7pub struct EnergyBillUploadUseCases {
8 upload_repo: Arc<dyn EnergyBillUploadRepository>,
9 campaign_repo: Arc<dyn EnergyCampaignRepository>,
10}
11
12impl EnergyBillUploadUseCases {
13 pub fn new(
14 upload_repo: Arc<dyn EnergyBillUploadRepository>,
15 campaign_repo: Arc<dyn EnergyCampaignRepository>,
16 ) -> Self {
17 Self {
18 upload_repo,
19 campaign_repo,
20 }
21 }
22
23 pub async fn upload_bill(&self, upload: EnergyBillUpload) -> Result<EnergyBillUpload, String> {
25 let campaign = self
27 .campaign_repo
28 .find_by_id(upload.campaign_id)
29 .await?
30 .ok_or_else(|| "Campaign not found".to_string())?;
31
32 if campaign.status != CampaignStatus::CollectingData {
33 return Err("Campaign is not collecting data".to_string());
34 }
35
36 let existing = self
38 .upload_repo
39 .find_by_campaign_and_unit(upload.campaign_id, upload.unit_id)
40 .await?;
41
42 if existing.is_some() {
43 return Err("Unit has already uploaded bill for this campaign".to_string());
44 }
45
46 self.upload_repo.create(&upload).await
47 }
48
49 pub async fn get_upload(&self, id: Uuid) -> Result<Option<EnergyBillUpload>, String> {
51 self.upload_repo.find_by_id(id).await
52 }
53
54 pub async fn get_uploads_by_campaign(
56 &self,
57 campaign_id: Uuid,
58 ) -> Result<Vec<EnergyBillUpload>, String> {
59 self.upload_repo.find_by_campaign(campaign_id).await
60 }
61
62 pub async fn get_my_uploads(&self, unit_id: Uuid) -> Result<Vec<EnergyBillUpload>, String> {
64 self.upload_repo.find_by_unit(unit_id).await
65 }
66
67 pub async fn verify_upload(
69 &self,
70 upload_id: Uuid,
71 verified_by: Uuid,
72 ) -> Result<EnergyBillUpload, String> {
73 let mut upload = self
74 .upload_repo
75 .find_by_id(upload_id)
76 .await?
77 .ok_or_else(|| "Upload not found".to_string())?;
78
79 upload.mark_verified(verified_by)?;
80 self.upload_repo.update(&upload).await
81 }
82
83 pub async fn anonymize_upload(&self, upload_id: Uuid) -> Result<EnergyBillUpload, String> {
85 let mut upload = self
86 .upload_repo
87 .find_by_id(upload_id)
88 .await?
89 .ok_or_else(|| "Upload not found".to_string())?;
90
91 upload.anonymize()?;
92 self.upload_repo.update(&upload).await
93 }
94
95 pub async fn batch_anonymize_campaign(&self, campaign_id: Uuid) -> Result<i32, String> {
97 let uploads = self
98 .upload_repo
99 .find_verified_by_campaign(campaign_id)
100 .await?;
101
102 let mut count = 0;
103 for mut upload in uploads {
104 if !upload.anonymized {
105 if upload.anonymize().is_ok() {
106 self.upload_repo.update(&upload).await?;
107 count += 1;
108 }
109 }
110 }
111
112 Ok(count)
113 }
114
115 pub async fn decrypt_consumption(
117 &self,
118 upload_id: Uuid,
119 requester_unit_id: Uuid,
120 encryption_key: &[u8; 32],
121 ) -> Result<f64, String> {
122 let upload = self
123 .upload_repo
124 .find_by_id(upload_id)
125 .await?
126 .ok_or_else(|| "Upload not found".to_string())?;
127
128 if upload.unit_id != requester_unit_id {
130 return Err("Unauthorized: You can only access your own data".to_string());
131 }
132
133 upload.decrypt_kwh(encryption_key)
134 }
135
136 pub async fn delete_upload(
138 &self,
139 upload_id: Uuid,
140 requester_unit_id: Uuid,
141 ) -> Result<(), String> {
142 let mut upload = self
143 .upload_repo
144 .find_by_id(upload_id)
145 .await?
146 .ok_or_else(|| "Upload not found".to_string())?;
147
148 if upload.unit_id != requester_unit_id {
150 return Err("Unauthorized: You can only delete your own data".to_string());
151 }
152
153 upload.delete()?;
154 self.upload_repo.update(&upload).await?;
155 Ok(())
156 }
157
158 pub async fn withdraw_consent(
160 &self,
161 upload_id: Uuid,
162 requester_unit_id: Uuid,
163 ) -> Result<(), String> {
164 let mut upload = self
165 .upload_repo
166 .find_by_id(upload_id)
167 .await?
168 .ok_or_else(|| "Upload not found".to_string())?;
169
170 if upload.unit_id != requester_unit_id {
172 return Err("Unauthorized: You can only withdraw your own consent".to_string());
173 }
174
175 upload.withdraw_consent()?;
176 self.upload_repo.update(&upload).await?;
177 Ok(())
178 }
179
180 pub async fn get_verified_count(&self, campaign_id: Uuid) -> Result<i32, String> {
182 self.upload_repo
183 .count_verified_by_campaign(campaign_id)
184 .await
185 }
186
187 pub async fn check_k_anonymity(&self, campaign_id: Uuid) -> Result<bool, String> {
189 let count = self.get_verified_count(campaign_id).await?;
190 Ok(count >= 5)
191 }
192
193 pub async fn cleanup_expired(&self) -> Result<i32, String> {
195 self.upload_repo.delete_expired().await
196 }
197
198 pub async fn get_expired_count(&self) -> Result<usize, String> {
200 let expired = self.upload_repo.find_expired().await?;
201 Ok(expired.len())
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::application::ports::{EnergyBillUploadRepository, EnergyCampaignRepository};
209 use crate::domain::entities::{
210 CampaignStatus, CampaignType, ContractType, EnergyBillUpload, EnergyCampaign, EnergyType,
211 ProviderOffer,
212 };
213 use async_trait::async_trait;
214 use chrono::Utc;
215 use std::collections::HashMap;
216 use std::sync::Mutex;
217 use uuid::Uuid;
218
219 struct MockUploadRepo {
222 uploads: Mutex<HashMap<Uuid, EnergyBillUpload>>,
223 }
224
225 impl MockUploadRepo {
226 fn new() -> Self {
227 Self {
228 uploads: Mutex::new(HashMap::new()),
229 }
230 }
231
232 fn with_upload(upload: EnergyBillUpload) -> Self {
233 let mut map = HashMap::new();
234 map.insert(upload.id, upload);
235 Self {
236 uploads: Mutex::new(map),
237 }
238 }
239 }
240
241 #[async_trait]
242 impl EnergyBillUploadRepository for MockUploadRepo {
243 async fn create(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
244 let mut store = self.uploads.lock().unwrap();
245 store.insert(upload.id, upload.clone());
246 Ok(upload.clone())
247 }
248
249 async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyBillUpload>, String> {
250 let store = self.uploads.lock().unwrap();
251 Ok(store.get(&id).cloned())
252 }
253
254 async fn find_by_campaign(
255 &self,
256 campaign_id: Uuid,
257 ) -> Result<Vec<EnergyBillUpload>, String> {
258 let store = self.uploads.lock().unwrap();
259 Ok(store
260 .values()
261 .filter(|u| u.campaign_id == campaign_id)
262 .cloned()
263 .collect())
264 }
265
266 async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<EnergyBillUpload>, String> {
267 let store = self.uploads.lock().unwrap();
268 Ok(store
269 .values()
270 .filter(|u| u.unit_id == unit_id)
271 .cloned()
272 .collect())
273 }
274
275 async fn find_by_campaign_and_unit(
276 &self,
277 campaign_id: Uuid,
278 unit_id: Uuid,
279 ) -> Result<Option<EnergyBillUpload>, String> {
280 let store = self.uploads.lock().unwrap();
281 Ok(store
282 .values()
283 .find(|u| u.campaign_id == campaign_id && u.unit_id == unit_id)
284 .cloned())
285 }
286
287 async fn find_by_building(
288 &self,
289 building_id: Uuid,
290 ) -> Result<Vec<EnergyBillUpload>, String> {
291 let store = self.uploads.lock().unwrap();
292 Ok(store
293 .values()
294 .filter(|u| u.building_id == building_id)
295 .cloned()
296 .collect())
297 }
298
299 async fn update(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
300 let mut store = self.uploads.lock().unwrap();
301 store.insert(upload.id, upload.clone());
302 Ok(upload.clone())
303 }
304
305 async fn delete(&self, id: Uuid) -> Result<(), String> {
306 let mut store = self.uploads.lock().unwrap();
307 store.remove(&id);
308 Ok(())
309 }
310
311 async fn find_expired(&self) -> Result<Vec<EnergyBillUpload>, String> {
312 let store = self.uploads.lock().unwrap();
313 Ok(store
314 .values()
315 .filter(|u| u.should_auto_delete())
316 .cloned()
317 .collect())
318 }
319
320 async fn count_verified_by_campaign(&self, campaign_id: Uuid) -> Result<i32, String> {
321 let store = self.uploads.lock().unwrap();
322 Ok(store
323 .values()
324 .filter(|u| u.campaign_id == campaign_id && u.verified_at.is_some())
325 .count() as i32)
326 }
327
328 async fn find_verified_by_campaign(
329 &self,
330 campaign_id: Uuid,
331 ) -> Result<Vec<EnergyBillUpload>, String> {
332 let store = self.uploads.lock().unwrap();
333 Ok(store
334 .values()
335 .filter(|u| u.campaign_id == campaign_id && u.verified_at.is_some())
336 .cloned()
337 .collect())
338 }
339
340 async fn delete_expired(&self) -> Result<i32, String> {
341 let mut store = self.uploads.lock().unwrap();
342 let expired_ids: Vec<Uuid> = store
343 .values()
344 .filter(|u| u.should_auto_delete())
345 .map(|u| u.id)
346 .collect();
347 let count = expired_ids.len() as i32;
348 for id in expired_ids {
349 store.remove(&id);
350 }
351 Ok(count)
352 }
353 }
354
355 struct MockCampaignRepo {
358 campaigns: Mutex<HashMap<Uuid, EnergyCampaign>>,
359 offers: Mutex<HashMap<Uuid, ProviderOffer>>,
360 }
361
362 impl MockCampaignRepo {
363 fn new() -> Self {
364 Self {
365 campaigns: Mutex::new(HashMap::new()),
366 offers: Mutex::new(HashMap::new()),
367 }
368 }
369
370 fn with_campaign(campaign: EnergyCampaign) -> Self {
371 let mut map = HashMap::new();
372 map.insert(campaign.id, campaign);
373 Self {
374 campaigns: Mutex::new(map),
375 offers: Mutex::new(HashMap::new()),
376 }
377 }
378 }
379
380 #[async_trait]
381 impl EnergyCampaignRepository for MockCampaignRepo {
382 async fn create(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
383 let mut store = self.campaigns.lock().unwrap();
384 store.insert(campaign.id, campaign.clone());
385 Ok(campaign.clone())
386 }
387
388 async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
389 let store = self.campaigns.lock().unwrap();
390 Ok(store.get(&id).cloned())
391 }
392
393 async fn find_by_organization(
394 &self,
395 organization_id: Uuid,
396 ) -> Result<Vec<EnergyCampaign>, String> {
397 let store = self.campaigns.lock().unwrap();
398 Ok(store
399 .values()
400 .filter(|c| c.organization_id == organization_id)
401 .cloned()
402 .collect())
403 }
404
405 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<EnergyCampaign>, String> {
406 let store = self.campaigns.lock().unwrap();
407 Ok(store
408 .values()
409 .filter(|c| c.building_id == Some(building_id))
410 .cloned()
411 .collect())
412 }
413
414 async fn update(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
415 let mut store = self.campaigns.lock().unwrap();
416 store.insert(campaign.id, campaign.clone());
417 Ok(campaign.clone())
418 }
419
420 async fn delete(&self, id: Uuid) -> Result<(), String> {
421 let mut store = self.campaigns.lock().unwrap();
422 store.remove(&id);
423 Ok(())
424 }
425
426 async fn add_offer(
427 &self,
428 _campaign_id: Uuid,
429 offer: &ProviderOffer,
430 ) -> Result<ProviderOffer, String> {
431 let mut store = self.offers.lock().unwrap();
432 store.insert(offer.id, offer.clone());
433 Ok(offer.clone())
434 }
435
436 async fn get_offers(&self, campaign_id: Uuid) -> Result<Vec<ProviderOffer>, String> {
437 let store = self.offers.lock().unwrap();
438 Ok(store
439 .values()
440 .filter(|o| o.campaign_id == campaign_id)
441 .cloned()
442 .collect())
443 }
444
445 async fn update_offer(&self, offer: &ProviderOffer) -> Result<ProviderOffer, String> {
446 let mut store = self.offers.lock().unwrap();
447 store.insert(offer.id, offer.clone());
448 Ok(offer.clone())
449 }
450
451 async fn delete_offer(&self, offer_id: Uuid) -> Result<(), String> {
452 let mut store = self.offers.lock().unwrap();
453 store.remove(&offer_id);
454 Ok(())
455 }
456
457 async fn find_offer_by_id(&self, offer_id: Uuid) -> Result<Option<ProviderOffer>, String> {
458 let store = self.offers.lock().unwrap();
459 Ok(store.get(&offer_id).cloned())
460 }
461
462 async fn update_aggregation(
463 &self,
464 _campaign_id: Uuid,
465 _total_kwh_electricity: Option<f64>,
466 _total_kwh_gas: Option<f64>,
467 _avg_kwh_per_unit: Option<f64>,
468 ) -> Result<(), String> {
469 Ok(())
470 }
471 }
472
473 fn get_test_encryption_key() -> [u8; 32] {
476 *b"test_master_key_for_32bytes!##!!"
477 }
478
479 fn make_collecting_campaign() -> EnergyCampaign {
480 let mut campaign = EnergyCampaign::new(
481 Uuid::new_v4(),
482 Some(Uuid::new_v4()),
483 "Winter Campaign 2025".to_string(),
484 Utc::now() + chrono::Duration::days(30),
485 vec![EnergyType::Electricity],
486 Uuid::new_v4(),
487 )
488 .unwrap();
489 campaign.status = CampaignStatus::CollectingData;
491 campaign
492 }
493
494 fn make_upload(campaign_id: Uuid, unit_id: Uuid, building_id: Uuid) -> EnergyBillUpload {
495 let key = get_test_encryption_key();
496 EnergyBillUpload::new(
497 campaign_id,
498 unit_id,
499 building_id,
500 Uuid::new_v4(),
501 Utc::now() - chrono::Duration::days(365),
502 Utc::now(),
503 2400.0,
504 EnergyType::Electricity,
505 "1050".to_string(),
506 "abc123hash".to_string(),
507 "/encrypted/path".to_string(),
508 Uuid::new_v4(),
509 "192.168.1.1".to_string(),
510 "Mozilla/5.0".to_string(),
511 &key,
512 )
513 .unwrap()
514 }
515
516 #[tokio::test]
519 async fn test_upload_bill_success() {
520 let campaign = make_collecting_campaign();
521 let campaign_id = campaign.id;
522 let building_id = campaign.building_id.unwrap();
523 let unit_id = Uuid::new_v4();
524
525 let upload = make_upload(campaign_id, unit_id, building_id);
526
527 let uc = EnergyBillUploadUseCases::new(
528 Arc::new(MockUploadRepo::new()),
529 Arc::new(MockCampaignRepo::with_campaign(campaign)),
530 );
531
532 let result = uc.upload_bill(upload).await;
533 assert!(result.is_ok());
534 let created = result.unwrap();
535 assert_eq!(created.campaign_id, campaign_id);
536 assert_eq!(created.unit_id, unit_id);
537 }
538
539 #[tokio::test]
540 async fn test_upload_bill_campaign_not_collecting() {
541 let mut campaign = make_collecting_campaign();
542 campaign.status = CampaignStatus::Draft; let campaign_id = campaign.id;
544 let building_id = campaign.building_id.unwrap();
545 let unit_id = Uuid::new_v4();
546
547 let upload = make_upload(campaign_id, unit_id, building_id);
548
549 let uc = EnergyBillUploadUseCases::new(
550 Arc::new(MockUploadRepo::new()),
551 Arc::new(MockCampaignRepo::with_campaign(campaign)),
552 );
553
554 let result = uc.upload_bill(upload).await;
555 assert!(result.is_err());
556 assert_eq!(result.unwrap_err(), "Campaign is not collecting data");
557 }
558
559 #[tokio::test]
560 async fn test_upload_bill_duplicate_unit() {
561 let campaign = make_collecting_campaign();
562 let campaign_id = campaign.id;
563 let building_id = campaign.building_id.unwrap();
564 let unit_id = Uuid::new_v4();
565
566 let existing_upload = make_upload(campaign_id, unit_id, building_id);
568 let upload_repo = MockUploadRepo::with_upload(existing_upload);
569
570 let new_upload = make_upload(campaign_id, unit_id, building_id);
571
572 let uc = EnergyBillUploadUseCases::new(
573 Arc::new(upload_repo),
574 Arc::new(MockCampaignRepo::with_campaign(campaign)),
575 );
576
577 let result = uc.upload_bill(new_upload).await;
578 assert!(result.is_err());
579 assert_eq!(
580 result.unwrap_err(),
581 "Unit has already uploaded bill for this campaign"
582 );
583 }
584
585 #[tokio::test]
586 async fn test_get_upload_success() {
587 let campaign_id = Uuid::new_v4();
588 let unit_id = Uuid::new_v4();
589 let building_id = Uuid::new_v4();
590 let upload = make_upload(campaign_id, unit_id, building_id);
591 let upload_id = upload.id;
592
593 let uc = EnergyBillUploadUseCases::new(
594 Arc::new(MockUploadRepo::with_upload(upload)),
595 Arc::new(MockCampaignRepo::new()),
596 );
597
598 let result = uc.get_upload(upload_id).await;
599 assert!(result.is_ok());
600 assert!(result.unwrap().is_some());
601 }
602
603 #[tokio::test]
604 async fn test_get_upload_not_found() {
605 let uc = EnergyBillUploadUseCases::new(
606 Arc::new(MockUploadRepo::new()),
607 Arc::new(MockCampaignRepo::new()),
608 );
609
610 let result = uc.get_upload(Uuid::new_v4()).await;
611 assert!(result.is_ok());
612 assert!(result.unwrap().is_none());
613 }
614
615 #[tokio::test]
616 async fn test_verify_upload_success() {
617 let campaign_id = Uuid::new_v4();
618 let unit_id = Uuid::new_v4();
619 let building_id = Uuid::new_v4();
620 let upload = make_upload(campaign_id, unit_id, building_id);
621 let upload_id = upload.id;
622 let verifier_id = Uuid::new_v4();
623
624 let uc = EnergyBillUploadUseCases::new(
625 Arc::new(MockUploadRepo::with_upload(upload)),
626 Arc::new(MockCampaignRepo::new()),
627 );
628
629 let result = uc.verify_upload(upload_id, verifier_id).await;
630 assert!(result.is_ok());
631 let verified = result.unwrap();
632 assert!(verified.manually_verified);
633 assert_eq!(verified.verified_by, Some(verifier_id));
634 }
635
636 #[tokio::test]
637 async fn test_verify_upload_not_found() {
638 let uc = EnergyBillUploadUseCases::new(
639 Arc::new(MockUploadRepo::new()),
640 Arc::new(MockCampaignRepo::new()),
641 );
642
643 let result = uc.verify_upload(Uuid::new_v4(), Uuid::new_v4()).await;
644 assert!(result.is_err());
645 assert_eq!(result.unwrap_err(), "Upload not found");
646 }
647
648 #[tokio::test]
649 async fn test_delete_upload_success() {
650 let campaign_id = Uuid::new_v4();
651 let unit_id = Uuid::new_v4();
652 let building_id = Uuid::new_v4();
653 let upload = make_upload(campaign_id, unit_id, building_id);
654 let upload_id = upload.id;
655
656 let uc = EnergyBillUploadUseCases::new(
657 Arc::new(MockUploadRepo::with_upload(upload)),
658 Arc::new(MockCampaignRepo::new()),
659 );
660
661 let result = uc.delete_upload(upload_id, unit_id).await;
662 assert!(result.is_ok());
663 }
664
665 #[tokio::test]
666 async fn test_delete_upload_unauthorized() {
667 let campaign_id = Uuid::new_v4();
668 let unit_id = Uuid::new_v4();
669 let other_unit_id = Uuid::new_v4();
670 let building_id = Uuid::new_v4();
671 let upload = make_upload(campaign_id, unit_id, building_id);
672 let upload_id = upload.id;
673
674 let uc = EnergyBillUploadUseCases::new(
675 Arc::new(MockUploadRepo::with_upload(upload)),
676 Arc::new(MockCampaignRepo::new()),
677 );
678
679 let result = uc.delete_upload(upload_id, other_unit_id).await;
680 assert!(result.is_err());
681 assert!(result
682 .unwrap_err()
683 .contains("Unauthorized: You can only delete your own data"));
684 }
685
686 #[tokio::test]
687 async fn test_check_k_anonymity_met() {
688 let campaign_id = Uuid::new_v4();
689 let building_id = Uuid::new_v4();
690
691 let mut upload_map = HashMap::new();
693 for _ in 0..5 {
694 let mut upload = make_upload(campaign_id, Uuid::new_v4(), building_id);
695 upload.mark_verified(Uuid::new_v4()).unwrap();
696 upload_map.insert(upload.id, upload);
697 }
698 let upload_repo = MockUploadRepo {
699 uploads: Mutex::new(upload_map),
700 };
701
702 let uc =
703 EnergyBillUploadUseCases::new(Arc::new(upload_repo), Arc::new(MockCampaignRepo::new()));
704
705 let result = uc.check_k_anonymity(campaign_id).await;
706 assert!(result.is_ok());
707 assert!(result.unwrap()); }
709
710 #[tokio::test]
711 async fn test_check_k_anonymity_not_met() {
712 let campaign_id = Uuid::new_v4();
713 let building_id = Uuid::new_v4();
714
715 let mut upload_map = HashMap::new();
717 for _ in 0..3 {
718 let mut upload = make_upload(campaign_id, Uuid::new_v4(), building_id);
719 upload.mark_verified(Uuid::new_v4()).unwrap();
720 upload_map.insert(upload.id, upload);
721 }
722 let upload_repo = MockUploadRepo {
723 uploads: Mutex::new(upload_map),
724 };
725
726 let uc =
727 EnergyBillUploadUseCases::new(Arc::new(upload_repo), Arc::new(MockCampaignRepo::new()));
728
729 let result = uc.check_k_anonymity(campaign_id).await;
730 assert!(result.is_ok());
731 assert!(!result.unwrap()); }
733}