koprogo_api/infrastructure/database/repositories/
energy_campaign_repository_impl.rs

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                // Fetch offers
97                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}