1use crate::application::dto::{
2 CastVoteDto, CreatePollDto, PageRequest, PollFilters, SortOrder, UpdatePollDto,
3};
4use crate::infrastructure::web::middleware::AuthenticatedUser;
5use crate::infrastructure::web::AppState;
6use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10#[utoipa::path(
17 post,
18 path = "/polls",
19 tag = "Polls",
20 summary = "Create a new poll",
21 request_body = CreatePollDto,
22 responses(
23 (status = 201, description = "Poll created"),
24 (status = 400, description = "Bad Request"),
25 ),
26 security(("bearer_auth" = []))
27)]
28#[post("/polls")]
29pub async fn create_poll(
30 state: web::Data<AppState>,
31 auth_user: AuthenticatedUser,
32 dto: web::Json<CreatePollDto>,
33) -> HttpResponse {
34 match state
35 .poll_use_cases
36 .create_poll(dto.into_inner(), auth_user.user_id)
37 .await
38 {
39 Ok(poll) => HttpResponse::Created().json(poll),
40 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
41 "error": e
42 })),
43 }
44}
45
46#[utoipa::path(
49 get,
50 path = "/polls/{id}",
51 tag = "Polls",
52 summary = "Get poll by ID",
53 params(
54 ("id" = String, Path, description = "Poll UUID")
55 ),
56 responses(
57 (status = 200, description = "Poll found"),
58 (status = 400, description = "Invalid ID format"),
59 (status = 404, description = "Poll not found"),
60 (status = 500, description = "Internal Server Error"),
61 ),
62 security(("bearer_auth" = []))
63)]
64#[get("/polls/{id}")]
65pub async fn get_poll(
66 state: web::Data<AppState>,
67 _auth_user: AuthenticatedUser,
68 path: web::Path<String>,
69) -> HttpResponse {
70 let poll_id = match Uuid::parse_str(&path.into_inner()) {
71 Ok(id) => id,
72 Err(_) => {
73 return HttpResponse::BadRequest().json(serde_json::json!({
74 "error": "Invalid poll ID format"
75 }))
76 }
77 };
78
79 match state.poll_use_cases.get_poll(poll_id).await {
80 Ok(poll) => HttpResponse::Ok().json(poll),
81 Err(e) => {
82 if e.contains("not found") {
83 HttpResponse::NotFound().json(serde_json::json!({
84 "error": e
85 }))
86 } else {
87 HttpResponse::InternalServerError().json(serde_json::json!({
88 "error": e
89 }))
90 }
91 }
92 }
93}
94
95#[utoipa::path(
98 put,
99 path = "/polls/{id}",
100 tag = "Polls",
101 summary = "Update a draft poll",
102 params(
103 ("id" = String, Path, description = "Poll UUID")
104 ),
105 request_body = UpdatePollDto,
106 responses(
107 (status = 200, description = "Poll updated"),
108 (status = 400, description = "Bad Request"),
109 (status = 403, description = "Forbidden"),
110 (status = 404, description = "Poll not found"),
111 ),
112 security(("bearer_auth" = []))
113)]
114#[put("/polls/{id}")]
115pub async fn update_poll(
116 state: web::Data<AppState>,
117 auth_user: AuthenticatedUser,
118 path: web::Path<String>,
119 dto: web::Json<UpdatePollDto>,
120) -> HttpResponse {
121 let poll_id = match Uuid::parse_str(&path.into_inner()) {
122 Ok(id) => id,
123 Err(_) => {
124 return HttpResponse::BadRequest().json(serde_json::json!({
125 "error": "Invalid poll ID format"
126 }))
127 }
128 };
129
130 match state
131 .poll_use_cases
132 .update_poll(poll_id, dto.into_inner(), auth_user.user_id)
133 .await
134 {
135 Ok(poll) => HttpResponse::Ok().json(poll),
136 Err(e) => {
137 if e.contains("not found") {
138 HttpResponse::NotFound().json(serde_json::json!({
139 "error": e
140 }))
141 } else if e.contains("Only the poll creator") {
142 HttpResponse::Forbidden().json(serde_json::json!({
143 "error": e
144 }))
145 } else {
146 HttpResponse::BadRequest().json(serde_json::json!({
147 "error": e
148 }))
149 }
150 }
151 }
152}
153
154#[utoipa::path(
157 get,
158 path = "/polls",
159 tag = "Polls",
160 summary = "List polls with pagination and filters",
161 params(
162 ("page" = Option<i64>, Query, description = "Page number"),
163 ("per_page" = Option<i64>, Query, description = "Items per page"),
164 ("building_id" = Option<String>, Query, description = "Filter by building UUID"),
165 ("created_by" = Option<String>, Query, description = "Filter by creator UUID"),
166 ("ends_before" = Option<String>, Query, description = "Filter polls ending before date"),
167 ("ends_after" = Option<String>, Query, description = "Filter polls ending after date"),
168 ),
169 responses(
170 (status = 200, description = "Paginated list of polls"),
171 (status = 500, description = "Internal Server Error"),
172 ),
173 security(("bearer_auth" = []))
174)]
175#[get("/polls")]
176pub async fn list_polls(
177 state: web::Data<AppState>,
178 _auth_user: AuthenticatedUser,
179 query: web::Query<ListPollsQuery>,
180) -> HttpResponse {
181 let page_request = PageRequest {
182 page: query.page.unwrap_or(1),
183 per_page: query.per_page.unwrap_or(10),
184 sort_by: None,
185 order: SortOrder::Desc,
186 };
187
188 let filters = PollFilters {
189 building_id: query.building_id.clone(),
190 created_by: query.created_by.clone(),
191 status: None, poll_type: None,
193 ends_before: query.ends_before.clone(),
194 ends_after: query.ends_after.clone(),
195 };
196
197 match state
198 .poll_use_cases
199 .list_polls_paginated(&page_request, &filters)
200 .await
201 {
202 Ok(response) => HttpResponse::Ok().json(response),
203 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
204 "error": e
205 })),
206 }
207}
208
209#[derive(Debug, Deserialize)]
210pub struct ListPollsQuery {
211 pub page: Option<i64>,
212 pub per_page: Option<i64>,
213 pub building_id: Option<String>,
214 pub created_by: Option<String>,
215 pub ends_before: Option<String>,
216 pub ends_after: Option<String>,
217}
218
219#[utoipa::path(
222 get,
223 path = "/buildings/{building_id}/polls/active",
224 tag = "Polls",
225 summary = "List active polls for a building",
226 params(
227 ("building_id" = String, Path, description = "Building UUID")
228 ),
229 responses(
230 (status = 200, description = "List of active polls"),
231 (status = 400, description = "Invalid building ID format"),
232 (status = 500, description = "Internal Server Error"),
233 ),
234 security(("bearer_auth" = []))
235)]
236#[get("/buildings/{building_id}/polls/active")]
237pub async fn find_active_polls(
238 state: web::Data<AppState>,
239 _auth_user: AuthenticatedUser,
240 path: web::Path<String>,
241) -> HttpResponse {
242 let building_id = match Uuid::parse_str(&path.into_inner()) {
243 Ok(id) => id,
244 Err(_) => {
245 return HttpResponse::BadRequest().json(serde_json::json!({
246 "error": "Invalid building ID format"
247 }))
248 }
249 };
250
251 match state.poll_use_cases.find_active_polls(building_id).await {
252 Ok(polls) => HttpResponse::Ok().json(polls),
253 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
254 "error": e
255 })),
256 }
257}
258
259#[utoipa::path(
262 post,
263 path = "/polls/{id}/publish",
264 tag = "Polls",
265 summary = "Publish a draft poll",
266 params(
267 ("id" = String, Path, description = "Poll UUID")
268 ),
269 responses(
270 (status = 200, description = "Poll published"),
271 (status = 400, description = "Bad Request"),
272 (status = 403, description = "Forbidden"),
273 (status = 404, description = "Poll not found"),
274 ),
275 security(("bearer_auth" = []))
276)]
277#[post("/polls/{id}/publish")]
278pub async fn publish_poll(
279 state: web::Data<AppState>,
280 auth_user: AuthenticatedUser,
281 path: web::Path<String>,
282) -> HttpResponse {
283 let poll_id = match Uuid::parse_str(&path.into_inner()) {
284 Ok(id) => id,
285 Err(_) => {
286 return HttpResponse::BadRequest().json(serde_json::json!({
287 "error": "Invalid poll ID format"
288 }))
289 }
290 };
291
292 match state
293 .poll_use_cases
294 .publish_poll(poll_id, auth_user.user_id)
295 .await
296 {
297 Ok(poll) => HttpResponse::Ok().json(poll),
298 Err(e) => {
299 if e.contains("not found") {
300 HttpResponse::NotFound().json(serde_json::json!({
301 "error": e
302 }))
303 } else if e.contains("Only the poll creator") {
304 HttpResponse::Forbidden().json(serde_json::json!({
305 "error": e
306 }))
307 } else {
308 HttpResponse::BadRequest().json(serde_json::json!({
309 "error": e
310 }))
311 }
312 }
313 }
314}
315
316#[utoipa::path(
319 post,
320 path = "/polls/{id}/close",
321 tag = "Polls",
322 summary = "Close a poll manually",
323 params(
324 ("id" = String, Path, description = "Poll UUID")
325 ),
326 responses(
327 (status = 200, description = "Poll closed"),
328 (status = 400, description = "Bad Request"),
329 (status = 403, description = "Forbidden"),
330 (status = 404, description = "Poll not found"),
331 ),
332 security(("bearer_auth" = []))
333)]
334#[post("/polls/{id}/close")]
335pub async fn close_poll(
336 state: web::Data<AppState>,
337 auth_user: AuthenticatedUser,
338 path: web::Path<String>,
339) -> HttpResponse {
340 let poll_id = match Uuid::parse_str(&path.into_inner()) {
341 Ok(id) => id,
342 Err(_) => {
343 return HttpResponse::BadRequest().json(serde_json::json!({
344 "error": "Invalid poll ID format"
345 }))
346 }
347 };
348
349 match state
350 .poll_use_cases
351 .close_poll(poll_id, auth_user.user_id)
352 .await
353 {
354 Ok(poll) => HttpResponse::Ok().json(poll),
355 Err(e) => {
356 if e.contains("not found") {
357 HttpResponse::NotFound().json(serde_json::json!({
358 "error": e
359 }))
360 } else if e.contains("Only the poll creator") {
361 HttpResponse::Forbidden().json(serde_json::json!({
362 "error": e
363 }))
364 } else {
365 HttpResponse::BadRequest().json(serde_json::json!({
366 "error": e
367 }))
368 }
369 }
370 }
371}
372
373#[utoipa::path(
376 post,
377 path = "/polls/{id}/cancel",
378 tag = "Polls",
379 summary = "Cancel a poll",
380 params(
381 ("id" = String, Path, description = "Poll UUID")
382 ),
383 responses(
384 (status = 200, description = "Poll cancelled"),
385 (status = 400, description = "Bad Request"),
386 (status = 403, description = "Forbidden"),
387 (status = 404, description = "Poll not found"),
388 ),
389 security(("bearer_auth" = []))
390)]
391#[post("/polls/{id}/cancel")]
392pub async fn cancel_poll(
393 state: web::Data<AppState>,
394 auth_user: AuthenticatedUser,
395 path: web::Path<String>,
396) -> HttpResponse {
397 let poll_id = match Uuid::parse_str(&path.into_inner()) {
398 Ok(id) => id,
399 Err(_) => {
400 return HttpResponse::BadRequest().json(serde_json::json!({
401 "error": "Invalid poll ID format"
402 }))
403 }
404 };
405
406 match state
407 .poll_use_cases
408 .cancel_poll(poll_id, auth_user.user_id)
409 .await
410 {
411 Ok(poll) => HttpResponse::Ok().json(poll),
412 Err(e) => {
413 if e.contains("not found") {
414 HttpResponse::NotFound().json(serde_json::json!({
415 "error": e
416 }))
417 } else if e.contains("Only the poll creator") {
418 HttpResponse::Forbidden().json(serde_json::json!({
419 "error": e
420 }))
421 } else {
422 HttpResponse::BadRequest().json(serde_json::json!({
423 "error": e
424 }))
425 }
426 }
427 }
428}
429
430#[utoipa::path(
433 delete,
434 path = "/polls/{id}",
435 tag = "Polls",
436 summary = "Delete a draft or cancelled poll",
437 params(
438 ("id" = String, Path, description = "Poll UUID")
439 ),
440 responses(
441 (status = 204, description = "Poll deleted"),
442 (status = 400, description = "Bad Request"),
443 (status = 403, description = "Forbidden"),
444 (status = 404, description = "Poll not found"),
445 ),
446 security(("bearer_auth" = []))
447)]
448#[delete("/polls/{id}")]
449pub async fn delete_poll(
450 state: web::Data<AppState>,
451 auth_user: AuthenticatedUser,
452 path: web::Path<String>,
453) -> HttpResponse {
454 let poll_id = match Uuid::parse_str(&path.into_inner()) {
455 Ok(id) => id,
456 Err(_) => {
457 return HttpResponse::BadRequest().json(serde_json::json!({
458 "error": "Invalid poll ID format"
459 }))
460 }
461 };
462
463 match state
464 .poll_use_cases
465 .delete_poll(poll_id, auth_user.user_id)
466 .await
467 {
468 Ok(true) => HttpResponse::NoContent().finish(),
469 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
470 "error": "Poll not found"
471 })),
472 Err(e) => {
473 if e.contains("Only the poll creator") {
474 HttpResponse::Forbidden().json(serde_json::json!({
475 "error": e
476 }))
477 } else {
478 HttpResponse::BadRequest().json(serde_json::json!({
479 "error": e
480 }))
481 }
482 }
483 }
484}
485
486#[utoipa::path(
493 post,
494 path = "/polls/vote",
495 tag = "Polls",
496 summary = "Cast a vote on a poll",
497 request_body = CastVoteDto,
498 responses(
499 (status = 201, description = "Vote cast successfully"),
500 (status = 400, description = "Bad Request"),
501 (status = 404, description = "Poll not found"),
502 (status = 409, description = "Already voted"),
503 ),
504 security(("bearer_auth" = []))
505)]
506#[post("/polls/vote")]
507pub async fn cast_poll_vote(
508 state: web::Data<AppState>,
509 auth_user: AuthenticatedUser,
510 dto: web::Json<CastVoteDto>,
511 _req: HttpRequest,
512) -> HttpResponse {
513 let owner_id = Some(auth_user.user_id);
517
518 match state
519 .poll_use_cases
520 .cast_vote(dto.into_inner(), owner_id)
521 .await
522 {
523 Ok(message) => HttpResponse::Created().json(serde_json::json!({
524 "message": message
525 })),
526 Err(e) => {
527 if e.contains("not active") || e.contains("expired") {
528 HttpResponse::BadRequest().json(serde_json::json!({
529 "error": e
530 }))
531 } else if e.contains("already voted") {
532 HttpResponse::Conflict().json(serde_json::json!({
533 "error": e
534 }))
535 } else if e.contains("not found") {
536 HttpResponse::NotFound().json(serde_json::json!({
537 "error": e
538 }))
539 } else {
540 HttpResponse::BadRequest().json(serde_json::json!({
541 "error": e
542 }))
543 }
544 }
545 }
546}
547
548#[utoipa::path(
551 get,
552 path = "/polls/{id}/results",
553 tag = "Polls",
554 summary = "Get poll results and statistics",
555 params(
556 ("id" = String, Path, description = "Poll UUID")
557 ),
558 responses(
559 (status = 200, description = "Poll results"),
560 (status = 400, description = "Invalid ID format"),
561 (status = 404, description = "Poll not found"),
562 (status = 500, description = "Internal Server Error"),
563 ),
564 security(("bearer_auth" = []))
565)]
566#[get("/polls/{id}/results")]
567pub async fn get_poll_results(
568 state: web::Data<AppState>,
569 _auth_user: AuthenticatedUser,
570 path: web::Path<String>,
571) -> HttpResponse {
572 let poll_id = match Uuid::parse_str(&path.into_inner()) {
573 Ok(id) => id,
574 Err(_) => {
575 return HttpResponse::BadRequest().json(serde_json::json!({
576 "error": "Invalid poll ID format"
577 }))
578 }
579 };
580
581 match state.poll_use_cases.get_poll_results(poll_id).await {
582 Ok(results) => HttpResponse::Ok().json(results),
583 Err(e) => {
584 if e.contains("not found") {
585 HttpResponse::NotFound().json(serde_json::json!({
586 "error": e
587 }))
588 } else {
589 HttpResponse::InternalServerError().json(serde_json::json!({
590 "error": e
591 }))
592 }
593 }
594 }
595}
596
597#[utoipa::path(
604 get,
605 path = "/buildings/{building_id}/polls/statistics",
606 tag = "Polls",
607 summary = "Get poll statistics for a building",
608 params(
609 ("building_id" = String, Path, description = "Building UUID")
610 ),
611 responses(
612 (status = 200, description = "Poll statistics"),
613 (status = 400, description = "Invalid building ID format"),
614 (status = 500, description = "Internal Server Error"),
615 ),
616 security(("bearer_auth" = []))
617)]
618#[get("/buildings/{building_id}/polls/statistics")]
619pub async fn get_poll_building_statistics(
620 state: web::Data<AppState>,
621 _auth_user: AuthenticatedUser,
622 path: web::Path<String>,
623) -> HttpResponse {
624 let building_id = match Uuid::parse_str(&path.into_inner()) {
625 Ok(id) => id,
626 Err(_) => {
627 return HttpResponse::BadRequest().json(serde_json::json!({
628 "error": "Invalid building ID format"
629 }))
630 }
631 };
632
633 match state
634 .poll_use_cases
635 .get_building_statistics(building_id)
636 .await
637 {
638 Ok(stats) => HttpResponse::Ok().json(stats),
639 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
640 "error": e
641 })),
642 }
643}
644
645#[derive(Debug, Serialize)]
650pub struct PollStatisticsResponse {
651 pub total_polls: i64,
652 pub active_polls: i64,
653 pub closed_polls: i64,
654 pub average_participation_rate: f64,
655}