koprogo_api/infrastructure/web/handlers/
energy_campaign_handlers.rs1use 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("/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("/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("/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 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("/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 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("/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 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("/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 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("/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 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("/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 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("/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 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("/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 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}