koprogo_api/infrastructure/web/handlers/
energy_campaign_handlers.rs

1use actix_web::{delete, get, post, put, web, HttpResponse};
2use uuid::Uuid;
3
4use crate::application::dto::{
5    CampaignStatsResponse, CreateEnergyCampaignRequest, CreateProviderOfferRequest,
6    EnergyCampaignResponse, ProviderOfferResponse, SelectOfferRequest, UpdateCampaignStatusRequest,
7};
8use crate::application::use_cases::EnergyCampaignUseCases;
9use crate::domain::entities::EnergyCampaign;
10use crate::infrastructure::web::middleware::AuthenticatedUser;
11
12/// POST /api/v1/energy-campaigns
13/// Create a new energy campaign
14#[post("/energy-campaigns")]
15pub async fn create_campaign(
16    campaigns: web::Data<EnergyCampaignUseCases>,
17    request: web::Json<CreateEnergyCampaignRequest>,
18    user: AuthenticatedUser,
19) -> Result<HttpResponse, actix_web::Error> {
20    let org_id = user
21        .organization_id
22        .ok_or_else(|| actix_web::error::ErrorBadRequest("Organization ID required"))?;
23
24    let campaign = EnergyCampaign::new(
25        org_id,
26        request.building_id,
27        request.campaign_name.clone(),
28        request.deadline_participation,
29        request.energy_types.clone(),
30        user.user_id,
31    )
32    .map_err(actix_web::error::ErrorBadRequest)?;
33
34    let created = campaigns
35        .create_campaign(campaign)
36        .await
37        .map_err(actix_web::error::ErrorInternalServerError)?;
38
39    Ok(HttpResponse::Created().json(EnergyCampaignResponse::from(created)))
40}
41
42/// GET /api/v1/energy-campaigns
43/// List all campaigns for current organization
44#[get("/energy-campaigns")]
45pub async fn list_campaigns(
46    campaigns: web::Data<EnergyCampaignUseCases>,
47    user: AuthenticatedUser,
48) -> Result<HttpResponse, actix_web::Error> {
49    let org_id = user
50        .organization_id
51        .ok_or_else(|| actix_web::error::ErrorBadRequest("Organization ID required"))?;
52
53    let list = campaigns
54        .get_campaigns_by_organization(org_id)
55        .await
56        .map_err(actix_web::error::ErrorInternalServerError)?;
57
58    let response: Vec<EnergyCampaignResponse> =
59        list.into_iter().map(EnergyCampaignResponse::from).collect();
60
61    Ok(HttpResponse::Ok().json(response))
62}
63
64/// GET /api/v1/energy-campaigns/{id}
65/// Get campaign by ID
66#[get("/energy-campaigns/{id}")]
67pub async fn get_campaign(
68    campaigns: web::Data<EnergyCampaignUseCases>,
69    path: web::Path<Uuid>,
70    user: AuthenticatedUser,
71) -> Result<HttpResponse, actix_web::Error> {
72    let id = path.into_inner();
73
74    let campaign = campaigns
75        .get_campaign(id)
76        .await
77        .map_err(actix_web::error::ErrorInternalServerError)?
78        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
79
80    // Verify organization access
81    if campaign.organization_id
82        != user
83            .organization_id
84            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
85    {
86        return Err(actix_web::error::ErrorForbidden("Access denied"));
87    }
88
89    Ok(HttpResponse::Ok().json(EnergyCampaignResponse::from(campaign)))
90}
91
92/// PUT /api/v1/energy-campaigns/{id}/status
93/// Update campaign status
94#[put("/energy-campaigns/{id}/status")]
95pub async fn update_campaign_status(
96    campaigns: web::Data<EnergyCampaignUseCases>,
97    path: web::Path<Uuid>,
98    request: web::Json<UpdateCampaignStatusRequest>,
99    user: AuthenticatedUser,
100) -> Result<HttpResponse, actix_web::Error> {
101    let id = path.into_inner();
102
103    // Verify ownership
104    let campaign = campaigns
105        .get_campaign(id)
106        .await
107        .map_err(actix_web::error::ErrorInternalServerError)?
108        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
109
110    if campaign.organization_id
111        != user
112            .organization_id
113            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
114    {
115        return Err(actix_web::error::ErrorForbidden("Access denied"));
116    }
117
118    let updated = campaigns
119        .update_campaign_status(id, request.status.clone())
120        .await
121        .map_err(actix_web::error::ErrorInternalServerError)?;
122
123    Ok(HttpResponse::Ok().json(EnergyCampaignResponse::from(updated)))
124}
125
126/// GET /api/v1/energy-campaigns/{id}/stats
127/// Get campaign statistics (anonymized)
128#[get("/energy-campaigns/{id}/stats")]
129pub async fn get_campaign_stats(
130    campaigns: web::Data<EnergyCampaignUseCases>,
131    path: web::Path<Uuid>,
132    user: AuthenticatedUser,
133) -> Result<HttpResponse, actix_web::Error> {
134    let id = path.into_inner();
135
136    // Verify ownership
137    let campaign = campaigns
138        .get_campaign(id)
139        .await
140        .map_err(actix_web::error::ErrorInternalServerError)?
141        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
142
143    if campaign.organization_id
144        != user
145            .organization_id
146            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
147    {
148        return Err(actix_web::error::ErrorForbidden("Access denied"));
149    }
150
151    let stats = campaigns
152        .get_campaign_stats(id)
153        .await
154        .map_err(actix_web::error::ErrorInternalServerError)?;
155
156    let response = CampaignStatsResponse {
157        total_participants: stats.total_participants,
158        participation_rate: stats.participation_rate,
159        total_kwh_electricity: stats.total_kwh_electricity,
160        total_kwh_gas: stats.total_kwh_gas,
161        avg_kwh_per_unit: stats.avg_kwh_per_unit,
162        can_negotiate: stats.can_negotiate,
163        estimated_savings_pct: stats.estimated_savings_pct,
164        k_anonymity_met: stats.total_participants >= 5,
165    };
166
167    Ok(HttpResponse::Ok().json(response))
168}
169
170/// POST /api/v1/energy-campaigns/{id}/offers
171/// Add provider offer (broker/admin only)
172#[post("/energy-campaigns/{id}/offers")]
173pub async fn add_offer(
174    campaigns: web::Data<EnergyCampaignUseCases>,
175    path: web::Path<Uuid>,
176    request: web::Json<CreateProviderOfferRequest>,
177    user: AuthenticatedUser,
178) -> Result<HttpResponse, actix_web::Error> {
179    let campaign_id = path.into_inner();
180
181    // Verify ownership
182    let campaign = campaigns
183        .get_campaign(campaign_id)
184        .await
185        .map_err(actix_web::error::ErrorInternalServerError)?
186        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
187
188    if campaign.organization_id
189        != user
190            .organization_id
191            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
192    {
193        return Err(actix_web::error::ErrorForbidden("Access denied"));
194    }
195
196    use crate::domain::entities::ProviderOffer;
197
198    let offer = ProviderOffer::new(
199        campaign_id,
200        request.provider_name.clone(),
201        request.price_kwh_electricity,
202        request.price_kwh_gas,
203        request.fixed_monthly_fee,
204        request.green_energy_pct,
205        request.contract_duration_months,
206        request.estimated_savings_pct,
207        request.offer_valid_until,
208    )
209    .map_err(actix_web::error::ErrorBadRequest)?;
210
211    let created = campaigns
212        .add_offer(campaign_id, offer)
213        .await
214        .map_err(actix_web::error::ErrorInternalServerError)?;
215
216    Ok(HttpResponse::Created().json(ProviderOfferResponse::from(created)))
217}
218
219/// GET /api/v1/energy-campaigns/{id}/offers
220/// List all offers for a campaign
221#[get("/energy-campaigns/{id}/offers")]
222pub async fn list_offers(
223    campaigns: web::Data<EnergyCampaignUseCases>,
224    path: web::Path<Uuid>,
225    user: AuthenticatedUser,
226) -> Result<HttpResponse, actix_web::Error> {
227    let campaign_id = path.into_inner();
228
229    // Verify ownership
230    let campaign = campaigns
231        .get_campaign(campaign_id)
232        .await
233        .map_err(actix_web::error::ErrorInternalServerError)?
234        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
235
236    if campaign.organization_id
237        != user
238            .organization_id
239            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
240    {
241        return Err(actix_web::error::ErrorForbidden("Access denied"));
242    }
243
244    let offers = campaigns
245        .get_campaign_offers(campaign_id)
246        .await
247        .map_err(actix_web::error::ErrorInternalServerError)?;
248
249    let response: Vec<ProviderOfferResponse> = offers
250        .into_iter()
251        .map(ProviderOfferResponse::from)
252        .collect();
253
254    Ok(HttpResponse::Ok().json(response))
255}
256
257/// POST /api/v1/energy-campaigns/{id}/select-offer
258/// Select winning offer (after vote)
259#[post("/energy-campaigns/{id}/select-offer")]
260pub async fn select_offer(
261    campaigns: web::Data<EnergyCampaignUseCases>,
262    path: web::Path<Uuid>,
263    request: web::Json<SelectOfferRequest>,
264    user: AuthenticatedUser,
265) -> Result<HttpResponse, actix_web::Error> {
266    let campaign_id = path.into_inner();
267
268    // Verify ownership
269    let campaign = campaigns
270        .get_campaign(campaign_id)
271        .await
272        .map_err(actix_web::error::ErrorInternalServerError)?
273        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
274
275    if campaign.organization_id
276        != user
277            .organization_id
278            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
279    {
280        return Err(actix_web::error::ErrorForbidden("Access denied"));
281    }
282
283    let updated = campaigns
284        .select_offer(campaign_id, request.offer_id)
285        .await
286        .map_err(actix_web::error::ErrorInternalServerError)?;
287
288    Ok(HttpResponse::Ok().json(EnergyCampaignResponse::from(updated)))
289}
290
291/// POST /api/v1/energy-campaigns/{id}/finalize
292/// Finalize campaign (after final vote)
293#[post("/energy-campaigns/{id}/finalize")]
294pub async fn finalize_campaign(
295    campaigns: web::Data<EnergyCampaignUseCases>,
296    path: web::Path<Uuid>,
297    user: AuthenticatedUser,
298) -> Result<HttpResponse, actix_web::Error> {
299    let campaign_id = path.into_inner();
300
301    // Verify ownership
302    let campaign = campaigns
303        .get_campaign(campaign_id)
304        .await
305        .map_err(actix_web::error::ErrorInternalServerError)?
306        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
307
308    if campaign.organization_id
309        != user
310            .organization_id
311            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
312    {
313        return Err(actix_web::error::ErrorForbidden("Access denied"));
314    }
315
316    let updated = campaigns
317        .finalize_campaign(campaign_id)
318        .await
319        .map_err(actix_web::error::ErrorInternalServerError)?;
320
321    Ok(HttpResponse::Ok().json(EnergyCampaignResponse::from(updated)))
322}
323
324/// DELETE /api/v1/energy-campaigns/{id}
325/// Delete campaign
326#[delete("/energy-campaigns/{id}")]
327pub async fn delete_campaign(
328    campaigns: web::Data<EnergyCampaignUseCases>,
329    path: web::Path<Uuid>,
330    user: AuthenticatedUser,
331) -> Result<HttpResponse, actix_web::Error> {
332    let campaign_id = path.into_inner();
333
334    // Verify ownership
335    let campaign = campaigns
336        .get_campaign(campaign_id)
337        .await
338        .map_err(actix_web::error::ErrorInternalServerError)?
339        .ok_or_else(|| actix_web::error::ErrorNotFound("Campaign not found"))?;
340
341    if campaign.organization_id
342        != user
343            .organization_id
344            .ok_or_else(|| actix_web::error::ErrorForbidden("Organization ID required"))?
345    {
346        return Err(actix_web::error::ErrorForbidden("Access denied"));
347    }
348
349    campaigns
350        .delete_campaign(campaign_id)
351        .await
352        .map_err(actix_web::error::ErrorInternalServerError)?;
353
354    Ok(HttpResponse::NoContent().finish())
355}