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