1use crate::application::dto::{
2 CastVoteDto, CreatePollDto, PageRequest, PollFilters, SortOrder, UpdatePollDto,
3};
4use crate::infrastructure::web::classification_erreurs::{est_interdit, est_introuvable};
5use crate::infrastructure::web::middleware::scope_guard::{
6 verify_building_org_access, verify_poll_org_access,
7};
8use crate::infrastructure::web::middleware::AuthenticatedUser;
9use crate::infrastructure::web::AppState;
10use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, ResponseError};
11use serde::{Deserialize, Serialize};
12use uuid::Uuid;
13
14#[utoipa::path(
21 post,
22 path = "/polls",
23 tag = "Polls",
24 summary = "Create a new poll",
25 request_body = CreatePollDto,
26 responses(
27 (status = 201, description = "Poll created"),
28 (status = 400, description = "Bad Request"),
29 ),
30 security(("bearer_auth" = []))
31)]
32#[post("/polls")]
33pub async fn create_poll(
34 state: web::Data<AppState>,
35 auth_user: AuthenticatedUser,
36 dto: web::Json<CreatePollDto>,
37) -> HttpResponse {
38 match state
39 .poll_use_cases
40 .create_poll(dto.into_inner(), auth_user.user_id)
41 .await
42 {
43 Ok(poll) => HttpResponse::Created().json(poll),
44 Err(e) => HttpResponse::BadRequest().json(serde_json::json!({
45 "error": e
46 })),
47 }
48}
49
50#[utoipa::path(
53 get,
54 path = "/polls/{id}",
55 tag = "Polls",
56 summary = "Get poll by ID",
57 params(
58 ("id" = String, Path, description = "Poll UUID")
59 ),
60 responses(
61 (status = 200, description = "Poll found"),
62 (status = 400, description = "Invalid ID format"),
63 (status = 404, description = "Poll not found"),
64 (status = 500, description = "Internal Server Error"),
65 ),
66 security(("bearer_auth" = []))
67)]
68#[get("/polls/{id}")]
69pub async fn get_poll(
70 state: web::Data<AppState>,
71 auth_user: AuthenticatedUser,
72 path: web::Path<String>,
73) -> HttpResponse {
74 let poll_id = match Uuid::parse_str(&path.into_inner()) {
75 Ok(id) => id,
76 Err(_) => {
77 return HttpResponse::BadRequest().json(serde_json::json!({
78 "error": "Invalid poll ID format"
79 }))
80 }
81 };
82
83 if let Err(err) = verify_poll_org_access(
85 &auth_user,
86 poll_id,
87 &state.poll_use_cases,
88 &state.building_use_cases,
89 &state.acp_use_cases,
90 )
91 .await
92 {
93 return err.error_response();
94 }
95
96 match state.poll_use_cases.get_poll(poll_id).await {
97 Ok(poll) => HttpResponse::Ok().json(poll),
98 Err(e) => {
99 if est_introuvable(&e) {
100 HttpResponse::NotFound().json(serde_json::json!({
101 "error": e
102 }))
103 } else {
104 HttpResponse::InternalServerError().json(serde_json::json!({
105 "error": e
106 }))
107 }
108 }
109 }
110}
111
112#[utoipa::path(
115 put,
116 path = "/polls/{id}",
117 tag = "Polls",
118 summary = "Update a draft poll",
119 params(
120 ("id" = String, Path, description = "Poll UUID")
121 ),
122 request_body = UpdatePollDto,
123 responses(
124 (status = 200, description = "Poll updated"),
125 (status = 400, description = "Bad Request"),
126 (status = 403, description = "Forbidden"),
127 (status = 404, description = "Poll not found"),
128 ),
129 security(("bearer_auth" = []))
130)]
131#[put("/polls/{id}")]
132pub async fn update_poll(
133 state: web::Data<AppState>,
134 auth_user: AuthenticatedUser,
135 path: web::Path<String>,
136 dto: web::Json<UpdatePollDto>,
137) -> HttpResponse {
138 let poll_id = match Uuid::parse_str(&path.into_inner()) {
139 Ok(id) => id,
140 Err(_) => {
141 return HttpResponse::BadRequest().json(serde_json::json!({
142 "error": "Invalid poll ID format"
143 }))
144 }
145 };
146
147 match state
148 .poll_use_cases
149 .update_poll(poll_id, dto.into_inner(), auth_user.user_id)
150 .await
151 {
152 Ok(poll) => HttpResponse::Ok().json(poll),
153 Err(e) => {
154 if est_introuvable(&e) {
155 HttpResponse::NotFound().json(serde_json::json!({
156 "error": e
157 }))
158 } else if est_interdit(&e) {
159 HttpResponse::Forbidden().json(serde_json::json!({
160 "error": e
161 }))
162 } else {
163 HttpResponse::BadRequest().json(serde_json::json!({
164 "error": e
165 }))
166 }
167 }
168 }
169}
170
171#[utoipa::path(
174 get,
175 path = "/polls",
176 tag = "Polls",
177 summary = "List polls with pagination and filters",
178 params(
179 ("page" = Option<i64>, Query, description = "Page number"),
180 ("per_page" = Option<i64>, Query, description = "Items per page"),
181 ("building_id" = Option<String>, Query, description = "Filter by building UUID"),
182 ("created_by" = Option<String>, Query, description = "Filter by creator UUID"),
183 ("ends_before" = Option<String>, Query, description = "Filter polls ending before date"),
184 ("ends_after" = Option<String>, Query, description = "Filter polls ending after date"),
185 ),
186 responses(
187 (status = 200, description = "Paginated list of polls"),
188 (status = 500, description = "Internal Server Error"),
189 ),
190 security(("bearer_auth" = []))
191)]
192#[get("/polls")]
193pub async fn list_polls(
194 state: web::Data<AppState>,
195 auth_user: AuthenticatedUser,
196 query: web::Query<ListPollsQuery>,
197) -> HttpResponse {
198 match &query.building_id {
205 Some(id_str) => {
206 let building_id = match Uuid::parse_str(id_str) {
207 Ok(id) => id,
208 Err(_) => {
209 return HttpResponse::BadRequest().json(serde_json::json!({
210 "error": "Invalid building_id format"
211 }))
212 }
213 };
214 if let Err(err) = verify_building_org_access(
215 &auth_user,
216 building_id,
217 &state.building_use_cases,
218 &state.acp_use_cases,
219 )
220 .await
221 {
222 return err.error_response();
223 }
224 }
225 None => {
226 if !auth_user.is_superadmin() {
230 return HttpResponse::Forbidden().json(serde_json::json!({
231 "error": "building_id is required"
232 }));
233 }
234 }
235 }
236
237 let page_request = PageRequest {
238 page: query.page.unwrap_or(1),
239 per_page: query.per_page.unwrap_or(10),
240 sort_by: None,
241 order: SortOrder::Desc,
242 };
243
244 let filters = PollFilters {
245 building_id: query.building_id.clone(),
246 created_by: query.created_by.clone(),
247 status: None, poll_type: None,
249 ends_before: query.ends_before.clone(),
250 ends_after: query.ends_after.clone(),
251 };
252
253 match state
254 .poll_use_cases
255 .list_polls_paginated(&page_request, &filters)
256 .await
257 {
258 Ok(response) => HttpResponse::Ok().json(response),
259 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
260 "error": e
261 })),
262 }
263}
264
265#[derive(Debug, Deserialize)]
266pub struct ListPollsQuery {
267 pub page: Option<i64>,
268 pub per_page: Option<i64>,
269 pub building_id: Option<String>,
270 pub created_by: Option<String>,
271 pub ends_before: Option<String>,
272 pub ends_after: Option<String>,
273}
274
275#[utoipa::path(
278 get,
279 path = "/buildings/{building_id}/polls/active",
280 tag = "Polls",
281 summary = "List active polls for a building",
282 params(
283 ("building_id" = String, Path, description = "Building UUID")
284 ),
285 responses(
286 (status = 200, description = "List of active polls"),
287 (status = 400, description = "Invalid building ID format"),
288 (status = 500, description = "Internal Server Error"),
289 ),
290 security(("bearer_auth" = []))
291)]
292#[get("/buildings/{building_id}/polls/active")]
293pub async fn find_active_polls(
294 state: web::Data<AppState>,
295 auth_user: AuthenticatedUser,
296 path: web::Path<String>,
297) -> HttpResponse {
298 let building_id = match Uuid::parse_str(&path.into_inner()) {
299 Ok(id) => id,
300 Err(_) => {
301 return HttpResponse::BadRequest().json(serde_json::json!({
302 "error": "Invalid building ID format"
303 }))
304 }
305 };
306
307 if let Err(err) = verify_building_org_access(
314 &auth_user,
315 building_id,
316 &state.building_use_cases,
317 &state.acp_use_cases,
318 )
319 .await
320 {
321 return err.error_response();
322 }
323
324 match state.poll_use_cases.find_active_polls(building_id).await {
325 Ok(polls) => HttpResponse::Ok().json(polls),
326 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
327 "error": e
328 })),
329 }
330}
331
332#[utoipa::path(
335 post,
336 path = "/polls/{id}/publish",
337 tag = "Polls",
338 summary = "Publish a draft poll",
339 params(
340 ("id" = String, Path, description = "Poll UUID")
341 ),
342 responses(
343 (status = 200, description = "Poll published"),
344 (status = 400, description = "Bad Request"),
345 (status = 403, description = "Forbidden"),
346 (status = 404, description = "Poll not found"),
347 ),
348 security(("bearer_auth" = []))
349)]
350#[post("/polls/{id}/publish")]
351pub async fn publish_poll(
352 state: web::Data<AppState>,
353 auth_user: AuthenticatedUser,
354 path: web::Path<String>,
355) -> HttpResponse {
356 let poll_id = match Uuid::parse_str(&path.into_inner()) {
357 Ok(id) => id,
358 Err(_) => {
359 return HttpResponse::BadRequest().json(serde_json::json!({
360 "error": "Invalid poll ID format"
361 }))
362 }
363 };
364
365 if let Err(err) = verify_poll_org_access(
371 &auth_user,
372 poll_id,
373 &state.poll_use_cases,
374 &state.building_use_cases,
375 &state.acp_use_cases,
376 )
377 .await
378 {
379 return err.error_response();
380 }
381
382 match state
383 .poll_use_cases
384 .publish_poll(poll_id, auth_user.user_id)
385 .await
386 {
387 Ok(poll) => HttpResponse::Ok().json(poll),
388 Err(e) => {
389 if est_introuvable(&e) {
390 HttpResponse::NotFound().json(serde_json::json!({
391 "error": e
392 }))
393 } else if est_interdit(&e) {
394 HttpResponse::Forbidden().json(serde_json::json!({
395 "error": e
396 }))
397 } else {
398 HttpResponse::BadRequest().json(serde_json::json!({
399 "error": e
400 }))
401 }
402 }
403 }
404}
405
406#[utoipa::path(
409 post,
410 path = "/polls/{id}/close",
411 tag = "Polls",
412 summary = "Close a poll manually",
413 params(
414 ("id" = String, Path, description = "Poll UUID")
415 ),
416 responses(
417 (status = 200, description = "Poll closed"),
418 (status = 400, description = "Bad Request"),
419 (status = 403, description = "Forbidden"),
420 (status = 404, description = "Poll not found"),
421 ),
422 security(("bearer_auth" = []))
423)]
424#[post("/polls/{id}/close")]
425pub async fn close_poll(
426 state: web::Data<AppState>,
427 auth_user: AuthenticatedUser,
428 path: web::Path<String>,
429) -> HttpResponse {
430 let poll_id = match Uuid::parse_str(&path.into_inner()) {
431 Ok(id) => id,
432 Err(_) => {
433 return HttpResponse::BadRequest().json(serde_json::json!({
434 "error": "Invalid poll ID format"
435 }))
436 }
437 };
438
439 if let Err(err) = verify_poll_org_access(
445 &auth_user,
446 poll_id,
447 &state.poll_use_cases,
448 &state.building_use_cases,
449 &state.acp_use_cases,
450 )
451 .await
452 {
453 return err.error_response();
454 }
455
456 match state
457 .poll_use_cases
458 .close_poll(poll_id, auth_user.user_id)
459 .await
460 {
461 Ok(poll) => HttpResponse::Ok().json(poll),
462 Err(e) => {
463 if est_introuvable(&e) {
464 HttpResponse::NotFound().json(serde_json::json!({
465 "error": e
466 }))
467 } else if est_interdit(&e) {
468 HttpResponse::Forbidden().json(serde_json::json!({
469 "error": e
470 }))
471 } else {
472 HttpResponse::BadRequest().json(serde_json::json!({
473 "error": e
474 }))
475 }
476 }
477 }
478}
479
480#[utoipa::path(
483 post,
484 path = "/polls/{id}/cancel",
485 tag = "Polls",
486 summary = "Cancel a poll",
487 params(
488 ("id" = String, Path, description = "Poll UUID")
489 ),
490 responses(
491 (status = 200, description = "Poll cancelled"),
492 (status = 400, description = "Bad Request"),
493 (status = 403, description = "Forbidden"),
494 (status = 404, description = "Poll not found"),
495 ),
496 security(("bearer_auth" = []))
497)]
498#[post("/polls/{id}/cancel")]
499pub async fn cancel_poll(
500 state: web::Data<AppState>,
501 auth_user: AuthenticatedUser,
502 path: web::Path<String>,
503) -> HttpResponse {
504 let poll_id = match Uuid::parse_str(&path.into_inner()) {
505 Ok(id) => id,
506 Err(_) => {
507 return HttpResponse::BadRequest().json(serde_json::json!({
508 "error": "Invalid poll ID format"
509 }))
510 }
511 };
512
513 if let Err(err) = verify_poll_org_access(
519 &auth_user,
520 poll_id,
521 &state.poll_use_cases,
522 &state.building_use_cases,
523 &state.acp_use_cases,
524 )
525 .await
526 {
527 return err.error_response();
528 }
529
530 match state
531 .poll_use_cases
532 .cancel_poll(poll_id, auth_user.user_id)
533 .await
534 {
535 Ok(poll) => HttpResponse::Ok().json(poll),
536 Err(e) => {
537 if est_introuvable(&e) {
538 HttpResponse::NotFound().json(serde_json::json!({
539 "error": e
540 }))
541 } else if est_interdit(&e) {
542 HttpResponse::Forbidden().json(serde_json::json!({
543 "error": e
544 }))
545 } else {
546 HttpResponse::BadRequest().json(serde_json::json!({
547 "error": e
548 }))
549 }
550 }
551 }
552}
553
554#[utoipa::path(
557 delete,
558 path = "/polls/{id}",
559 tag = "Polls",
560 summary = "Delete a draft or cancelled poll",
561 params(
562 ("id" = String, Path, description = "Poll UUID")
563 ),
564 responses(
565 (status = 204, description = "Poll deleted"),
566 (status = 400, description = "Bad Request"),
567 (status = 403, description = "Forbidden"),
568 (status = 404, description = "Poll not found"),
569 ),
570 security(("bearer_auth" = []))
571)]
572#[delete("/polls/{id}")]
573pub async fn delete_poll(
574 state: web::Data<AppState>,
575 auth_user: AuthenticatedUser,
576 path: web::Path<String>,
577) -> HttpResponse {
578 let poll_id = match Uuid::parse_str(&path.into_inner()) {
579 Ok(id) => id,
580 Err(_) => {
581 return HttpResponse::BadRequest().json(serde_json::json!({
582 "error": "Invalid poll ID format"
583 }))
584 }
585 };
586
587 match state
588 .poll_use_cases
589 .delete_poll(poll_id, auth_user.user_id)
590 .await
591 {
592 Ok(true) => HttpResponse::NoContent().finish(),
593 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
594 "error": "Poll not found"
595 })),
596 Err(e) => {
597 if est_interdit(&e) {
598 HttpResponse::Forbidden().json(serde_json::json!({
599 "error": e
600 }))
601 } else {
602 HttpResponse::BadRequest().json(serde_json::json!({
603 "error": e
604 }))
605 }
606 }
607 }
608}
609
610#[utoipa::path(
617 post,
618 path = "/polls/vote",
619 tag = "Polls",
620 summary = "Cast a vote on a poll",
621 request_body = CastVoteDto,
622 responses(
623 (status = 201, description = "Vote cast successfully"),
624 (status = 400, description = "Bad Request"),
625 (status = 404, description = "Poll not found"),
626 (status = 409, description = "Already voted"),
627 ),
628 security(("bearer_auth" = []))
629)]
630#[post("/polls/vote")]
631pub async fn cast_poll_vote(
632 state: web::Data<AppState>,
633 auth_user: AuthenticatedUser,
634 dto: web::Json<CastVoteDto>,
635 _req: HttpRequest,
636) -> HttpResponse {
637 let owner_id = match state
653 .owner_use_cases
654 .find_owner_by_user_id(auth_user.user_id)
655 .await
656 {
657 Ok(Some(owner)) => match Uuid::parse_str(&owner.id) {
661 Ok(id) => Some(id),
662 Err(e) => {
663 return HttpResponse::InternalServerError().json(serde_json::json!({
664 "error": format!("Identifiant de copropriétaire illisible : {}", e)
665 }));
666 }
667 },
668 Ok(None) => {
669 return HttpResponse::Forbidden().json(serde_json::json!({
679 "error": crate::application::error::REFUS_VOTE_RESERVE_AUX_COPROPRIETAIRES,
680 "kind": "owner_not_linked"
681 }));
682 }
683 Err(e) => {
684 return HttpResponse::InternalServerError().json(serde_json::json!({
685 "error": format!("Failed to resolve owner for user: {}", e)
686 }));
687 }
688 };
689
690 match state
691 .poll_use_cases
692 .cast_vote(dto.into_inner(), owner_id)
693 .await
694 {
695 Ok(message) => HttpResponse::Created().json(serde_json::json!({
696 "message": message
697 })),
698 Err(e) => {
699 if e.contains("not active") || e.contains("expired") {
700 HttpResponse::BadRequest().json(serde_json::json!({
701 "error": e
702 }))
703 } else if e.contains("already voted") {
704 HttpResponse::Conflict().json(serde_json::json!({
705 "error": e
706 }))
707 } else if est_introuvable(&e) {
708 HttpResponse::NotFound().json(serde_json::json!({
709 "error": e
710 }))
711 } else {
712 HttpResponse::BadRequest().json(serde_json::json!({
713 "error": e
714 }))
715 }
716 }
717 }
718}
719
720#[utoipa::path(
723 get,
724 path = "/polls/{id}/results",
725 tag = "Polls",
726 summary = "Get poll results and statistics",
727 params(
728 ("id" = String, Path, description = "Poll UUID")
729 ),
730 responses(
731 (status = 200, description = "Poll results"),
732 (status = 400, description = "Invalid ID format"),
733 (status = 404, description = "Poll not found"),
734 (status = 500, description = "Internal Server Error"),
735 ),
736 security(("bearer_auth" = []))
737)]
738#[get("/polls/{id}/results")]
739pub async fn get_poll_results(
740 state: web::Data<AppState>,
741 auth_user: AuthenticatedUser,
742 path: web::Path<String>,
743) -> HttpResponse {
744 let poll_id = match Uuid::parse_str(&path.into_inner()) {
745 Ok(id) => id,
746 Err(_) => {
747 return HttpResponse::BadRequest().json(serde_json::json!({
748 "error": "Invalid poll ID format"
749 }))
750 }
751 };
752
753 if let Err(err) = verify_poll_org_access(
759 &auth_user,
760 poll_id,
761 &state.poll_use_cases,
762 &state.building_use_cases,
763 &state.acp_use_cases,
764 )
765 .await
766 {
767 return err.error_response();
768 }
769
770 match state.poll_use_cases.get_poll_results(poll_id).await {
771 Ok(results) => HttpResponse::Ok().json(results),
772 Err(e) => {
773 if est_introuvable(&e) {
774 HttpResponse::NotFound().json(serde_json::json!({
775 "error": e
776 }))
777 } else {
778 HttpResponse::InternalServerError().json(serde_json::json!({
779 "error": e
780 }))
781 }
782 }
783 }
784}
785
786#[utoipa::path(
793 get,
794 path = "/buildings/{building_id}/polls/statistics",
795 tag = "Polls",
796 summary = "Get poll statistics for a building",
797 params(
798 ("building_id" = String, Path, description = "Building UUID")
799 ),
800 responses(
801 (status = 200, description = "Poll statistics"),
802 (status = 400, description = "Invalid building ID format"),
803 (status = 500, description = "Internal Server Error"),
804 ),
805 security(("bearer_auth" = []))
806)]
807#[get("/buildings/{building_id}/polls/statistics")]
808pub async fn get_poll_building_statistics(
809 state: web::Data<AppState>,
810 auth_user: AuthenticatedUser,
811 path: web::Path<String>,
812) -> HttpResponse {
813 let building_id = match Uuid::parse_str(&path.into_inner()) {
814 Ok(id) => id,
815 Err(_) => {
816 return HttpResponse::BadRequest().json(serde_json::json!({
817 "error": "Invalid building ID format"
818 }))
819 }
820 };
821
822 if let Err(err) = verify_building_org_access(
827 &auth_user,
828 building_id,
829 &state.building_use_cases,
830 &state.acp_use_cases,
831 )
832 .await
833 {
834 return err.error_response();
835 }
836
837 match state
838 .poll_use_cases
839 .get_building_statistics(building_id)
840 .await
841 {
842 Ok(stats) => HttpResponse::Ok().json(stats),
843 Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({
844 "error": e
845 })),
846 }
847}
848
849#[derive(Debug, Serialize)]
854pub struct PollStatisticsResponse {
855 pub total_polls: i64,
856 pub active_polls: i64,
857 pub closed_polls: i64,
858 pub average_participation_rate: f64,
859}