koprogo_api/application/use_cases/
energy_campaign_use_cases.rs1use 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}