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 (by the user who uploaded them)
63    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    /// 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, 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    // ─── Mock EnergyBillUploadRepository ────────────────────────────────
219
220    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    // ─── Mock EnergyCampaignRepository ──────────────────────────────────
367
368    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    // ─── Helpers ────────────────────────────────────────────────────────
485
486    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        // Move to CollectingData status
501        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    // ─── Tests ──────────────────────────────────────────────────────────
528
529    #[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; // Not collecting
554        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        // Pre-populate the repo with an existing upload for the same unit+campaign
578        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        // Create 5 verified uploads
703        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()); // k >= 5
719    }
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        // Create only 3 verified uploads (below threshold of 5)
727        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()); // k < 5
743    }
744}