1use async_trait::async_trait;
2use sqlx::PgPool;
3use uuid::Uuid;
4
5use crate::application::ports::EnergyCampaignRepository;
6use crate::domain::entities::{
7 CampaignStatus, CampaignType, ContractType, EnergyCampaign, EnergyType, ProviderOffer,
8};
9
10pub struct PostgresEnergyCampaignRepository {
11 pub pool: PgPool,
12}
13
14impl PostgresEnergyCampaignRepository {
15 pub fn new(pool: PgPool) -> Self {
16 Self { pool }
17 }
18}
19
20#[async_trait]
21impl EnergyCampaignRepository for PostgresEnergyCampaignRepository {
22 async fn create(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
23 let energy_types_str: Vec<String> = campaign
24 .energy_types
25 .iter()
26 .map(|t| t.to_string())
27 .collect();
28
29 let _result = sqlx::query!(
30 r#"
31 INSERT INTO energy_campaigns (
32 id, organization_id, building_id, campaign_name, campaign_type, status,
33 deadline_participation, deadline_vote, contract_start_date,
34 energy_types, contract_duration_months, contract_type,
35 total_participants, total_kwh_electricity, total_kwh_gas, avg_kwh_per_unit,
36 selected_offer_id, estimated_savings_pct, created_by, created_at, updated_at
37 )
38 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21)
39 RETURNING id
40 "#,
41 campaign.id,
42 campaign.organization_id,
43 campaign.building_id,
44 campaign.campaign_name,
45 campaign.campaign_type.to_string(),
46 campaign.status.to_string(),
47 campaign.deadline_participation,
48 campaign.deadline_vote,
49 campaign.contract_start_date,
50 &energy_types_str,
51 campaign.contract_duration_months,
52 campaign.contract_type.to_string(),
53 campaign.total_participants,
54 campaign.total_kwh_electricity,
55 campaign.total_kwh_gas,
56 campaign.avg_kwh_per_unit,
57 campaign.selected_offer_id,
58 campaign.estimated_savings_pct,
59 campaign.created_by,
60 campaign.created_at,
61 campaign.updated_at,
62 )
63 .fetch_one(&self.pool)
64 .await
65 .map_err(|e| format!("Failed to create energy campaign: {}", e))?;
66
67 Ok(campaign.clone())
68 }
69
70 async fn find_by_id(&self, id: Uuid) -> Result<Option<EnergyCampaign>, String> {
71 let row = sqlx::query!(
72 r#"
73 SELECT
74 id, organization_id, building_id, campaign_name, campaign_type, status,
75 deadline_participation, deadline_vote, contract_start_date,
76 energy_types, contract_duration_months, contract_type,
77 total_participants, total_kwh_electricity, total_kwh_gas, avg_kwh_per_unit,
78 selected_offer_id, estimated_savings_pct, created_by, created_at, updated_at
79 FROM energy_campaigns
80 WHERE id = $1
81 "#,
82 id
83 )
84 .fetch_optional(&self.pool)
85 .await
86 .map_err(|e| format!("Failed to find energy campaign: {}", e))?;
87
88 match row {
89 Some(r) => {
90 let energy_types: Vec<EnergyType> = r
91 .energy_types
92 .iter()
93 .map(|s| s.parse().unwrap_or(EnergyType::Electricity))
94 .collect();
95
96 let offers = self.get_offers(id).await?;
98
99 Ok(Some(EnergyCampaign {
100 id: r.id,
101 organization_id: r.organization_id,
102 building_id: r.building_id,
103 campaign_name: r.campaign_name,
104 campaign_type: r.campaign_type.parse().unwrap_or(CampaignType::BuyingGroup),
105 status: r.status.parse().unwrap_or(CampaignStatus::Draft),
106 deadline_participation: r.deadline_participation,
107 deadline_vote: r.deadline_vote,
108 contract_start_date: r.contract_start_date,
109 energy_types,
110 contract_duration_months: r.contract_duration_months,
111 contract_type: r.contract_type.parse().unwrap_or(ContractType::Fixed),
112 total_participants: r.total_participants,
113 total_kwh_electricity: r.total_kwh_electricity,
114 total_kwh_gas: r.total_kwh_gas,
115 avg_kwh_per_unit: r.avg_kwh_per_unit,
116 offers_received: offers,
117 selected_offer_id: r.selected_offer_id,
118 estimated_savings_pct: r.estimated_savings_pct,
119 created_by: r.created_by,
120 created_at: r.created_at,
121 updated_at: r.updated_at,
122 }))
123 }
124 None => Ok(None),
125 }
126 }
127
128 async fn find_by_organization(
129 &self,
130 organization_id: Uuid,
131 ) -> Result<Vec<EnergyCampaign>, String> {
132 let rows = sqlx::query!(
133 r#"
134 SELECT
135 id, organization_id, building_id, campaign_name, campaign_type, status,
136 deadline_participation, deadline_vote, contract_start_date,
137 energy_types, contract_duration_months, contract_type,
138 total_participants, total_kwh_electricity, total_kwh_gas, avg_kwh_per_unit,
139 selected_offer_id, estimated_savings_pct, created_by, created_at, updated_at
140 FROM energy_campaigns
141 WHERE organization_id = $1
142 ORDER BY created_at DESC
143 "#,
144 organization_id
145 )
146 .fetch_all(&self.pool)
147 .await
148 .map_err(|e| format!("Failed to find campaigns by organization: {}", e))?;
149
150 let mut campaigns = Vec::new();
151 for r in rows {
152 let energy_types: Vec<EnergyType> = r
153 .energy_types
154 .iter()
155 .map(|s| s.parse().unwrap_or(EnergyType::Electricity))
156 .collect();
157
158 let offers = self.get_offers(r.id).await?;
159
160 campaigns.push(EnergyCampaign {
161 id: r.id,
162 organization_id: r.organization_id,
163 building_id: r.building_id,
164 campaign_name: r.campaign_name,
165 campaign_type: r.campaign_type.parse().unwrap_or(CampaignType::BuyingGroup),
166 status: r.status.parse().unwrap_or(CampaignStatus::Draft),
167 deadline_participation: r.deadline_participation,
168 deadline_vote: r.deadline_vote,
169 contract_start_date: r.contract_start_date,
170 energy_types,
171 contract_duration_months: r.contract_duration_months,
172 contract_type: r.contract_type.parse().unwrap_or(ContractType::Fixed),
173 total_participants: r.total_participants,
174 total_kwh_electricity: r.total_kwh_electricity,
175 total_kwh_gas: r.total_kwh_gas,
176 avg_kwh_per_unit: r.avg_kwh_per_unit,
177 offers_received: offers,
178 selected_offer_id: r.selected_offer_id,
179 estimated_savings_pct: r.estimated_savings_pct,
180 created_by: r.created_by,
181 created_at: r.created_at,
182 updated_at: r.updated_at,
183 });
184 }
185
186 Ok(campaigns)
187 }
188
189 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<EnergyCampaign>, String> {
190 let rows = sqlx::query!(
191 r#"
192 SELECT
193 id, organization_id, building_id, campaign_name, campaign_type, status,
194 deadline_participation, deadline_vote, contract_start_date,
195 energy_types, contract_duration_months, contract_type,
196 total_participants, total_kwh_electricity, total_kwh_gas, avg_kwh_per_unit,
197 selected_offer_id, estimated_savings_pct, created_by, created_at, updated_at
198 FROM energy_campaigns
199 WHERE building_id = $1
200 ORDER BY created_at DESC
201 "#,
202 building_id
203 )
204 .fetch_all(&self.pool)
205 .await
206 .map_err(|e| format!("Failed to find campaigns by building: {}", e))?;
207
208 let mut campaigns = Vec::new();
209 for r in rows {
210 let energy_types: Vec<EnergyType> = r
211 .energy_types
212 .iter()
213 .map(|s| s.parse().unwrap_or(EnergyType::Electricity))
214 .collect();
215
216 let offers = self.get_offers(r.id).await?;
217
218 campaigns.push(EnergyCampaign {
219 id: r.id,
220 organization_id: r.organization_id,
221 building_id: r.building_id,
222 campaign_name: r.campaign_name,
223 campaign_type: r.campaign_type.parse().unwrap_or(CampaignType::BuyingGroup),
224 status: r.status.parse().unwrap_or(CampaignStatus::Draft),
225 deadline_participation: r.deadline_participation,
226 deadline_vote: r.deadline_vote,
227 contract_start_date: r.contract_start_date,
228 energy_types,
229 contract_duration_months: r.contract_duration_months,
230 contract_type: r.contract_type.parse().unwrap_or(ContractType::Fixed),
231 total_participants: r.total_participants,
232 total_kwh_electricity: r.total_kwh_electricity,
233 total_kwh_gas: r.total_kwh_gas,
234 avg_kwh_per_unit: r.avg_kwh_per_unit,
235 offers_received: offers,
236 selected_offer_id: r.selected_offer_id,
237 estimated_savings_pct: r.estimated_savings_pct,
238 created_by: r.created_by,
239 created_at: r.created_at,
240 updated_at: r.updated_at,
241 });
242 }
243
244 Ok(campaigns)
245 }
246
247 async fn update(&self, campaign: &EnergyCampaign) -> Result<EnergyCampaign, String> {
248 let energy_types_str: Vec<String> = campaign
249 .energy_types
250 .iter()
251 .map(|t| t.to_string())
252 .collect();
253
254 sqlx::query!(
255 r#"
256 UPDATE energy_campaigns
257 SET
258 campaign_name = $2,
259 campaign_type = $3,
260 status = $4,
261 deadline_participation = $5,
262 deadline_vote = $6,
263 contract_start_date = $7,
264 energy_types = $8,
265 contract_duration_months = $9,
266 contract_type = $10,
267 total_participants = $11,
268 total_kwh_electricity = $12,
269 total_kwh_gas = $13,
270 avg_kwh_per_unit = $14,
271 selected_offer_id = $15,
272 estimated_savings_pct = $16,
273 updated_at = $17
274 WHERE id = $1
275 "#,
276 campaign.id,
277 campaign.campaign_name,
278 campaign.campaign_type.to_string(),
279 campaign.status.to_string(),
280 campaign.deadline_participation,
281 campaign.deadline_vote,
282 campaign.contract_start_date,
283 &energy_types_str,
284 campaign.contract_duration_months,
285 campaign.contract_type.to_string(),
286 campaign.total_participants,
287 campaign.total_kwh_electricity,
288 campaign.total_kwh_gas,
289 campaign.avg_kwh_per_unit,
290 campaign.selected_offer_id,
291 campaign.estimated_savings_pct,
292 campaign.updated_at,
293 )
294 .execute(&self.pool)
295 .await
296 .map_err(|e| format!("Failed to update energy campaign: {}", e))?;
297
298 Ok(campaign.clone())
299 }
300
301 async fn delete(&self, id: Uuid) -> Result<(), String> {
302 sqlx::query!(
303 r#"
304 DELETE FROM energy_campaigns
305 WHERE id = $1
306 "#,
307 id
308 )
309 .execute(&self.pool)
310 .await
311 .map_err(|e| format!("Failed to delete energy campaign: {}", e))?;
312
313 Ok(())
314 }
315
316 async fn add_offer(
317 &self,
318 campaign_id: Uuid,
319 offer: &ProviderOffer,
320 ) -> Result<ProviderOffer, String> {
321 sqlx::query!(
322 r#"
323 INSERT INTO provider_offers (
324 id, campaign_id, provider_name, price_kwh_electricity, price_kwh_gas,
325 fixed_monthly_fee, green_energy_pct, contract_duration_months,
326 estimated_savings_pct, offer_valid_until, created_at, updated_at
327 )
328 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
329 "#,
330 offer.id,
331 campaign_id,
332 offer.provider_name,
333 offer.price_kwh_electricity,
334 offer.price_kwh_gas,
335 offer.fixed_monthly_fee,
336 offer.green_energy_pct,
337 offer.contract_duration_months,
338 offer.estimated_savings_pct,
339 offer.offer_valid_until,
340 offer.created_at,
341 offer.updated_at,
342 )
343 .execute(&self.pool)
344 .await
345 .map_err(|e| format!("Failed to add provider offer: {}", e))?;
346
347 Ok(offer.clone())
348 }
349
350 async fn get_offers(&self, campaign_id: Uuid) -> Result<Vec<ProviderOffer>, String> {
351 let rows = sqlx::query!(
352 r#"
353 SELECT
354 id, campaign_id, provider_name, price_kwh_electricity, price_kwh_gas,
355 fixed_monthly_fee, green_energy_pct, contract_duration_months,
356 estimated_savings_pct, offer_valid_until, created_at, updated_at
357 FROM provider_offers
358 WHERE campaign_id = $1
359 ORDER BY estimated_savings_pct DESC
360 "#,
361 campaign_id
362 )
363 .fetch_all(&self.pool)
364 .await
365 .map_err(|e| format!("Failed to get provider offers: {}", e))?;
366
367 let offers = rows
368 .into_iter()
369 .map(|r| ProviderOffer {
370 id: r.id,
371 campaign_id: r.campaign_id,
372 provider_name: r.provider_name,
373 price_kwh_electricity: r.price_kwh_electricity,
374 price_kwh_gas: r.price_kwh_gas,
375 fixed_monthly_fee: r.fixed_monthly_fee,
376 green_energy_pct: r.green_energy_pct,
377 contract_duration_months: r.contract_duration_months,
378 estimated_savings_pct: r.estimated_savings_pct,
379 offer_valid_until: r.offer_valid_until,
380 created_at: r.created_at,
381 updated_at: r.updated_at,
382 })
383 .collect();
384
385 Ok(offers)
386 }
387
388 async fn update_offer(&self, offer: &ProviderOffer) -> Result<ProviderOffer, String> {
389 sqlx::query!(
390 r#"
391 UPDATE provider_offers
392 SET
393 provider_name = $2,
394 price_kwh_electricity = $3,
395 price_kwh_gas = $4,
396 fixed_monthly_fee = $5,
397 green_energy_pct = $6,
398 contract_duration_months = $7,
399 estimated_savings_pct = $8,
400 offer_valid_until = $9,
401 updated_at = $10
402 WHERE id = $1
403 "#,
404 offer.id,
405 offer.provider_name,
406 offer.price_kwh_electricity,
407 offer.price_kwh_gas,
408 offer.fixed_monthly_fee,
409 offer.green_energy_pct,
410 offer.contract_duration_months,
411 offer.estimated_savings_pct,
412 offer.offer_valid_until,
413 offer.updated_at,
414 )
415 .execute(&self.pool)
416 .await
417 .map_err(|e| format!("Failed to update provider offer: {}", e))?;
418
419 Ok(offer.clone())
420 }
421
422 async fn delete_offer(&self, offer_id: Uuid) -> Result<(), String> {
423 sqlx::query!(
424 r#"
425 DELETE FROM provider_offers
426 WHERE id = $1
427 "#,
428 offer_id
429 )
430 .execute(&self.pool)
431 .await
432 .map_err(|e| format!("Failed to delete provider offer: {}", e))?;
433
434 Ok(())
435 }
436
437 async fn find_offer_by_id(&self, offer_id: Uuid) -> Result<Option<ProviderOffer>, String> {
438 let row = sqlx::query!(
439 r#"
440 SELECT
441 id, campaign_id, provider_name, price_kwh_electricity, price_kwh_gas,
442 fixed_monthly_fee, green_energy_pct, contract_duration_months,
443 estimated_savings_pct, offer_valid_until, created_at, updated_at
444 FROM provider_offers
445 WHERE id = $1
446 "#,
447 offer_id
448 )
449 .fetch_optional(&self.pool)
450 .await
451 .map_err(|e| format!("Failed to find provider offer: {}", e))?;
452
453 Ok(row.map(|r| ProviderOffer {
454 id: r.id,
455 campaign_id: r.campaign_id,
456 provider_name: r.provider_name,
457 price_kwh_electricity: r.price_kwh_electricity,
458 price_kwh_gas: r.price_kwh_gas,
459 fixed_monthly_fee: r.fixed_monthly_fee,
460 green_energy_pct: r.green_energy_pct,
461 contract_duration_months: r.contract_duration_months,
462 estimated_savings_pct: r.estimated_savings_pct,
463 offer_valid_until: r.offer_valid_until,
464 created_at: r.created_at,
465 updated_at: r.updated_at,
466 }))
467 }
468
469 async fn update_aggregation(
470 &self,
471 campaign_id: Uuid,
472 total_kwh_electricity: Option<f64>,
473 total_kwh_gas: Option<f64>,
474 avg_kwh_per_unit: Option<f64>,
475 ) -> Result<(), String> {
476 sqlx::query!(
477 r#"
478 UPDATE energy_campaigns
479 SET
480 total_kwh_electricity = $2,
481 total_kwh_gas = $3,
482 avg_kwh_per_unit = $4,
483 updated_at = NOW()
484 WHERE id = $1
485 "#,
486 campaign_id,
487 total_kwh_electricity,
488 total_kwh_gas,
489 avg_kwh_per_unit,
490 )
491 .execute(&self.pool)
492 .await
493 .map_err(|e| format!("Failed to update campaign aggregation: {}", e))?;
494
495 Ok(())
496 }
497}