koprogo_api/application/use_cases/
energy_campaign_use_cases.rs

1use std::sync::Arc;
2use uuid::Uuid;
3
4use crate::application::ports::{
5    BuildingRepository, EnergyBillUploadRepository, EnergyCampaignRepository,
6};
7use crate::domain::entities::{CampaignStatus, EnergyCampaign, ProviderOffer};
8
9pub struct EnergyCampaignUseCases {
10    campaign_repo: Arc<dyn EnergyCampaignRepository>,
11    bill_upload_repo: Arc<dyn EnergyBillUploadRepository>,
12    building_repo: Arc<dyn BuildingRepository>,
13}
14
15impl EnergyCampaignUseCases {
16    pub fn new(
17        campaign_repo: Arc<dyn EnergyCampaignRepository>,
18        bill_upload_repo: Arc<dyn EnergyBillUploadRepository>,
19        building_repo: Arc<dyn BuildingRepository>,
20    ) -> Self {
21        Self {
22            campaign_repo,
23            bill_upload_repo,
24            building_repo,
25        }
26    }
27
28    /// Create a new energy campaign
29    pub async fn create_campaign(
30        &self,
31        campaign: EnergyCampaign,
32    ) -> Result<EnergyCampaign, String> {
33        // Validate building exists if building_id is provided
34        if let Some(building_id) = campaign.building_id {
35            let building = self
36                .building_repo
37                .find_by_id(building_id)
38                .await
39                .map_err(|e| format!("Failed to validate building: {}", e))?;
40
41            if building.is_none() {
42                return Err("Building not found".to_string());
43            }
44        }
45
46        self.campaign_repo.create(&campaign).await
47    }
48
49    /// Get campaign by ID
50    pub async fn get_campaign(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
51        self.campaign_repo.find_by_id(id).await
52    }
53
54    /// Get all campaigns for an organization
55    pub async fn get_campaigns_by_organization(
56        &self,
57        organization_id: Uuid,
58    ) -> Result<Vec<EnergyCampaign>, String> {
59        self.campaign_repo
60            .find_by_organization(organization_id)
61            .await
62    }
63
64    /// Get all campaigns for a building
65    pub async fn get_campaigns_by_building(
66        &self,
67        building_id: Uuid,
68    ) -> Result<Vec<EnergyCampaign>, String> {
69        self.campaign_repo.find_by_building(building_id).await
70    }
71
72    /// Update campaign status
73    pub async fn update_campaign_status(
74        &self,
75        id: Uuid,
76        new_status: CampaignStatus,
77    ) -> Result<EnergyCampaign, String> {
78        let mut campaign = self
79            .campaign_repo
80            .find_by_id(id)
81            .await?
82            .ok_or_else(|| "Campaign not found".to_string())?;
83
84        campaign.status = new_status;
85        self.campaign_repo.update(&campaign).await
86    }
87
88    /// Add provider offer to campaign
89    pub async fn add_offer(
90        &self,
91        campaign_id: Uuid,
92        offer: ProviderOffer,
93    ) -> Result<ProviderOffer, String> {
94        // Verify campaign exists and is in Negotiating status
95        let campaign = self
96            .campaign_repo
97            .find_by_id(campaign_id)
98            .await?
99            .ok_or_else(|| "Campaign not found".to_string())?;
100
101        if campaign.status != CampaignStatus::Negotiating {
102            return Err("Campaign must be in Negotiating status to add offers".to_string());
103        }
104
105        self.campaign_repo.add_offer(campaign_id, &offer).await
106    }
107
108    /// Get all offers for a campaign
109    pub async fn get_campaign_offers(
110        &self,
111        campaign_id: Uuid,
112    ) -> Result<Vec<ProviderOffer>, String> {
113        self.campaign_repo.get_offers(campaign_id).await
114    }
115
116    /// Select winning offer
117    pub async fn select_offer(
118        &self,
119        campaign_id: Uuid,
120        offer_id: Uuid,
121    ) -> Result<EnergyCampaign, String> {
122        let mut campaign = self
123            .campaign_repo
124            .find_by_id(campaign_id)
125            .await?
126            .ok_or_else(|| "Campaign not found".to_string())?;
127
128        // Validate offer exists
129        let offer = self
130            .campaign_repo
131            .find_offer_by_id(offer_id)
132            .await?
133            .ok_or_else(|| "Offer not found".to_string())?;
134
135        if offer.campaign_id != campaign_id {
136            return Err("Offer does not belong to this campaign".to_string());
137        }
138
139        campaign.select_offer(offer_id)?;
140        self.campaign_repo.update(&campaign).await
141    }
142
143    /// Calculate and update campaign aggregations (called after bill uploads)
144    pub async fn update_campaign_aggregation(
145        &self,
146        campaign_id: Uuid,
147        encryption_key: &[u8; 32],
148    ) -> Result<(), String> {
149        // Get all verified bill uploads for this campaign
150        let uploads = self
151            .bill_upload_repo
152            .find_verified_by_campaign(campaign_id)
153            .await?;
154
155        if uploads.is_empty() {
156            return Ok(());
157        }
158
159        // Decrypt and aggregate consumption data
160        let mut total_kwh_electricity = 0.0;
161        let mut total_kwh_gas = 0.0;
162        let mut count_electricity = 0;
163        let mut count_gas = 0;
164
165        for upload in &uploads {
166            let kwh = upload.decrypt_kwh(encryption_key)?;
167
168            match upload.energy_type {
169                crate::domain::entities::EnergyType::Electricity => {
170                    total_kwh_electricity += kwh;
171                    count_electricity += 1;
172                }
173                crate::domain::entities::EnergyType::Gas => {
174                    total_kwh_gas += kwh;
175                    count_gas += 1;
176                }
177                crate::domain::entities::EnergyType::Both => {
178                    // For "Both", split 50/50 (simplified logic)
179                    total_kwh_electricity += kwh / 2.0;
180                    total_kwh_gas += kwh / 2.0;
181                    count_electricity += 1;
182                    count_gas += 1;
183                }
184            }
185        }
186
187        let total_kwh_elec = if count_electricity > 0 {
188            Some(total_kwh_electricity)
189        } else {
190            None
191        };
192
193        let total_kwh_g = if count_gas > 0 {
194            Some(total_kwh_gas)
195        } else {
196            None
197        };
198
199        let avg_kwh = if !uploads.is_empty() {
200            Some((total_kwh_electricity + total_kwh_gas) / uploads.len() as f64)
201        } else {
202            None
203        };
204
205        // Update campaign with aggregated data
206        self.campaign_repo
207            .update_aggregation(campaign_id, total_kwh_elec, total_kwh_g, avg_kwh)
208            .await
209    }
210
211    /// Get campaign statistics (anonymized)
212    pub async fn get_campaign_stats(&self, campaign_id: Uuid) -> Result<CampaignStats, String> {
213        let campaign = self
214            .campaign_repo
215            .find_by_id(campaign_id)
216            .await?
217            .ok_or_else(|| "Campaign not found".to_string())?;
218
219        // Get building total units if building_id is set
220        let total_units = if let Some(building_id) = campaign.building_id {
221            let building = self
222                .building_repo
223                .find_by_id(building_id)
224                .await?
225                .ok_or_else(|| "Building not found".to_string())?;
226            building.total_units
227        } else {
228            // For multi-building campaigns, we'd need to sum all buildings
229            0
230        };
231
232        let participation_rate = if total_units > 0 {
233            campaign.participation_rate(total_units)
234        } else {
235            0.0
236        };
237
238        let can_negotiate = if total_units > 0 {
239            campaign.can_negotiate(total_units)
240        } else {
241            false
242        };
243
244        Ok(CampaignStats {
245            total_participants: campaign.total_participants,
246            participation_rate,
247            total_kwh_electricity: campaign.total_kwh_electricity,
248            total_kwh_gas: campaign.total_kwh_gas,
249            avg_kwh_per_unit: campaign.avg_kwh_per_unit,
250            can_negotiate,
251            estimated_savings_pct: campaign.estimated_savings_pct,
252        })
253    }
254
255    /// Finalize campaign (after final vote)
256    pub async fn finalize_campaign(&self, id: Uuid) -> Result<EnergyCampaign, String> {
257        let mut campaign = self
258            .campaign_repo
259            .find_by_id(id)
260            .await?
261            .ok_or_else(|| "Campaign not found".to_string())?;
262
263        campaign.finalize()?;
264        self.campaign_repo.update(&campaign).await
265    }
266
267    /// Complete campaign (contracts signed)
268    pub async fn complete_campaign(&self, id: Uuid) -> Result<EnergyCampaign, String> {
269        let mut campaign = self
270            .campaign_repo
271            .find_by_id(id)
272            .await?
273            .ok_or_else(|| "Campaign not found".to_string())?;
274
275        campaign.complete()?;
276        self.campaign_repo.update(&campaign).await
277    }
278
279    /// Cancel campaign
280    pub async fn cancel_campaign(&self, id: Uuid) -> Result<EnergyCampaign, String> {
281        let mut campaign = self
282            .campaign_repo
283            .find_by_id(id)
284            .await?
285            .ok_or_else(|| "Campaign not found".to_string())?;
286
287        campaign.cancel()?;
288        self.campaign_repo.update(&campaign).await
289    }
290
291    /// Delete campaign
292    pub async fn delete_campaign(&self, id: Uuid) -> Result<(), String> {
293        self.campaign_repo.delete(id).await
294    }
295}
296
297#[derive(Debug, serde::Serialize, serde::Deserialize)]
298pub struct CampaignStats {
299    pub total_participants: i32,
300    pub participation_rate: f64,
301    pub total_kwh_electricity: Option<f64>,
302    pub total_kwh_gas: Option<f64>,
303    pub avg_kwh_per_unit: Option<f64>,
304    pub can_negotiate: bool,
305    pub estimated_savings_pct: Option<f64>,
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::application::dto::{BuildingFilters, PageRequest};
312    use crate::application::ports::{
313        BuildingRepository, EnergyBillUploadRepository, EnergyCampaignRepository,
314    };
315    use crate::domain::entities::{
316        Building, CampaignStatus, EnergyBillUpload, EnergyCampaign, EnergyType, ProviderOffer,
317    };
318    use async_trait::async_trait;
319    use chrono::Utc;
320    use std::collections::HashMap;
321    use std::sync::Mutex;
322    use uuid::Uuid;
323
324    // ─── Mock EnergyCampaignRepository ──────────────────────────────────
325
326    struct MockCampaignRepo {
327        campaigns: Mutex<HashMap<Uuid, EnergyCampaign>>,
328        offers: Mutex<HashMap<Uuid, ProviderOffer>>,
329    }
330
331    impl MockCampaignRepo {
332        fn new() -> Self {
333            Self {
334                campaigns: Mutex::new(HashMap::new()),
335                offers: Mutex::new(HashMap::new()),
336            }
337        }
338
339        fn with_campaign(campaign: EnergyCampaign) -> Self {
340            let mut map = HashMap::new();
341            map.insert(campaign.id, campaign);
342            Self {
343                campaigns: Mutex::new(map),
344                offers: Mutex::new(HashMap::new()),
345            }
346        }
347
348        fn with_campaign_and_offer(campaign: EnergyCampaign, offer: ProviderOffer) -> Self {
349            let mut c_map = HashMap::new();
350            c_map.insert(campaign.id, campaign);
351            let mut o_map = HashMap::new();
352            o_map.insert(offer.id, offer);
353            Self {
354                campaigns: Mutex::new(c_map),
355                offers: Mutex::new(o_map),
356            }
357        }
358    }
359
360    #[async_trait]
361    impl EnergyCampaignRepository for MockCampaignRepo {
362        async fn create(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
363            let mut store = self.campaigns.lock().unwrap();
364            store.insert(campaign.id, campaign.clone());
365            Ok(campaign.clone())
366        }
367
368        async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
369            let store = self.campaigns.lock().unwrap();
370            Ok(store.get(&id).cloned())
371        }
372
373        async fn find_by_organization(
374            &self,
375            organization_id: Uuid,
376        ) -> Result<Vec<EnergyCampaign>, String> {
377            let store = self.campaigns.lock().unwrap();
378            Ok(store
379                .values()
380                .filter(|c| c.organization_id == organization_id)
381                .cloned()
382                .collect())
383        }
384
385        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<EnergyCampaign>, String> {
386            let store = self.campaigns.lock().unwrap();
387            Ok(store
388                .values()
389                .filter(|c| c.building_id == Some(building_id))
390                .cloned()
391                .collect())
392        }
393
394        async fn update(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
395            let mut store = self.campaigns.lock().unwrap();
396            store.insert(campaign.id, campaign.clone());
397            Ok(campaign.clone())
398        }
399
400        async fn delete(&self, id: Uuid) -> Result<(), String> {
401            let mut store = self.campaigns.lock().unwrap();
402            store.remove(&id);
403            Ok(())
404        }
405
406        async fn add_offer(
407            &self,
408            _campaign_id: Uuid,
409            offer: &ProviderOffer,
410        ) -> Result<ProviderOffer, String> {
411            let mut store = self.offers.lock().unwrap();
412            store.insert(offer.id, offer.clone());
413            Ok(offer.clone())
414        }
415
416        async fn get_offers(&self, campaign_id: Uuid) -> Result<Vec<ProviderOffer>, String> {
417            let store = self.offers.lock().unwrap();
418            Ok(store
419                .values()
420                .filter(|o| o.campaign_id == campaign_id)
421                .cloned()
422                .collect())
423        }
424
425        async fn update_offer(&self, offer: &ProviderOffer) -> Result<ProviderOffer, String> {
426            let mut store = self.offers.lock().unwrap();
427            store.insert(offer.id, offer.clone());
428            Ok(offer.clone())
429        }
430
431        async fn delete_offer(&self, offer_id: Uuid) -> Result<(), String> {
432            let mut store = self.offers.lock().unwrap();
433            store.remove(&offer_id);
434            Ok(())
435        }
436
437        async fn find_offer_by_id(&self, offer_id: Uuid) -> Result<Option<ProviderOffer>, String> {
438            let store = self.offers.lock().unwrap();
439            Ok(store.get(&offer_id).cloned())
440        }
441
442        async fn update_aggregation(
443            &self,
444            _campaign_id: Uuid,
445            _total_kwh_electricity: Option<f64>,
446            _total_kwh_gas: Option<f64>,
447            _avg_kwh_per_unit: Option<f64>,
448        ) -> Result<(), String> {
449            Ok(())
450        }
451    }
452
453    // ─── Mock EnergyBillUploadRepository ────────────────────────────────
454
455    struct MockUploadRepo {
456        uploads: Mutex<HashMap<Uuid, EnergyBillUpload>>,
457    }
458
459    impl MockUploadRepo {
460        fn new() -> Self {
461            Self {
462                uploads: Mutex::new(HashMap::new()),
463            }
464        }
465    }
466
467    #[async_trait]
468    impl EnergyBillUploadRepository for MockUploadRepo {
469        async fn create(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
470            let mut store = self.uploads.lock().unwrap();
471            store.insert(upload.id, upload.clone());
472            Ok(upload.clone())
473        }
474
475        async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyBillUpload>, String> {
476            let store = self.uploads.lock().unwrap();
477            Ok(store.get(&id).cloned())
478        }
479
480        async fn find_by_campaign(
481            &self,
482            campaign_id: Uuid,
483        ) -> Result<Vec<EnergyBillUpload>, String> {
484            let store = self.uploads.lock().unwrap();
485            Ok(store
486                .values()
487                .filter(|u| u.campaign_id == campaign_id)
488                .cloned()
489                .collect())
490        }
491
492        async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<EnergyBillUpload>, String> {
493            let store = self.uploads.lock().unwrap();
494            Ok(store
495                .values()
496                .filter(|u| u.unit_id == unit_id)
497                .cloned()
498                .collect())
499        }
500
501        async fn find_by_campaign_and_unit(
502            &self,
503            campaign_id: Uuid,
504            unit_id: Uuid,
505        ) -> Result<Option<EnergyBillUpload>, String> {
506            let store = self.uploads.lock().unwrap();
507            Ok(store
508                .values()
509                .find(|u| u.campaign_id == campaign_id && u.unit_id == unit_id)
510                .cloned())
511        }
512
513        async fn find_by_uploaded_by(
514            &self,
515            uploaded_by: Uuid,
516        ) -> Result<Vec<EnergyBillUpload>, String> {
517            let store = self.uploads.lock().unwrap();
518            Ok(store
519                .values()
520                .filter(|u| u.uploaded_by == uploaded_by)
521                .cloned()
522                .collect())
523        }
524
525        async fn find_by_building(
526            &self,
527            building_id: Uuid,
528        ) -> Result<Vec<EnergyBillUpload>, String> {
529            let store = self.uploads.lock().unwrap();
530            Ok(store
531                .values()
532                .filter(|u| u.building_id == building_id)
533                .cloned()
534                .collect())
535        }
536
537        async fn update(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
538            let mut store = self.uploads.lock().unwrap();
539            store.insert(upload.id, upload.clone());
540            Ok(upload.clone())
541        }
542
543        async fn delete(&self, id: Uuid) -> Result<(), String> {
544            let mut store = self.uploads.lock().unwrap();
545            store.remove(&id);
546            Ok(())
547        }
548
549        async fn find_expired(&self) -> Result<Vec<EnergyBillUpload>, String> {
550            Ok(vec![])
551        }
552
553        async fn count_verified_by_campaign(&self, _campaign_id: Uuid) -> Result<i32, String> {
554            Ok(0)
555        }
556
557        async fn find_verified_by_campaign(
558            &self,
559            _campaign_id: Uuid,
560        ) -> Result<Vec<EnergyBillUpload>, String> {
561            Ok(vec![])
562        }
563
564        async fn delete_expired(&self) -> Result<i32, String> {
565            Ok(0)
566        }
567    }
568
569    // ─── Mock BuildingRepository ────────────────────────────────────────
570
571    struct MockBuildingRepo {
572        buildings: Mutex<HashMap<Uuid, Building>>,
573    }
574
575    impl MockBuildingRepo {
576        fn new() -> Self {
577            Self {
578                buildings: Mutex::new(HashMap::new()),
579            }
580        }
581
582        fn with_building(building: Building) -> Self {
583            let mut map = HashMap::new();
584            map.insert(building.id, building);
585            Self {
586                buildings: Mutex::new(map),
587            }
588        }
589    }
590
591    #[async_trait]
592    impl BuildingRepository for MockBuildingRepo {
593        async fn create(&self, building: &Building) -> Result<Building, String> {
594            let mut store = self.buildings.lock().unwrap();
595            store.insert(building.id, building.clone());
596            Ok(building.clone())
597        }
598
599        async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String> {
600            let store = self.buildings.lock().unwrap();
601            Ok(store.get(&id).cloned())
602        }
603
604        async fn find_all(&self) -> Result<Vec<Building>, String> {
605            let store = self.buildings.lock().unwrap();
606            Ok(store.values().cloned().collect())
607        }
608
609        async fn find_all_paginated(
610            &self,
611            _page_request: &PageRequest,
612            _filters: &BuildingFilters,
613        ) -> Result<(Vec<Building>, i64), String> {
614            let store = self.buildings.lock().unwrap();
615            let all: Vec<Building> = store.values().cloned().collect();
616            let count = all.len() as i64;
617            Ok((all, count))
618        }
619
620        async fn update(&self, building: &Building) -> Result<Building, String> {
621            let mut store = self.buildings.lock().unwrap();
622            store.insert(building.id, building.clone());
623            Ok(building.clone())
624        }
625
626        async fn delete(&self, id: Uuid) -> Result<bool, String> {
627            let mut store = self.buildings.lock().unwrap();
628            Ok(store.remove(&id).is_some())
629        }
630
631        async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String> {
632            let store = self.buildings.lock().unwrap();
633            Ok(store
634                .values()
635                .find(|b| b.slug.as_deref() == Some(slug))
636                .cloned())
637        }
638    }
639
640    // ─── Helpers ────────────────────────────────────────────────────────
641
642    fn make_building(id: Uuid) -> Building {
643        let org_id = Uuid::new_v4();
644        let mut building = Building::new(
645            org_id,
646            "Test Building".to_string(),
647            "123 Test Street".to_string(),
648            "Brussels".to_string(),
649            "1000".to_string(),
650            "Belgium".to_string(),
651            25,
652            1000,
653            Some(1990),
654        )
655        .unwrap();
656        building.id = id;
657        building
658    }
659
660    fn make_campaign_for_building(building_id: Uuid) -> EnergyCampaign {
661        EnergyCampaign::new(
662            Uuid::new_v4(),
663            Some(building_id),
664            "Winter Campaign 2025".to_string(),
665            Utc::now() + chrono::Duration::days(30),
666            vec![EnergyType::Electricity],
667            Uuid::new_v4(),
668        )
669        .unwrap()
670    }
671
672    fn make_negotiating_campaign(building_id: Uuid) -> EnergyCampaign {
673        let mut campaign = make_campaign_for_building(building_id);
674        campaign.status = CampaignStatus::Negotiating;
675        campaign
676    }
677
678    fn make_offer(campaign_id: Uuid) -> ProviderOffer {
679        ProviderOffer::new(
680            campaign_id,
681            "Lampiris".to_string(),
682            Some(0.27),
683            None,
684            12.50,
685            100.0,
686            12,
687            15.0,
688            Utc::now() + chrono::Duration::days(30),
689        )
690        .unwrap()
691    }
692
693    // ─── Tests ──────────────────────────────────────────────────────────
694
695    #[tokio::test]
696    async fn test_create_campaign_success() {
697        let building_id = Uuid::new_v4();
698        let building = make_building(building_id);
699        let campaign = make_campaign_for_building(building_id);
700
701        let uc = EnergyCampaignUseCases::new(
702            Arc::new(MockCampaignRepo::new()),
703            Arc::new(MockUploadRepo::new()),
704            Arc::new(MockBuildingRepo::with_building(building)),
705        );
706
707        let result = uc.create_campaign(campaign).await;
708        assert!(result.is_ok());
709        let created = result.unwrap();
710        assert_eq!(created.status, CampaignStatus::Draft);
711        assert_eq!(created.building_id, Some(building_id));
712    }
713
714    #[tokio::test]
715    async fn test_create_campaign_building_not_found() {
716        let building_id = Uuid::new_v4();
717        let campaign = make_campaign_for_building(building_id);
718
719        let uc = EnergyCampaignUseCases::new(
720            Arc::new(MockCampaignRepo::new()),
721            Arc::new(MockUploadRepo::new()),
722            Arc::new(MockBuildingRepo::new()), // Empty -- building not found
723        );
724
725        let result = uc.create_campaign(campaign).await;
726        assert!(result.is_err());
727        assert_eq!(result.unwrap_err(), "Building not found");
728    }
729
730    #[tokio::test]
731    async fn test_create_campaign_no_building_id() {
732        // Campaign without building_id (multi-building) should succeed without building check
733        let campaign = EnergyCampaign::new(
734            Uuid::new_v4(),
735            None, // No building_id
736            "Multi-Building Campaign".to_string(),
737            Utc::now() + chrono::Duration::days(30),
738            vec![EnergyType::Both],
739            Uuid::new_v4(),
740        )
741        .unwrap();
742
743        let uc = EnergyCampaignUseCases::new(
744            Arc::new(MockCampaignRepo::new()),
745            Arc::new(MockUploadRepo::new()),
746            Arc::new(MockBuildingRepo::new()),
747        );
748
749        let result = uc.create_campaign(campaign).await;
750        assert!(result.is_ok());
751    }
752
753    #[tokio::test]
754    async fn test_get_campaign_success() {
755        let building_id = Uuid::new_v4();
756        let campaign = make_campaign_for_building(building_id);
757        let campaign_id = campaign.id;
758
759        let uc = EnergyCampaignUseCases::new(
760            Arc::new(MockCampaignRepo::with_campaign(campaign)),
761            Arc::new(MockUploadRepo::new()),
762            Arc::new(MockBuildingRepo::new()),
763        );
764
765        let result = uc.get_campaign(campaign_id).await;
766        assert!(result.is_ok());
767        let found = result.unwrap();
768        assert!(found.is_some());
769        assert_eq!(found.unwrap().id, campaign_id);
770    }
771
772    #[tokio::test]
773    async fn test_get_campaign_not_found() {
774        let uc = EnergyCampaignUseCases::new(
775            Arc::new(MockCampaignRepo::new()),
776            Arc::new(MockUploadRepo::new()),
777            Arc::new(MockBuildingRepo::new()),
778        );
779
780        let result = uc.get_campaign(Uuid::new_v4()).await;
781        assert!(result.is_ok());
782        assert!(result.unwrap().is_none());
783    }
784
785    #[tokio::test]
786    async fn test_add_offer_success() {
787        let building_id = Uuid::new_v4();
788        let campaign = make_negotiating_campaign(building_id);
789        let campaign_id = campaign.id;
790        let offer = make_offer(campaign_id);
791
792        let uc = EnergyCampaignUseCases::new(
793            Arc::new(MockCampaignRepo::with_campaign(campaign)),
794            Arc::new(MockUploadRepo::new()),
795            Arc::new(MockBuildingRepo::new()),
796        );
797
798        let result = uc.add_offer(campaign_id, offer).await;
799        assert!(result.is_ok());
800        let created_offer = result.unwrap();
801        assert_eq!(created_offer.provider_name, "Lampiris");
802    }
803
804    #[tokio::test]
805    async fn test_add_offer_wrong_status() {
806        let building_id = Uuid::new_v4();
807        let campaign = make_campaign_for_building(building_id); // Draft status
808        let campaign_id = campaign.id;
809        let offer = make_offer(campaign_id);
810
811        let uc = EnergyCampaignUseCases::new(
812            Arc::new(MockCampaignRepo::with_campaign(campaign)),
813            Arc::new(MockUploadRepo::new()),
814            Arc::new(MockBuildingRepo::new()),
815        );
816
817        let result = uc.add_offer(campaign_id, offer).await;
818        assert!(result.is_err());
819        assert!(result
820            .unwrap_err()
821            .contains("Campaign must be in Negotiating status to add offers"));
822    }
823
824    #[tokio::test]
825    async fn test_select_offer_success() {
826        let building_id = Uuid::new_v4();
827        let mut campaign = make_negotiating_campaign(building_id);
828        let campaign_id = campaign.id;
829        let offer = make_offer(campaign_id);
830        let offer_id = offer.id;
831
832        // Add the offer to campaign's offers_received so select_offer domain method works
833        campaign.offers_received.push(offer.clone());
834
835        let uc = EnergyCampaignUseCases::new(
836            Arc::new(MockCampaignRepo::with_campaign_and_offer(campaign, offer)),
837            Arc::new(MockUploadRepo::new()),
838            Arc::new(MockBuildingRepo::new()),
839        );
840
841        let result = uc.select_offer(campaign_id, offer_id).await;
842        assert!(result.is_ok());
843        let updated = result.unwrap();
844        assert_eq!(updated.selected_offer_id, Some(offer_id));
845    }
846
847    #[tokio::test]
848    async fn test_select_offer_not_in_campaign() {
849        let building_id = Uuid::new_v4();
850        let campaign = make_negotiating_campaign(building_id);
851        let campaign_id = campaign.id;
852
853        // Create an offer that belongs to a different campaign
854        let other_campaign_id = Uuid::new_v4();
855        let offer = make_offer(other_campaign_id);
856        let offer_id = offer.id;
857
858        let repo = MockCampaignRepo::with_campaign_and_offer(campaign, offer);
859
860        let uc = EnergyCampaignUseCases::new(
861            Arc::new(repo),
862            Arc::new(MockUploadRepo::new()),
863            Arc::new(MockBuildingRepo::new()),
864        );
865
866        let result = uc.select_offer(campaign_id, offer_id).await;
867        assert!(result.is_err());
868        assert!(result
869            .unwrap_err()
870            .contains("Offer does not belong to this campaign"));
871    }
872
873    #[tokio::test]
874    async fn test_cancel_campaign_success() {
875        let building_id = Uuid::new_v4();
876        let campaign = make_campaign_for_building(building_id);
877        let campaign_id = campaign.id;
878
879        let uc = EnergyCampaignUseCases::new(
880            Arc::new(MockCampaignRepo::with_campaign(campaign)),
881            Arc::new(MockUploadRepo::new()),
882            Arc::new(MockBuildingRepo::new()),
883        );
884
885        let result = uc.cancel_campaign(campaign_id).await;
886        assert!(result.is_ok());
887        assert_eq!(result.unwrap().status, CampaignStatus::Cancelled);
888    }
889
890    #[tokio::test]
891    async fn test_cancel_campaign_not_found() {
892        let uc = EnergyCampaignUseCases::new(
893            Arc::new(MockCampaignRepo::new()),
894            Arc::new(MockUploadRepo::new()),
895            Arc::new(MockBuildingRepo::new()),
896        );
897
898        let result = uc.cancel_campaign(Uuid::new_v4()).await;
899        assert!(result.is_err());
900        assert_eq!(result.unwrap_err(), "Campaign not found");
901    }
902
903    #[tokio::test]
904    async fn test_delete_campaign_success() {
905        let building_id = Uuid::new_v4();
906        let campaign = make_campaign_for_building(building_id);
907        let campaign_id = campaign.id;
908
909        let uc = EnergyCampaignUseCases::new(
910            Arc::new(MockCampaignRepo::with_campaign(campaign)),
911            Arc::new(MockUploadRepo::new()),
912            Arc::new(MockBuildingRepo::new()),
913        );
914
915        let result = uc.delete_campaign(campaign_id).await;
916        assert!(result.is_ok());
917    }
918}