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, CampaignType, ContractType, EnergyBillUpload, EnergyCampaign,
317        EnergyType, ProviderOffer,
318    };
319    use async_trait::async_trait;
320    use chrono::Utc;
321    use std::collections::HashMap;
322    use std::sync::Mutex;
323    use uuid::Uuid;
324
325    // ─── Mock EnergyCampaignRepository ──────────────────────────────────
326
327    struct MockCampaignRepo {
328        campaigns: Mutex<HashMap<Uuid, EnergyCampaign>>,
329        offers: Mutex<HashMap<Uuid, ProviderOffer>>,
330    }
331
332    impl MockCampaignRepo {
333        fn new() -> Self {
334            Self {
335                campaigns: Mutex::new(HashMap::new()),
336                offers: Mutex::new(HashMap::new()),
337            }
338        }
339
340        fn with_campaign(campaign: EnergyCampaign) -> Self {
341            let mut map = HashMap::new();
342            map.insert(campaign.id, campaign);
343            Self {
344                campaigns: Mutex::new(map),
345                offers: Mutex::new(HashMap::new()),
346            }
347        }
348
349        fn with_campaign_and_offer(campaign: EnergyCampaign, offer: ProviderOffer) -> Self {
350            let mut c_map = HashMap::new();
351            c_map.insert(campaign.id, campaign);
352            let mut o_map = HashMap::new();
353            o_map.insert(offer.id, offer);
354            Self {
355                campaigns: Mutex::new(c_map),
356                offers: Mutex::new(o_map),
357            }
358        }
359    }
360
361    #[async_trait]
362    impl EnergyCampaignRepository for MockCampaignRepo {
363        async fn create(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
364            let mut store = self.campaigns.lock().unwrap();
365            store.insert(campaign.id, campaign.clone());
366            Ok(campaign.clone())
367        }
368
369        async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
370            let store = self.campaigns.lock().unwrap();
371            Ok(store.get(&id).cloned())
372        }
373
374        async fn find_by_organization(
375            &self,
376            organization_id: Uuid,
377        ) -> Result<Vec<EnergyCampaign>, String> {
378            let store = self.campaigns.lock().unwrap();
379            Ok(store
380                .values()
381                .filter(|c| c.organization_id == organization_id)
382                .cloned()
383                .collect())
384        }
385
386        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<EnergyCampaign>, String> {
387            let store = self.campaigns.lock().unwrap();
388            Ok(store
389                .values()
390                .filter(|c| c.building_id == Some(building_id))
391                .cloned()
392                .collect())
393        }
394
395        async fn update(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
396            let mut store = self.campaigns.lock().unwrap();
397            store.insert(campaign.id, campaign.clone());
398            Ok(campaign.clone())
399        }
400
401        async fn delete(&self, id: Uuid) -> Result<(), String> {
402            let mut store = self.campaigns.lock().unwrap();
403            store.remove(&id);
404            Ok(())
405        }
406
407        async fn add_offer(
408            &self,
409            _campaign_id: Uuid,
410            offer: &ProviderOffer,
411        ) -> Result<ProviderOffer, String> {
412            let mut store = self.offers.lock().unwrap();
413            store.insert(offer.id, offer.clone());
414            Ok(offer.clone())
415        }
416
417        async fn get_offers(&self, campaign_id: Uuid) -> Result<Vec<ProviderOffer>, String> {
418            let store = self.offers.lock().unwrap();
419            Ok(store
420                .values()
421                .filter(|o| o.campaign_id == campaign_id)
422                .cloned()
423                .collect())
424        }
425
426        async fn update_offer(&self, offer: &ProviderOffer) -> Result<ProviderOffer, String> {
427            let mut store = self.offers.lock().unwrap();
428            store.insert(offer.id, offer.clone());
429            Ok(offer.clone())
430        }
431
432        async fn delete_offer(&self, offer_id: Uuid) -> Result<(), String> {
433            let mut store = self.offers.lock().unwrap();
434            store.remove(&offer_id);
435            Ok(())
436        }
437
438        async fn find_offer_by_id(&self, offer_id: Uuid) -> Result<Option<ProviderOffer>, String> {
439            let store = self.offers.lock().unwrap();
440            Ok(store.get(&offer_id).cloned())
441        }
442
443        async fn update_aggregation(
444            &self,
445            _campaign_id: Uuid,
446            _total_kwh_electricity: Option<f64>,
447            _total_kwh_gas: Option<f64>,
448            _avg_kwh_per_unit: Option<f64>,
449        ) -> Result<(), String> {
450            Ok(())
451        }
452    }
453
454    // ─── Mock EnergyBillUploadRepository ────────────────────────────────
455
456    struct MockUploadRepo {
457        uploads: Mutex<HashMap<Uuid, EnergyBillUpload>>,
458    }
459
460    impl MockUploadRepo {
461        fn new() -> Self {
462            Self {
463                uploads: Mutex::new(HashMap::new()),
464            }
465        }
466    }
467
468    #[async_trait]
469    impl EnergyBillUploadRepository for MockUploadRepo {
470        async fn create(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
471            let mut store = self.uploads.lock().unwrap();
472            store.insert(upload.id, upload.clone());
473            Ok(upload.clone())
474        }
475
476        async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyBillUpload>, String> {
477            let store = self.uploads.lock().unwrap();
478            Ok(store.get(&id).cloned())
479        }
480
481        async fn find_by_campaign(
482            &self,
483            campaign_id: Uuid,
484        ) -> Result<Vec<EnergyBillUpload>, String> {
485            let store = self.uploads.lock().unwrap();
486            Ok(store
487                .values()
488                .filter(|u| u.campaign_id == campaign_id)
489                .cloned()
490                .collect())
491        }
492
493        async fn find_by_unit(&self, unit_id: Uuid) -> Result<Vec<EnergyBillUpload>, String> {
494            let store = self.uploads.lock().unwrap();
495            Ok(store
496                .values()
497                .filter(|u| u.unit_id == unit_id)
498                .cloned()
499                .collect())
500        }
501
502        async fn find_by_campaign_and_unit(
503            &self,
504            campaign_id: Uuid,
505            unit_id: Uuid,
506        ) -> Result<Option<EnergyBillUpload>, String> {
507            let store = self.uploads.lock().unwrap();
508            Ok(store
509                .values()
510                .find(|u| u.campaign_id == campaign_id && u.unit_id == unit_id)
511                .cloned())
512        }
513
514        async fn find_by_building(
515            &self,
516            building_id: Uuid,
517        ) -> Result<Vec<EnergyBillUpload>, String> {
518            let store = self.uploads.lock().unwrap();
519            Ok(store
520                .values()
521                .filter(|u| u.building_id == building_id)
522                .cloned()
523                .collect())
524        }
525
526        async fn update(&self, upload: &EnergyBillUpload) -> Result<EnergyBillUpload, String> {
527            let mut store = self.uploads.lock().unwrap();
528            store.insert(upload.id, upload.clone());
529            Ok(upload.clone())
530        }
531
532        async fn delete(&self, id: Uuid) -> Result<(), String> {
533            let mut store = self.uploads.lock().unwrap();
534            store.remove(&id);
535            Ok(())
536        }
537
538        async fn find_expired(&self) -> Result<Vec<EnergyBillUpload>, String> {
539            Ok(vec![])
540        }
541
542        async fn count_verified_by_campaign(&self, _campaign_id: Uuid) -> Result<i32, String> {
543            Ok(0)
544        }
545
546        async fn find_verified_by_campaign(
547            &self,
548            _campaign_id: Uuid,
549        ) -> Result<Vec<EnergyBillUpload>, String> {
550            Ok(vec![])
551        }
552
553        async fn delete_expired(&self) -> Result<i32, String> {
554            Ok(0)
555        }
556    }
557
558    // ─── Mock BuildingRepository ────────────────────────────────────────
559
560    struct MockBuildingRepo {
561        buildings: Mutex<HashMap<Uuid, Building>>,
562    }
563
564    impl MockBuildingRepo {
565        fn new() -> Self {
566            Self {
567                buildings: Mutex::new(HashMap::new()),
568            }
569        }
570
571        fn with_building(building: Building) -> Self {
572            let mut map = HashMap::new();
573            map.insert(building.id, building);
574            Self {
575                buildings: Mutex::new(map),
576            }
577        }
578    }
579
580    #[async_trait]
581    impl BuildingRepository for MockBuildingRepo {
582        async fn create(&self, building: &Building) -> Result<Building, String> {
583            let mut store = self.buildings.lock().unwrap();
584            store.insert(building.id, building.clone());
585            Ok(building.clone())
586        }
587
588        async fn find_by_id(&self, id: Uuid) -> Result<Option<Building>, String> {
589            let store = self.buildings.lock().unwrap();
590            Ok(store.get(&id).cloned())
591        }
592
593        async fn find_all(&self) -> Result<Vec<Building>, String> {
594            let store = self.buildings.lock().unwrap();
595            Ok(store.values().cloned().collect())
596        }
597
598        async fn find_all_paginated(
599            &self,
600            _page_request: &PageRequest,
601            _filters: &BuildingFilters,
602        ) -> Result<(Vec<Building>, i64), String> {
603            let store = self.buildings.lock().unwrap();
604            let all: Vec<Building> = store.values().cloned().collect();
605            let count = all.len() as i64;
606            Ok((all, count))
607        }
608
609        async fn update(&self, building: &Building) -> Result<Building, String> {
610            let mut store = self.buildings.lock().unwrap();
611            store.insert(building.id, building.clone());
612            Ok(building.clone())
613        }
614
615        async fn delete(&self, id: Uuid) -> Result<bool, String> {
616            let mut store = self.buildings.lock().unwrap();
617            Ok(store.remove(&id).is_some())
618        }
619
620        async fn find_by_slug(&self, slug: &str) -> Result<Option<Building>, String> {
621            let store = self.buildings.lock().unwrap();
622            Ok(store
623                .values()
624                .find(|b| b.slug.as_deref() == Some(slug))
625                .cloned())
626        }
627    }
628
629    // ─── Helpers ────────────────────────────────────────────────────────
630
631    fn make_building(id: Uuid) -> Building {
632        let org_id = Uuid::new_v4();
633        let mut building = Building::new(
634            org_id,
635            "Test Building".to_string(),
636            "123 Test Street".to_string(),
637            "Brussels".to_string(),
638            "1000".to_string(),
639            "Belgium".to_string(),
640            25,
641            1000,
642            Some(1990),
643        )
644        .unwrap();
645        building.id = id;
646        building
647    }
648
649    fn make_campaign_for_building(building_id: Uuid) -> EnergyCampaign {
650        EnergyCampaign::new(
651            Uuid::new_v4(),
652            Some(building_id),
653            "Winter Campaign 2025".to_string(),
654            Utc::now() + chrono::Duration::days(30),
655            vec![EnergyType::Electricity],
656            Uuid::new_v4(),
657        )
658        .unwrap()
659    }
660
661    fn make_negotiating_campaign(building_id: Uuid) -> EnergyCampaign {
662        let mut campaign = make_campaign_for_building(building_id);
663        campaign.status = CampaignStatus::Negotiating;
664        campaign
665    }
666
667    fn make_offer(campaign_id: Uuid) -> ProviderOffer {
668        ProviderOffer::new(
669            campaign_id,
670            "Lampiris".to_string(),
671            Some(0.27),
672            None,
673            12.50,
674            100.0,
675            12,
676            15.0,
677            Utc::now() + chrono::Duration::days(30),
678        )
679        .unwrap()
680    }
681
682    // ─── Tests ──────────────────────────────────────────────────────────
683
684    #[tokio::test]
685    async fn test_create_campaign_success() {
686        let building_id = Uuid::new_v4();
687        let building = make_building(building_id);
688        let campaign = make_campaign_for_building(building_id);
689
690        let uc = EnergyCampaignUseCases::new(
691            Arc::new(MockCampaignRepo::new()),
692            Arc::new(MockUploadRepo::new()),
693            Arc::new(MockBuildingRepo::with_building(building)),
694        );
695
696        let result = uc.create_campaign(campaign).await;
697        assert!(result.is_ok());
698        let created = result.unwrap();
699        assert_eq!(created.status, CampaignStatus::Draft);
700        assert_eq!(created.building_id, Some(building_id));
701    }
702
703    #[tokio::test]
704    async fn test_create_campaign_building_not_found() {
705        let building_id = Uuid::new_v4();
706        let campaign = make_campaign_for_building(building_id);
707
708        let uc = EnergyCampaignUseCases::new(
709            Arc::new(MockCampaignRepo::new()),
710            Arc::new(MockUploadRepo::new()),
711            Arc::new(MockBuildingRepo::new()), // Empty -- building not found
712        );
713
714        let result = uc.create_campaign(campaign).await;
715        assert!(result.is_err());
716        assert_eq!(result.unwrap_err(), "Building not found");
717    }
718
719    #[tokio::test]
720    async fn test_create_campaign_no_building_id() {
721        // Campaign without building_id (multi-building) should succeed without building check
722        let campaign = EnergyCampaign::new(
723            Uuid::new_v4(),
724            None, // No building_id
725            "Multi-Building Campaign".to_string(),
726            Utc::now() + chrono::Duration::days(30),
727            vec![EnergyType::Both],
728            Uuid::new_v4(),
729        )
730        .unwrap();
731
732        let uc = EnergyCampaignUseCases::new(
733            Arc::new(MockCampaignRepo::new()),
734            Arc::new(MockUploadRepo::new()),
735            Arc::new(MockBuildingRepo::new()),
736        );
737
738        let result = uc.create_campaign(campaign).await;
739        assert!(result.is_ok());
740    }
741
742    #[tokio::test]
743    async fn test_get_campaign_success() {
744        let building_id = Uuid::new_v4();
745        let campaign = make_campaign_for_building(building_id);
746        let campaign_id = campaign.id;
747
748        let uc = EnergyCampaignUseCases::new(
749            Arc::new(MockCampaignRepo::with_campaign(campaign)),
750            Arc::new(MockUploadRepo::new()),
751            Arc::new(MockBuildingRepo::new()),
752        );
753
754        let result = uc.get_campaign(campaign_id).await;
755        assert!(result.is_ok());
756        let found = result.unwrap();
757        assert!(found.is_some());
758        assert_eq!(found.unwrap().id, campaign_id);
759    }
760
761    #[tokio::test]
762    async fn test_get_campaign_not_found() {
763        let uc = EnergyCampaignUseCases::new(
764            Arc::new(MockCampaignRepo::new()),
765            Arc::new(MockUploadRepo::new()),
766            Arc::new(MockBuildingRepo::new()),
767        );
768
769        let result = uc.get_campaign(Uuid::new_v4()).await;
770        assert!(result.is_ok());
771        assert!(result.unwrap().is_none());
772    }
773
774    #[tokio::test]
775    async fn test_add_offer_success() {
776        let building_id = Uuid::new_v4();
777        let campaign = make_negotiating_campaign(building_id);
778        let campaign_id = campaign.id;
779        let offer = make_offer(campaign_id);
780
781        let uc = EnergyCampaignUseCases::new(
782            Arc::new(MockCampaignRepo::with_campaign(campaign)),
783            Arc::new(MockUploadRepo::new()),
784            Arc::new(MockBuildingRepo::new()),
785        );
786
787        let result = uc.add_offer(campaign_id, offer).await;
788        assert!(result.is_ok());
789        let created_offer = result.unwrap();
790        assert_eq!(created_offer.provider_name, "Lampiris");
791    }
792
793    #[tokio::test]
794    async fn test_add_offer_wrong_status() {
795        let building_id = Uuid::new_v4();
796        let campaign = make_campaign_for_building(building_id); // Draft status
797        let campaign_id = campaign.id;
798        let offer = make_offer(campaign_id);
799
800        let uc = EnergyCampaignUseCases::new(
801            Arc::new(MockCampaignRepo::with_campaign(campaign)),
802            Arc::new(MockUploadRepo::new()),
803            Arc::new(MockBuildingRepo::new()),
804        );
805
806        let result = uc.add_offer(campaign_id, offer).await;
807        assert!(result.is_err());
808        assert!(result
809            .unwrap_err()
810            .contains("Campaign must be in Negotiating status to add offers"));
811    }
812
813    #[tokio::test]
814    async fn test_select_offer_success() {
815        let building_id = Uuid::new_v4();
816        let mut campaign = make_negotiating_campaign(building_id);
817        let campaign_id = campaign.id;
818        let offer = make_offer(campaign_id);
819        let offer_id = offer.id;
820
821        // Add the offer to campaign's offers_received so select_offer domain method works
822        campaign.offers_received.push(offer.clone());
823
824        let uc = EnergyCampaignUseCases::new(
825            Arc::new(MockCampaignRepo::with_campaign_and_offer(campaign, offer)),
826            Arc::new(MockUploadRepo::new()),
827            Arc::new(MockBuildingRepo::new()),
828        );
829
830        let result = uc.select_offer(campaign_id, offer_id).await;
831        assert!(result.is_ok());
832        let updated = result.unwrap();
833        assert_eq!(updated.selected_offer_id, Some(offer_id));
834    }
835
836    #[tokio::test]
837    async fn test_select_offer_not_in_campaign() {
838        let building_id = Uuid::new_v4();
839        let campaign = make_negotiating_campaign(building_id);
840        let campaign_id = campaign.id;
841
842        // Create an offer that belongs to a different campaign
843        let other_campaign_id = Uuid::new_v4();
844        let offer = make_offer(other_campaign_id);
845        let offer_id = offer.id;
846
847        let repo = MockCampaignRepo::with_campaign_and_offer(campaign, offer);
848
849        let uc = EnergyCampaignUseCases::new(
850            Arc::new(repo),
851            Arc::new(MockUploadRepo::new()),
852            Arc::new(MockBuildingRepo::new()),
853        );
854
855        let result = uc.select_offer(campaign_id, offer_id).await;
856        assert!(result.is_err());
857        assert!(result
858            .unwrap_err()
859            .contains("Offer does not belong to this campaign"));
860    }
861
862    #[tokio::test]
863    async fn test_cancel_campaign_success() {
864        let building_id = Uuid::new_v4();
865        let campaign = make_campaign_for_building(building_id);
866        let campaign_id = campaign.id;
867
868        let uc = EnergyCampaignUseCases::new(
869            Arc::new(MockCampaignRepo::with_campaign(campaign)),
870            Arc::new(MockUploadRepo::new()),
871            Arc::new(MockBuildingRepo::new()),
872        );
873
874        let result = uc.cancel_campaign(campaign_id).await;
875        assert!(result.is_ok());
876        assert_eq!(result.unwrap().status, CampaignStatus::Cancelled);
877    }
878
879    #[tokio::test]
880    async fn test_cancel_campaign_not_found() {
881        let uc = EnergyCampaignUseCases::new(
882            Arc::new(MockCampaignRepo::new()),
883            Arc::new(MockUploadRepo::new()),
884            Arc::new(MockBuildingRepo::new()),
885        );
886
887        let result = uc.cancel_campaign(Uuid::new_v4()).await;
888        assert!(result.is_err());
889        assert_eq!(result.unwrap_err(), "Campaign not found");
890    }
891
892    #[tokio::test]
893    async fn test_delete_campaign_success() {
894        let building_id = Uuid::new_v4();
895        let campaign = make_campaign_for_building(building_id);
896        let campaign_id = campaign.id;
897
898        let uc = EnergyCampaignUseCases::new(
899            Arc::new(MockCampaignRepo::with_campaign(campaign)),
900            Arc::new(MockUploadRepo::new()),
901            Arc::new(MockBuildingRepo::new()),
902        );
903
904        let result = uc.delete_campaign(campaign_id).await;
905        assert!(result.is_ok());
906    }
907}