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, uploaded_by: Uuid) -> Result<Vec<EnergyBillUpload>, String> {
64 self.upload_repo.find_by_uploaded_by(uploaded_by).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, EnergyBillUpload, EnergyCampaign, EnergyType, ProviderOffer,
211 };
212 use async_trait::async_trait;
213 use chrono::Utc;
214 use std::collections::HashMap;
215 use std::sync::Mutex;
216 use uuid::Uuid;
217
218 struct MockUploadRepo {
221 uploads: Mutex<HashMap<Uuid, EnergyBillUpload>>,
222 }
223
224 impl MockUploadRepo {
225 fn new() -> Self {
226 Self {
227 uploads: Mutex::new(HashMap::new()),
228 }
229 }
230
231 fn with_upload(upload: EnergyBillUpload) -> Self {
232 let mut map = HashMap::new();
233 map.insert(upload.id, upload);
234 Self {
235 uploads: Mutex::new(map),
236 }
237 }
238 }
239
240 #[async_trait]
241 impl EnergyBillUploadRepository for MockUploadRepo {
242 async fn create(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
243 let mut store = self.uploads.lock().unwrap();
244 store.insert(upload.id, upload.clone());
245 Ok(upload.clone())
246 }
247
248 async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyBillUpload>, String> {
249 let store = self.uploads.lock().unwrap();
250 Ok(store.get(&id).cloned())
251 }
252
253 async fn find_by_campaign(
254 &self,
255 campaign_id: Uuid,
256 ) -> Result<Vec<EnergyBillUpload>, String> {
257 let store = self.uploads.lock().unwrap();
258 Ok(store
259 .values()
260 .filter(|u| u.campaign_id == campaign_id)
261 .cloned()
262 .collect())
263 }
264
265 async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<EnergyBillUpload>, String> {
266 let store = self.uploads.lock().unwrap();
267 Ok(store
268 .values()
269 .filter(|u| u.unit_id == unit_id)
270 .cloned()
271 .collect())
272 }
273
274 async fn find_by_campaign_and_unit(
275 &self,
276 campaign_id: Uuid,
277 unit_id: Uuid,
278 ) -> Result<Option<EnergyBillUpload>, String> {
279 let store = self.uploads.lock().unwrap();
280 Ok(store
281 .values()
282 .find(|u| u.campaign_id == campaign_id && u.unit_id == unit_id)
283 .cloned())
284 }
285
286 async fn find_by_uploaded_by(
287 &self,
288 uploaded_by: Uuid,
289 ) -> Result<Vec<EnergyBillUpload>, String> {
290 let store = self.uploads.lock().unwrap();
291 Ok(store
292 .values()
293 .filter(|u| u.uploaded_by == uploaded_by)
294 .cloned()
295 .collect())
296 }
297
298 async fn find_by_building(
299 &self,
300 building_id: Uuid,
301 ) -> Result<Vec<EnergyBillUpload>, String> {
302 let store = self.uploads.lock().unwrap();
303 Ok(store
304 .values()
305 .filter(|u| u.building_id == building_id)
306 .cloned()
307 .collect())
308 }
309
310 async fn update(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
311 let mut store = self.uploads.lock().unwrap();
312 store.insert(upload.id, upload.clone());
313 Ok(upload.clone())
314 }
315
316 async fn delete(&self, id: Uuid) -> Result<(), String> {
317 let mut store = self.uploads.lock().unwrap();
318 store.remove(&id);
319 Ok(())
320 }
321
322 async fn find_expired(&self) -> Result<Vec<EnergyBillUpload>, String> {
323 let store = self.uploads.lock().unwrap();
324 Ok(store
325 .values()
326 .filter(|u| u.should_auto_delete())
327 .cloned()
328 .collect())
329 }
330
331 async fn count_verified_by_campaign(&self, campaign_id: Uuid) -> Result<i32, 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 .count() as i32)
337 }
338
339 async fn find_verified_by_campaign(
340 &self,
341 campaign_id: Uuid,
342 ) -> Result<Vec<EnergyBillUpload>, String> {
343 let store = self.uploads.lock().unwrap();
344 Ok(store
345 .values()
346 .filter(|u| u.campaign_id == campaign_id && u.verified_at.is_some())
347 .cloned()
348 .collect())
349 }
350
351 async fn delete_expired(&self) -> Result<i32, String> {
352 let mut store = self.uploads.lock().unwrap();
353 let expired_ids: Vec<Uuid> = store
354 .values()
355 .filter(|u| u.should_auto_delete())
356 .map(|u| u.id)
357 .collect();
358 let count = expired_ids.len() as i32;
359 for id in expired_ids {
360 store.remove(&id);
361 }
362 Ok(count)
363 }
364 }
365
366 struct MockCampaignRepo {
369 campaigns: Mutex<HashMap<Uuid, EnergyCampaign>>,
370 offers: Mutex<HashMap<Uuid, ProviderOffer>>,
371 }
372
373 impl MockCampaignRepo {
374 fn new() -> Self {
375 Self {
376 campaigns: Mutex::new(HashMap::new()),
377 offers: Mutex::new(HashMap::new()),
378 }
379 }
380
381 fn with_campaign(campaign: EnergyCampaign) -> Self {
382 let mut map = HashMap::new();
383 map.insert(campaign.id, campaign);
384 Self {
385 campaigns: Mutex::new(map),
386 offers: Mutex::new(HashMap::new()),
387 }
388 }
389 }
390
391 #[async_trait]
392 impl EnergyCampaignRepository for MockCampaignRepo {
393 async fn create(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
394 let mut store = self.campaigns.lock().unwrap();
395 store.insert(campaign.id, campaign.clone());
396 Ok(campaign.clone())
397 }
398
399 async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
400 let store = self.campaigns.lock().unwrap();
401 Ok(store.get(&id).cloned())
402 }
403
404 async fn find_by_organization(
405 &self,
406 organization_id: Uuid,
407 ) -> Result<Vec<EnergyCampaign>, String> {
408 let store = self.campaigns.lock().unwrap();
409 Ok(store
410 .values()
411 .filter(|c| c.organization_id == organization_id)
412 .cloned()
413 .collect())
414 }
415
416 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<EnergyCampaign>, String> {
417 let store = self.campaigns.lock().unwrap();
418 Ok(store
419 .values()
420 .filter(|c| c.building_id == Some(building_id))
421 .cloned()
422 .collect())
423 }
424
425 async fn update(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
426 let mut store = self.campaigns.lock().unwrap();
427 store.insert(campaign.id, campaign.clone());
428 Ok(campaign.clone())
429 }
430
431 async fn delete(&self, id: Uuid) -> Result<(), String> {
432 let mut store = self.campaigns.lock().unwrap();
433 store.remove(&id);
434 Ok(())
435 }
436
437 async fn add_offer(
438 &self,
439 _campaign_id: Uuid,
440 offer: &ProviderOffer,
441 ) -> Result<ProviderOffer, String> {
442 let mut store = self.offers.lock().unwrap();
443 store.insert(offer.id, offer.clone());
444 Ok(offer.clone())
445 }
446
447 async fn get_offers(&self, campaign_id: Uuid) -> Result<Vec<ProviderOffer>, String> {
448 let store = self.offers.lock().unwrap();
449 Ok(store
450 .values()
451 .filter(|o| o.campaign_id == campaign_id)
452 .cloned()
453 .collect())
454 }
455
456 async fn update_offer(&self, offer: &ProviderOffer) -> Result<ProviderOffer, String> {
457 let mut store = self.offers.lock().unwrap();
458 store.insert(offer.id, offer.clone());
459 Ok(offer.clone())
460 }
461
462 async fn delete_offer(&self, offer_id: Uuid) -> Result<(), String> {
463 let mut store = self.offers.lock().unwrap();
464 store.remove(&offer_id);
465 Ok(())
466 }
467
468 async fn find_offer_by_id(&self, offer_id: Uuid) -> Result<Option<ProviderOffer>, String> {
469 let store = self.offers.lock().unwrap();
470 Ok(store.get(&offer_id).cloned())
471 }
472
473 async fn update_aggregation(
474 &self,
475 _campaign_id: Uuid,
476 _total_kwh_electricity: Option<f64>,
477 _total_kwh_gas: Option<f64>,
478 _avg_kwh_per_unit: Option<f64>,
479 ) -> Result<(), String> {
480 Ok(())
481 }
482 }
483
484 fn get_test_encryption_key() -> [u8; 32] {
487 *b"test_master_key_for_32bytes!##!!"
488 }
489
490 fn make_collecting_campaign() -> EnergyCampaign {
491 let mut campaign = EnergyCampaign::new(
492 Uuid::new_v4(),
493 Some(Uuid::new_v4()),
494 "Winter Campaign 2025".to_string(),
495 Utc::now() + chrono::Duration::days(30),
496 vec![EnergyType::Electricity],
497 Uuid::new_v4(),
498 )
499 .unwrap();
500 campaign.status = CampaignStatus::CollectingData;
502 campaign
503 }
504
505 fn make_upload(campaign_id: Uuid, unit_id: Uuid, building_id: Uuid) -> EnergyBillUpload {
506 let key = get_test_encryption_key();
507 EnergyBillUpload::new(
508 campaign_id,
509 unit_id,
510 building_id,
511 Uuid::new_v4(),
512 Utc::now() - chrono::Duration::days(365),
513 Utc::now(),
514 2400.0,
515 EnergyType::Electricity,
516 "1050".to_string(),
517 "abc123hash".to_string(),
518 "/encrypted/path".to_string(),
519 Uuid::new_v4(),
520 "192.168.1.1".to_string(),
521 "Mozilla/5.0".to_string(),
522 &key,
523 )
524 .unwrap()
525 }
526
527 #[tokio::test]
530 async fn test_upload_bill_success() {
531 let campaign = make_collecting_campaign();
532 let campaign_id = campaign.id;
533 let building_id = campaign.building_id.unwrap();
534 let unit_id = Uuid::new_v4();
535
536 let upload = make_upload(campaign_id, unit_id, building_id);
537
538 let uc = EnergyBillUploadUseCases::new(
539 Arc::new(MockUploadRepo::new()),
540 Arc::new(MockCampaignRepo::with_campaign(campaign)),
541 );
542
543 let result = uc.upload_bill(upload).await;
544 assert!(result.is_ok());
545 let created = result.unwrap();
546 assert_eq!(created.campaign_id, campaign_id);
547 assert_eq!(created.unit_id, unit_id);
548 }
549
550 #[tokio::test]
551 async fn test_upload_bill_campaign_not_collecting() {
552 let mut campaign = make_collecting_campaign();
553 campaign.status = CampaignStatus::Draft; let campaign_id = campaign.id;
555 let building_id = campaign.building_id.unwrap();
556 let unit_id = Uuid::new_v4();
557
558 let upload = make_upload(campaign_id, unit_id, building_id);
559
560 let uc = EnergyBillUploadUseCases::new(
561 Arc::new(MockUploadRepo::new()),
562 Arc::new(MockCampaignRepo::with_campaign(campaign)),
563 );
564
565 let result = uc.upload_bill(upload).await;
566 assert!(result.is_err());
567 assert_eq!(result.unwrap_err(), "Campaign is not collecting data");
568 }
569
570 #[tokio::test]
571 async fn test_upload_bill_duplicate_unit() {
572 let campaign = make_collecting_campaign();
573 let campaign_id = campaign.id;
574 let building_id = campaign.building_id.unwrap();
575 let unit_id = Uuid::new_v4();
576
577 let existing_upload = make_upload(campaign_id, unit_id, building_id);
579 let upload_repo = MockUploadRepo::with_upload(existing_upload);
580
581 let new_upload = make_upload(campaign_id, unit_id, building_id);
582
583 let uc = EnergyBillUploadUseCases::new(
584 Arc::new(upload_repo),
585 Arc::new(MockCampaignRepo::with_campaign(campaign)),
586 );
587
588 let result = uc.upload_bill(new_upload).await;
589 assert!(result.is_err());
590 assert_eq!(
591 result.unwrap_err(),
592 "Unit has already uploaded bill for this campaign"
593 );
594 }
595
596 #[tokio::test]
597 async fn test_get_upload_success() {
598 let campaign_id = Uuid::new_v4();
599 let unit_id = Uuid::new_v4();
600 let building_id = Uuid::new_v4();
601 let upload = make_upload(campaign_id, unit_id, building_id);
602 let upload_id = upload.id;
603
604 let uc = EnergyBillUploadUseCases::new(
605 Arc::new(MockUploadRepo::with_upload(upload)),
606 Arc::new(MockCampaignRepo::new()),
607 );
608
609 let result = uc.get_upload(upload_id).await;
610 assert!(result.is_ok());
611 assert!(result.unwrap().is_some());
612 }
613
614 #[tokio::test]
615 async fn test_get_upload_not_found() {
616 let uc = EnergyBillUploadUseCases::new(
617 Arc::new(MockUploadRepo::new()),
618 Arc::new(MockCampaignRepo::new()),
619 );
620
621 let result = uc.get_upload(Uuid::new_v4()).await;
622 assert!(result.is_ok());
623 assert!(result.unwrap().is_none());
624 }
625
626 #[tokio::test]
627 async fn test_verify_upload_success() {
628 let campaign_id = Uuid::new_v4();
629 let unit_id = Uuid::new_v4();
630 let building_id = Uuid::new_v4();
631 let upload = make_upload(campaign_id, unit_id, building_id);
632 let upload_id = upload.id;
633 let verifier_id = Uuid::new_v4();
634
635 let uc = EnergyBillUploadUseCases::new(
636 Arc::new(MockUploadRepo::with_upload(upload)),
637 Arc::new(MockCampaignRepo::new()),
638 );
639
640 let result = uc.verify_upload(upload_id, verifier_id).await;
641 assert!(result.is_ok());
642 let verified = result.unwrap();
643 assert!(verified.manually_verified);
644 assert_eq!(verified.verified_by, Some(verifier_id));
645 }
646
647 #[tokio::test]
648 async fn test_verify_upload_not_found() {
649 let uc = EnergyBillUploadUseCases::new(
650 Arc::new(MockUploadRepo::new()),
651 Arc::new(MockCampaignRepo::new()),
652 );
653
654 let result = uc.verify_upload(Uuid::new_v4(), Uuid::new_v4()).await;
655 assert!(result.is_err());
656 assert_eq!(result.unwrap_err(), "Upload not found");
657 }
658
659 #[tokio::test]
660 async fn test_delete_upload_success() {
661 let campaign_id = Uuid::new_v4();
662 let unit_id = Uuid::new_v4();
663 let building_id = Uuid::new_v4();
664 let upload = make_upload(campaign_id, unit_id, building_id);
665 let upload_id = upload.id;
666
667 let uc = EnergyBillUploadUseCases::new(
668 Arc::new(MockUploadRepo::with_upload(upload)),
669 Arc::new(MockCampaignRepo::new()),
670 );
671
672 let result = uc.delete_upload(upload_id, unit_id).await;
673 assert!(result.is_ok());
674 }
675
676 #[tokio::test]
677 async fn test_delete_upload_unauthorized() {
678 let campaign_id = Uuid::new_v4();
679 let unit_id = Uuid::new_v4();
680 let other_unit_id = Uuid::new_v4();
681 let building_id = Uuid::new_v4();
682 let upload = make_upload(campaign_id, unit_id, building_id);
683 let upload_id = upload.id;
684
685 let uc = EnergyBillUploadUseCases::new(
686 Arc::new(MockUploadRepo::with_upload(upload)),
687 Arc::new(MockCampaignRepo::new()),
688 );
689
690 let result = uc.delete_upload(upload_id, other_unit_id).await;
691 assert!(result.is_err());
692 assert!(result
693 .unwrap_err()
694 .contains("Unauthorized: You can only delete your own data"));
695 }
696
697 #[tokio::test]
698 async fn test_check_k_anonymity_met() {
699 let campaign_id = Uuid::new_v4();
700 let building_id = Uuid::new_v4();
701
702 let mut upload_map = HashMap::new();
704 for _ in 0..5 {
705 let mut upload = make_upload(campaign_id, Uuid::new_v4(), building_id);
706 upload.mark_verified(Uuid::new_v4()).unwrap();
707 upload_map.insert(upload.id, upload);
708 }
709 let upload_repo = MockUploadRepo {
710 uploads: Mutex::new(upload_map),
711 };
712
713 let uc =
714 EnergyBillUploadUseCases::new(Arc::new(upload_repo), Arc::new(MockCampaignRepo::new()));
715
716 let result = uc.check_k_anonymity(campaign_id).await;
717 assert!(result.is_ok());
718 assert!(result.unwrap()); }
720
721 #[tokio::test]
722 async fn test_check_k_anonymity_not_met() {
723 let campaign_id = Uuid::new_v4();
724 let building_id = Uuid::new_v4();
725
726 let mut upload_map = HashMap::new();
728 for _ in 0..3 {
729 let mut upload = make_upload(campaign_id, Uuid::new_v4(), building_id);
730 upload.mark_verified(Uuid::new_v4()).unwrap();
731 upload_map.insert(upload.id, upload);
732 }
733 let upload_repo = MockUploadRepo {
734 uploads: Mutex::new(upload_map),
735 };
736
737 let uc =
738 EnergyBillUploadUseCases::new(Arc::new(upload_repo), Arc::new(MockCampaignRepo::new()));
739
740 let result = uc.check_k_anonymity(campaign_id).await;
741 assert!(result.is_ok());
742 assert!(!result.unwrap()); }
744}