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, 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 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 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 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 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 #[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()), );
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 let campaign = EnergyCampaign::new(
734 Uuid::new_v4(),
735 None, "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); 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 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 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}