Skip to main content

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        async fn find_by_id_with_metrics(
640            &self,
641            id: Uuid,
642        ) -> Result<Option<(Building, crate::domain::entities::BuildingMetrics)>, String> {
643            let store = self.buildings.lock().unwrap();
644            Ok(store
645                .get(&id)
646                .cloned()
647                .map(|b| (b, crate::domain::entities::BuildingMetrics::empty())))
648        }
649    }
650
651    // ─── Helpers ────────────────────────────────────────────────────────
652
653    fn make_building(id: Uuid) -> Building {
654        let org_id = Uuid::new_v4();
655        let mut building = Building::new(
656            org_id,
657            "Test Building".to_string(),
658            "123 Test Street".to_string(),
659            "Brussels".to_string(),
660            "1000".to_string(),
661            "Belgium".to_string(),
662            25,
663            1000,
664            Some(1990),
665        )
666        .unwrap();
667        building.id = id;
668        building
669    }
670
671    fn make_campaign_for_building(building_id: Uuid) -> EnergyCampaign {
672        EnergyCampaign::new(
673            Uuid::new_v4(),
674            Some(building_id),
675            "Winter Campaign 2025".to_string(),
676            Utc::now() + chrono::Duration::days(30),
677            vec![EnergyType::Electricity],
678            Uuid::new_v4(),
679        )
680        .unwrap()
681    }
682
683    fn make_negotiating_campaign(building_id: Uuid) -> EnergyCampaign {
684        let mut campaign = make_campaign_for_building(building_id);
685        campaign.status = CampaignStatus::Negotiating;
686        campaign
687    }
688
689    fn make_offer(campaign_id: Uuid) -> ProviderOffer {
690        ProviderOffer::new(
691            campaign_id,
692            "Lampiris".to_string(),
693            // 0,27 € le kWh et 12,50 € de redevance : des montants, donc
694            // `Decimal` (ADR-0008 § A). Le pourcentage d'énergie verte reste
695            // en `f64`, c'est un affichage.
696            Some(rust_decimal_macros::dec!(0.27)),
697            None,
698            rust_decimal_macros::dec!(12.50),
699            100.0,
700            12,
701            15.0,
702            Utc::now() + chrono::Duration::days(30),
703        )
704        .unwrap()
705    }
706
707    // ─── Tests ──────────────────────────────────────────────────────────
708
709    #[tokio::test]
710    async fn test_create_campaign_success() {
711        let building_id = Uuid::new_v4();
712        let building = make_building(building_id);
713        let campaign = make_campaign_for_building(building_id);
714
715        let uc = EnergyCampaignUseCases::new(
716            Arc::new(MockCampaignRepo::new()),
717            Arc::new(MockUploadRepo::new()),
718            Arc::new(MockBuildingRepo::with_building(building)),
719        );
720
721        let result = uc.create_campaign(campaign).await;
722        assert!(result.is_ok());
723        let created = result.unwrap();
724        assert_eq!(created.status, CampaignStatus::Draft);
725        assert_eq!(created.building_id, Some(building_id));
726    }
727
728    #[tokio::test]
729    async fn test_create_campaign_building_not_found() {
730        let building_id = Uuid::new_v4();
731        let campaign = make_campaign_for_building(building_id);
732
733        let uc = EnergyCampaignUseCases::new(
734            Arc::new(MockCampaignRepo::new()),
735            Arc::new(MockUploadRepo::new()),
736            Arc::new(MockBuildingRepo::new()), // Empty -- building not found
737        );
738
739        let result = uc.create_campaign(campaign).await;
740        assert!(result.is_err());
741        assert_eq!(result.unwrap_err(), "Building not found");
742    }
743
744    #[tokio::test]
745    async fn test_create_campaign_no_building_id() {
746        // Campaign without building_id (multi-building) should succeed without building check
747        let campaign = EnergyCampaign::new(
748            Uuid::new_v4(),
749            None, // No building_id
750            "Multi-Building Campaign".to_string(),
751            Utc::now() + chrono::Duration::days(30),
752            vec![EnergyType::Both],
753            Uuid::new_v4(),
754        )
755        .unwrap();
756
757        let uc = EnergyCampaignUseCases::new(
758            Arc::new(MockCampaignRepo::new()),
759            Arc::new(MockUploadRepo::new()),
760            Arc::new(MockBuildingRepo::new()),
761        );
762
763        let result = uc.create_campaign(campaign).await;
764        assert!(result.is_ok());
765    }
766
767    #[tokio::test]
768    async fn test_get_campaign_success() {
769        let building_id = Uuid::new_v4();
770        let campaign = make_campaign_for_building(building_id);
771        let campaign_id = campaign.id;
772
773        let uc = EnergyCampaignUseCases::new(
774            Arc::new(MockCampaignRepo::with_campaign(campaign)),
775            Arc::new(MockUploadRepo::new()),
776            Arc::new(MockBuildingRepo::new()),
777        );
778
779        let result = uc.get_campaign(campaign_id).await;
780        assert!(result.is_ok());
781        let found = result.unwrap();
782        assert!(found.is_some());
783        assert_eq!(found.unwrap().id, campaign_id);
784    }
785
786    #[tokio::test]
787    async fn test_get_campaign_not_found() {
788        let uc = EnergyCampaignUseCases::new(
789            Arc::new(MockCampaignRepo::new()),
790            Arc::new(MockUploadRepo::new()),
791            Arc::new(MockBuildingRepo::new()),
792        );
793
794        let result = uc.get_campaign(Uuid::new_v4()).await;
795        assert!(result.is_ok());
796        assert!(result.unwrap().is_none());
797    }
798
799    #[tokio::test]
800    async fn test_add_offer_success() {
801        let building_id = Uuid::new_v4();
802        let campaign = make_negotiating_campaign(building_id);
803        let campaign_id = campaign.id;
804        let offer = make_offer(campaign_id);
805
806        let uc = EnergyCampaignUseCases::new(
807            Arc::new(MockCampaignRepo::with_campaign(campaign)),
808            Arc::new(MockUploadRepo::new()),
809            Arc::new(MockBuildingRepo::new()),
810        );
811
812        let result = uc.add_offer(campaign_id, offer).await;
813        assert!(result.is_ok());
814        let created_offer = result.unwrap();
815        assert_eq!(created_offer.provider_name, "Lampiris");
816    }
817
818    #[tokio::test]
819    async fn test_add_offer_wrong_status() {
820        let building_id = Uuid::new_v4();
821        let campaign = make_campaign_for_building(building_id); // Draft status
822        let campaign_id = campaign.id;
823        let offer = make_offer(campaign_id);
824
825        let uc = EnergyCampaignUseCases::new(
826            Arc::new(MockCampaignRepo::with_campaign(campaign)),
827            Arc::new(MockUploadRepo::new()),
828            Arc::new(MockBuildingRepo::new()),
829        );
830
831        let result = uc.add_offer(campaign_id, offer).await;
832        assert!(result.is_err());
833        assert!(result
834            .unwrap_err()
835            .contains("Campaign must be in Negotiating status to add offers"));
836    }
837
838    #[tokio::test]
839    async fn test_select_offer_success() {
840        let building_id = Uuid::new_v4();
841        let mut campaign = make_negotiating_campaign(building_id);
842        let campaign_id = campaign.id;
843        let offer = make_offer(campaign_id);
844        let offer_id = offer.id;
845
846        // Add the offer to campaign's offers_received so select_offer domain method works
847        campaign.offers_received.push(offer.clone());
848
849        let uc = EnergyCampaignUseCases::new(
850            Arc::new(MockCampaignRepo::with_campaign_and_offer(campaign, offer)),
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_ok());
857        let updated = result.unwrap();
858        assert_eq!(updated.selected_offer_id, Some(offer_id));
859    }
860
861    #[tokio::test]
862    async fn test_select_offer_not_in_campaign() {
863        let building_id = Uuid::new_v4();
864        let campaign = make_negotiating_campaign(building_id);
865        let campaign_id = campaign.id;
866
867        // Create an offer that belongs to a different campaign
868        let other_campaign_id = Uuid::new_v4();
869        let offer = make_offer(other_campaign_id);
870        let offer_id = offer.id;
871
872        let repo = MockCampaignRepo::with_campaign_and_offer(campaign, offer);
873
874        let uc = EnergyCampaignUseCases::new(
875            Arc::new(repo),
876            Arc::new(MockUploadRepo::new()),
877            Arc::new(MockBuildingRepo::new()),
878        );
879
880        let result = uc.select_offer(campaign_id, offer_id).await;
881        assert!(result.is_err());
882        assert!(result
883            .unwrap_err()
884            .contains("Offer does not belong to this campaign"));
885    }
886
887    #[tokio::test]
888    async fn test_cancel_campaign_success() {
889        let building_id = Uuid::new_v4();
890        let campaign = make_campaign_for_building(building_id);
891        let campaign_id = campaign.id;
892
893        let uc = EnergyCampaignUseCases::new(
894            Arc::new(MockCampaignRepo::with_campaign(campaign)),
895            Arc::new(MockUploadRepo::new()),
896            Arc::new(MockBuildingRepo::new()),
897        );
898
899        let result = uc.cancel_campaign(campaign_id).await;
900        assert!(result.is_ok());
901        assert_eq!(result.unwrap().status, CampaignStatus::Cancelled);
902    }
903
904    #[tokio::test]
905    async fn test_cancel_campaign_not_found() {
906        let uc = EnergyCampaignUseCases::new(
907            Arc::new(MockCampaignRepo::new()),
908            Arc::new(MockUploadRepo::new()),
909            Arc::new(MockBuildingRepo::new()),
910        );
911
912        let result = uc.cancel_campaign(Uuid::new_v4()).await;
913        assert!(result.is_err());
914        assert_eq!(result.unwrap_err(), "Campaign not found");
915    }
916
917    #[tokio::test]
918    async fn test_delete_campaign_success() {
919        let building_id = Uuid::new_v4();
920        let campaign = make_campaign_for_building(building_id);
921        let campaign_id = campaign.id;
922
923        let uc = EnergyCampaignUseCases::new(
924            Arc::new(MockCampaignRepo::with_campaign(campaign)),
925            Arc::new(MockUploadRepo::new()),
926            Arc::new(MockBuildingRepo::new()),
927        );
928
929        let result = uc.delete_campaign(campaign_id).await;
930        assert!(result.is_ok());
931    }
932}