koprogo_api/infrastructure/web/handlers/
poll_handlers.rs

1use crate::application::dto::{
2    CastVoteDto, CreatePollDto, PageRequest, PollFilters, SortOrder, UpdatePollDto,
3};
4use crate::application::use_cases::PollUseCases;
5use crate::infrastructure::web::middleware::AuthenticatedUser;
6use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10// ============================================================================
11// Poll Management Endpoints
12// ============================================================================
13
14/// Create a new poll (draft status)
15/// POST /api/v1/polls
16#[post("/polls")]
17pub async fn create_poll(
18    poll_use_cases: web::Data<PollUseCases>,
19    auth_user: AuthenticatedUser,
20    dto: web::Json<CreatePollDto>,
21) -> HttpResponse {
22    match poll_use_cases
23        .create_poll(dto.into_inner(), auth_user.user_id)
24        .await
25    {
26        Ok(poll) => HttpResponse::Created().json(poll),
27        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
28            "error": e
29        })),
30    }
31}
32
33/// Get poll by ID
34/// GET /api/v1/polls/:id
35#[get("/polls/{id}")]
36pub async fn get_poll(
37    poll_use_cases: web::Data<PollUseCases>,
38    _auth_user: AuthenticatedUser,
39    path: web::Path<String>,
40) -> HttpResponse {
41    let poll_id = match Uuid::parse_str(&path.into_inner()) {
42        Ok(id) => id,
43        Err(_) => {
44            return HttpResponse::BadRequest().json(serde_json::json!({
45                "error": "Invalid poll ID format"
46            }))
47        }
48    };
49
50    match poll_use_cases.get_poll(poll_id).await {
51        Ok(poll) => HttpResponse::Ok().json(poll),
52        Err(e) => {
53            if e.contains("not found") {
54                HttpResponse::NotFound().json(serde_json::json!({
55                    "error": e
56                }))
57            } else {
58                HttpResponse::InternalServerError().json(serde_json::json!({
59                    "error": e
60                }))
61            }
62        }
63    }
64}
65
66/// Update poll (only draft polls can be updated)
67/// PUT /api/v1/polls/:id
68#[put("/polls/{id}")]
69pub async fn update_poll(
70    poll_use_cases: web::Data<PollUseCases>,
71    auth_user: AuthenticatedUser,
72    path: web::Path<String>,
73    dto: web::Json<UpdatePollDto>,
74) -> HttpResponse {
75    let poll_id = match Uuid::parse_str(&path.into_inner()) {
76        Ok(id) => id,
77        Err(_) => {
78            return HttpResponse::BadRequest().json(serde_json::json!({
79                "error": "Invalid poll ID format"
80            }))
81        }
82    };
83
84    match poll_use_cases
85        .update_poll(poll_id, dto.into_inner(), auth_user.user_id)
86        .await
87    {
88        Ok(poll) => HttpResponse::Ok().json(poll),
89        Err(e) => {
90            if e.contains("not found") {
91                HttpResponse::NotFound().json(serde_json::json!({
92                    "error": e
93                }))
94            } else if e.contains("Only the poll creator") {
95                HttpResponse::Forbidden().json(serde_json::json!({
96                    "error": e
97                }))
98            } else {
99                HttpResponse::BadRequest().json(serde_json::json!({
100                    "error": e
101                }))
102            }
103        }
104    }
105}
106
107/// List polls with pagination and filters
108/// GET /api/v1/polls?page=1&per_page=10&building_id=xxx&status=active
109#[get("/polls")]
110pub async fn list_polls(
111    poll_use_cases: web::Data<PollUseCases>,
112    _auth_user: AuthenticatedUser,
113    query: web::Query<ListPollsQuery>,
114) -> HttpResponse {
115    let page_request = PageRequest {
116        page: query.page.unwrap_or(1),
117        per_page: query.per_page.unwrap_or(10),
118        sort_by: None,
119        order: SortOrder::Desc,
120    };
121
122    let filters = PollFilters {
123        building_id: query.building_id.clone(),
124        created_by: query.created_by.clone(),
125        status: None, // Parse from string if needed
126        poll_type: None,
127        ends_before: query.ends_before.clone(),
128        ends_after: query.ends_after.clone(),
129    };
130
131    match poll_use_cases
132        .list_polls_paginated(&page_request, &filters)
133        .await
134    {
135        Ok(response) => HttpResponse::Ok().json(response),
136        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
137            "error": e
138        })),
139    }
140}
141
142#[derive(Debug, Deserialize)]
143pub struct ListPollsQuery {
144    pub page: Option<i64>,
145    pub per_page: Option<i64>,
146    pub building_id: Option<String>,
147    pub created_by: Option<String>,
148    pub ends_before: Option<String>,
149    pub ends_after: Option<String>,
150}
151
152/// Find active polls for a building
153/// GET /api/v1/buildings/:building_id/polls/active
154#[get("/buildings/{building_id}/polls/active")]
155pub async fn find_active_polls(
156    poll_use_cases: web::Data<PollUseCases>,
157    _auth_user: AuthenticatedUser,
158    path: web::Path<String>,
159) -> HttpResponse {
160    let building_id = match Uuid::parse_str(&path.into_inner()) {
161        Ok(id) => id,
162        Err(_) => {
163            return HttpResponse::BadRequest().json(serde_json::json!({
164                "error": "Invalid building ID format"
165            }))
166        }
167    };
168
169    match poll_use_cases.find_active_polls(building_id).await {
170        Ok(polls) => HttpResponse::Ok().json(polls),
171        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
172            "error": e
173        })),
174    }
175}
176
177/// Publish a poll (change from draft to active)
178/// POST /api/v1/polls/:id/publish
179#[post("/polls/{id}/publish")]
180pub async fn publish_poll(
181    poll_use_cases: web::Data<PollUseCases>,
182    auth_user: AuthenticatedUser,
183    path: web::Path<String>,
184) -> HttpResponse {
185    let poll_id = match Uuid::parse_str(&path.into_inner()) {
186        Ok(id) => id,
187        Err(_) => {
188            return HttpResponse::BadRequest().json(serde_json::json!({
189                "error": "Invalid poll ID format"
190            }))
191        }
192    };
193
194    match poll_use_cases
195        .publish_poll(poll_id, auth_user.user_id)
196        .await
197    {
198        Ok(poll) => HttpResponse::Ok().json(poll),
199        Err(e) => {
200            if e.contains("not found") {
201                HttpResponse::NotFound().json(serde_json::json!({
202                    "error": e
203                }))
204            } else if e.contains("Only the poll creator") {
205                HttpResponse::Forbidden().json(serde_json::json!({
206                    "error": e
207                }))
208            } else {
209                HttpResponse::BadRequest().json(serde_json::json!({
210                    "error": e
211                }))
212            }
213        }
214    }
215}
216
217/// Close a poll manually
218/// POST /api/v1/polls/:id/close
219#[post("/polls/{id}/close")]
220pub async fn close_poll(
221    poll_use_cases: web::Data<PollUseCases>,
222    auth_user: AuthenticatedUser,
223    path: web::Path<String>,
224) -> HttpResponse {
225    let poll_id = match Uuid::parse_str(&path.into_inner()) {
226        Ok(id) => id,
227        Err(_) => {
228            return HttpResponse::BadRequest().json(serde_json::json!({
229                "error": "Invalid poll ID format"
230            }))
231        }
232    };
233
234    match poll_use_cases.close_poll(poll_id, auth_user.user_id).await {
235        Ok(poll) => HttpResponse::Ok().json(poll),
236        Err(e) => {
237            if e.contains("not found") {
238                HttpResponse::NotFound().json(serde_json::json!({
239                    "error": e
240                }))
241            } else if e.contains("Only the poll creator") {
242                HttpResponse::Forbidden().json(serde_json::json!({
243                    "error": e
244                }))
245            } else {
246                HttpResponse::BadRequest().json(serde_json::json!({
247                    "error": e
248                }))
249            }
250        }
251    }
252}
253
254/// Cancel a poll
255/// POST /api/v1/polls/:id/cancel
256#[post("/polls/{id}/cancel")]
257pub async fn cancel_poll(
258    poll_use_cases: web::Data<PollUseCases>,
259    auth_user: AuthenticatedUser,
260    path: web::Path<String>,
261) -> HttpResponse {
262    let poll_id = match Uuid::parse_str(&path.into_inner()) {
263        Ok(id) => id,
264        Err(_) => {
265            return HttpResponse::BadRequest().json(serde_json::json!({
266                "error": "Invalid poll ID format"
267            }))
268        }
269    };
270
271    match poll_use_cases.cancel_poll(poll_id, auth_user.user_id).await {
272        Ok(poll) => HttpResponse::Ok().json(poll),
273        Err(e) => {
274            if e.contains("not found") {
275                HttpResponse::NotFound().json(serde_json::json!({
276                    "error": e
277                }))
278            } else if e.contains("Only the poll creator") {
279                HttpResponse::Forbidden().json(serde_json::json!({
280                    "error": e
281                }))
282            } else {
283                HttpResponse::BadRequest().json(serde_json::json!({
284                    "error": e
285                }))
286            }
287        }
288    }
289}
290
291/// Delete a poll (only draft or cancelled)
292/// DELETE /api/v1/polls/:id
293#[delete("/polls/{id}")]
294pub async fn delete_poll(
295    poll_use_cases: web::Data<PollUseCases>,
296    auth_user: AuthenticatedUser,
297    path: web::Path<String>,
298) -> HttpResponse {
299    let poll_id = match Uuid::parse_str(&path.into_inner()) {
300        Ok(id) => id,
301        Err(_) => {
302            return HttpResponse::BadRequest().json(serde_json::json!({
303                "error": "Invalid poll ID format"
304            }))
305        }
306    };
307
308    match poll_use_cases.delete_poll(poll_id, auth_user.user_id).await {
309        Ok(true) => HttpResponse::NoContent().finish(),
310        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
311            "error": "Poll not found"
312        })),
313        Err(e) => {
314            if e.contains("Only the poll creator") {
315                HttpResponse::Forbidden().json(serde_json::json!({
316                    "error": e
317                }))
318            } else {
319                HttpResponse::BadRequest().json(serde_json::json!({
320                    "error": e
321                }))
322            }
323        }
324    }
325}
326
327// ============================================================================
328// Voting Endpoints
329// ============================================================================
330
331/// Cast a vote on a poll
332/// POST /api/v1/polls/vote
333#[post("/polls/vote")]
334pub async fn cast_poll_vote(
335    poll_use_cases: web::Data<PollUseCases>,
336    auth_user: AuthenticatedUser,
337    dto: web::Json<CastVoteDto>,
338    _req: HttpRequest,
339) -> HttpResponse {
340    // Owner ID is optional (anonymous votes)
341    // For now, we use the authenticated user's ID
342    // In production, you'd have logic to determine if vote is anonymous
343    let owner_id = Some(auth_user.user_id);
344
345    match poll_use_cases.cast_vote(dto.into_inner(), owner_id).await {
346        Ok(message) => HttpResponse::Created().json(serde_json::json!({
347            "message": message
348        })),
349        Err(e) => {
350            if e.contains("not active") || e.contains("expired") {
351                HttpResponse::BadRequest().json(serde_json::json!({
352                    "error": e
353                }))
354            } else if e.contains("already voted") {
355                HttpResponse::Conflict().json(serde_json::json!({
356                    "error": e
357                }))
358            } else if e.contains("not found") {
359                HttpResponse::NotFound().json(serde_json::json!({
360                    "error": e
361                }))
362            } else {
363                HttpResponse::BadRequest().json(serde_json::json!({
364                    "error": e
365                }))
366            }
367        }
368    }
369}
370
371/// Get poll results
372/// GET /api/v1/polls/:id/results
373#[get("/polls/{id}/results")]
374pub async fn get_poll_results(
375    poll_use_cases: web::Data<PollUseCases>,
376    _auth_user: AuthenticatedUser,
377    path: web::Path<String>,
378) -> HttpResponse {
379    let poll_id = match Uuid::parse_str(&path.into_inner()) {
380        Ok(id) => id,
381        Err(_) => {
382            return HttpResponse::BadRequest().json(serde_json::json!({
383                "error": "Invalid poll ID format"
384            }))
385        }
386    };
387
388    match poll_use_cases.get_poll_results(poll_id).await {
389        Ok(results) => HttpResponse::Ok().json(results),
390        Err(e) => {
391            if e.contains("not found") {
392                HttpResponse::NotFound().json(serde_json::json!({
393                    "error": e
394                }))
395            } else {
396                HttpResponse::InternalServerError().json(serde_json::json!({
397                    "error": e
398                }))
399            }
400        }
401    }
402}
403
404// ============================================================================
405// Statistics Endpoints
406// ============================================================================
407
408/// Get poll statistics for a building
409/// GET /api/v1/buildings/:building_id/polls/statistics
410#[get("/buildings/{building_id}/polls/statistics")]
411pub async fn get_poll_building_statistics(
412    poll_use_cases: web::Data<PollUseCases>,
413    _auth_user: AuthenticatedUser,
414    path: web::Path<String>,
415) -> HttpResponse {
416    let building_id = match Uuid::parse_str(&path.into_inner()) {
417        Ok(id) => id,
418        Err(_) => {
419            return HttpResponse::BadRequest().json(serde_json::json!({
420                "error": "Invalid building ID format"
421            }))
422        }
423    };
424
425    match poll_use_cases.get_building_statistics(building_id).await {
426        Ok(stats) => HttpResponse::Ok().json(stats),
427        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
428            "error": e
429        })),
430    }
431}
432
433// ============================================================================
434// Statistics Response DTO
435// ============================================================================
436
437#[derive(Debug, Serialize)]
438pub struct PollStatisticsResponse {
439    pub total_polls: i64,
440    pub active_polls: i64,
441    pub closed_polls: i64,
442    pub average_participation_rate: f64,
443}