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::application::use_cases::EnergyCampaignUseCases;
9use crate::domain::entities::EnergyCampaign;
10use crate::infrastructure::web::middleware::AuthenticatedUser;
11
12#[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("/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("/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 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("/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 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("/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 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("/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 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("/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 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("/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 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("/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 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("/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 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}