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 pub async fn create_campaign(
30 &self,
31 campaign: EnergyCampaign,
32 ) -> Result<EnergyCampaign, String> {
33 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 pub async fn get_campaign(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
51 self.campaign_repo.find_by_id(id).await
52 }
53
54 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 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 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 pub async fn add_offer(
90 &self,
91 campaign_id: Uuid,
92 offer: ProviderOffer,
93 ) -> Result<ProviderOffer, String> {
94 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 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 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 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 pub async fn update_campaign_aggregation(
145 &self,
146 campaign_id: Uuid,
147 encryption_key: &[u8; 32],
148 ) -> Result<(), String> {
149 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 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 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 self.campaign_repo
207 .update_aggregation(campaign_id, total_kwh_elec, total_kwh_g, avg_kwh)
208 .await
209 }
210
211 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 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 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 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 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 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 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 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 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 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 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 #[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()), );
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 let campaign = EnergyCampaign::new(
723 Uuid::new_v4(),
724 None, "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); 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 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 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}