koprogo_api/application/use_cases/
energy_bill_upload_use_cases.rs

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    /// Upload energy bill with GDPR consent
24    pub async fn upload_bill(&self, upload: EnergyBillUpload) -> Result<EnergyBillUpload, String> {
25        // Validate campaign exists and is accepting uploads
26        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        // Check if unit already uploaded for this campaign
37        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    /// Get upload by ID
50    pub async fn get_upload(&self, id: Uuid) -> Result<Option<EnergyBillUpload>, String> {
51        self.upload_repo.find_by_id(id).await
52    }
53
54    /// Get all uploads for a campaign
55    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    /// Get my uploads (for a specific unit)
63    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    /// Verify upload (manual verification by admin)
68    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    /// Anonymize upload (add to building aggregate)
84    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    /// Batch anonymize all verified uploads for a campaign
96    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    /// Decrypt consumption data (requires encryption key and ownership)
116    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        // Verify requester owns this unit (authorization check)
129        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    /// Delete upload (GDPR Art. 17 - Right to erasure)
137    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        // Verify requester owns this unit
149        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    /// Withdraw consent (GDPR Art. 7.3 - Immediate deletion)
159    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        // Verify requester owns this unit
171        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    /// Get count of verified uploads for a campaign (k-anonymity check)
181    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    /// Check if k-anonymity threshold is met (minimum 5 participants)
188    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    /// Auto-delete expired uploads (GDPR retention policy)
194    pub async fn cleanup_expired(&self) -> Result<i32, String> {
195        self.upload_repo.delete_expired().await
196    }
197
198    /// Get expired uploads count (for reporting)
199    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    // ─── Mock EnergyBillUploadRepository ────────────────────────────────
220
221    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    // ─── Mock EnergyCampaignRepository ──────────────────────────────────
356
357    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    // ─── Helpers ────────────────────────────────────────────────────────
474
475    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        // Move to CollectingData status
490        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    // ─── Tests ──────────────────────────────────────────────────────────
517
518    #[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; // Not collecting
543        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        // Pre-populate the repo with an existing upload for the same unit+campaign
567        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        // Create 5 verified uploads
692        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()); // k >= 5
708    }
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        // Create only 3 verified uploads (below threshold of 5)
716        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()); // k < 5
732    }
733}