1use crate::application::dto::{
2 CastVoteDto, CreatePollDto, PageRequest, PollFilters, PollListResponseDto, PollOptionDto,
3 PollResponseDto, PollResultsDto, UpdatePollDto,
4};
5use crate::application::ports::{
6 OwnerRepository, PollRepository, PollVoteRepository, UnitOwnerRepository,
7};
8use crate::domain::entities::{Poll, PollStatus, PollType, PollVote};
9use chrono::{DateTime, Utc};
10use std::collections::HashSet;
11use std::sync::Arc;
12use uuid::Uuid;
13
14pub struct PollUseCases {
15 poll_repository: Arc<dyn PollRepository>,
16 poll_vote_repository: Arc<dyn PollVoteRepository>,
17 #[allow(dead_code)]
18 owner_repository: Arc<dyn OwnerRepository>,
19 unit_owner_repository: Arc<dyn UnitOwnerRepository>,
20}
21
22impl PollUseCases {
23 pub fn new(
24 poll_repository: Arc<dyn PollRepository>,
25 poll_vote_repository: Arc<dyn PollVoteRepository>,
26 owner_repository: Arc<dyn OwnerRepository>,
27 unit_owner_repository: Arc<dyn UnitOwnerRepository>,
28 ) -> Self {
29 Self {
30 poll_repository,
31 poll_vote_repository,
32 owner_repository,
33 unit_owner_repository,
34 }
35 }
36
37 pub async fn create_poll(
39 &self,
40 dto: CreatePollDto,
41 created_by: Uuid,
42 ) -> Result<PollResponseDto, String> {
43 let building_id = Uuid::parse_str(&dto.building_id)
45 .map_err(|_| "Invalid building ID format".to_string())?;
46
47 let ends_at = DateTime::parse_from_rfc3339(&dto.ends_at)
49 .map_err(|_| "Invalid ends_at date format (expected RFC3339)".to_string())?
50 .with_timezone(&Utc);
51
52 if ends_at <= Utc::now() {
54 return Err("Poll end date must be in the future".to_string());
55 }
56
57 let poll_type = match dto.poll_type.as_str() {
59 "yes_no" => PollType::YesNo,
60 "multiple_choice" => PollType::MultipleChoice,
61 "rating" => PollType::Rating,
62 "open_ended" => PollType::OpenEnded,
63 _ => return Err("Invalid poll type".to_string()),
64 };
65
66 let options = dto
68 .options
69 .iter()
70 .map(|opt| crate::domain::entities::PollOption {
71 id: Uuid::new_v4(),
72 option_text: opt.option_text.clone(),
73 attachment_url: opt.attachment_url.clone(),
74 vote_count: 0,
75 display_order: opt.display_order,
76 })
77 .collect();
78
79 let active_unit_owners = self
82 .unit_owner_repository
83 .find_active_by_building(building_id)
84 .await?;
85
86 let unique_owner_ids: HashSet<Uuid> = active_unit_owners
88 .iter()
89 .map(|(_, owner_id, _)| *owner_id)
90 .collect();
91
92 let total_eligible_voters = unique_owner_ids.len() as i32;
93
94 let mut poll = Poll::new(
96 building_id,
97 created_by,
98 dto.title.clone(),
99 dto.description.clone(),
100 poll_type,
101 options,
102 dto.is_anonymous.unwrap_or(false),
103 ends_at,
104 total_eligible_voters,
105 )?;
106
107 poll.allow_multiple_votes = dto.allow_multiple_votes.unwrap_or(false);
109 poll.require_all_owners = dto.require_all_owners.unwrap_or(false);
110
111 let created_poll = self.poll_repository.create(&poll).await?;
113
114 Ok(PollResponseDto::from(created_poll))
115 }
116
117 pub async fn update_poll(
119 &self,
120 poll_id: Uuid,
121 dto: UpdatePollDto,
122 user_id: Uuid,
123 ) -> Result<PollResponseDto, String> {
124 let mut poll = self
126 .poll_repository
127 .find_by_id(poll_id)
128 .await?
129 .ok_or_else(|| "Poll not found".to_string())?;
130
131 if poll.created_by != user_id {
133 return Err("Only the poll creator can update the poll".to_string());
134 }
135
136 if poll.status != PollStatus::Draft {
138 return Err("Cannot update poll that is no longer in draft status".to_string());
139 }
140
141 if let Some(title) = dto.title {
143 if title.trim().is_empty() {
144 return Err("Poll title cannot be empty".to_string());
145 }
146 poll.title = title;
147 }
148
149 if let Some(description) = dto.description {
150 poll.description = Some(description);
151 }
152
153 if let Some(ends_at_str) = dto.ends_at {
154 let ends_at = DateTime::parse_from_rfc3339(&ends_at_str)
155 .map_err(|_| "Invalid ends_at date format".to_string())?
156 .with_timezone(&Utc);
157
158 if ends_at <= Utc::now() {
159 return Err("Poll end date must be in the future".to_string());
160 }
161 poll.ends_at = ends_at;
162 }
163
164 poll.updated_at = Utc::now();
165
166 let updated_poll = self.poll_repository.update(&poll).await?;
168
169 Ok(PollResponseDto::from(updated_poll))
170 }
171
172 pub async fn get_poll(&self, poll_id: Uuid) -> Result<PollResponseDto, String> {
174 let poll = self
175 .poll_repository
176 .find_by_id(poll_id)
177 .await?
178 .ok_or_else(|| "Poll not found".to_string())?;
179
180 Ok(PollResponseDto::from(poll))
181 }
182
183 pub async fn list_polls_paginated(
185 &self,
186 page_request: &PageRequest,
187 filters: &PollFilters,
188 ) -> Result<PollListResponseDto, String> {
189 let (polls, total) = self
190 .poll_repository
191 .find_all_paginated(page_request, filters)
192 .await?;
193
194 let poll_dtos = polls.into_iter().map(PollResponseDto::from).collect();
195
196 Ok(PollListResponseDto {
197 polls: poll_dtos,
198 total,
199 page: page_request.page,
200 page_size: page_request.per_page,
201 })
202 }
203
204 pub async fn find_active_polls(
206 &self,
207 building_id: Uuid,
208 ) -> Result<Vec<PollResponseDto>, String> {
209 let polls = self.poll_repository.find_active(building_id).await?;
210 Ok(polls.into_iter().map(PollResponseDto::from).collect())
211 }
212
213 pub async fn publish_poll(
215 &self,
216 poll_id: Uuid,
217 user_id: Uuid,
218 ) -> Result<PollResponseDto, String> {
219 let mut poll = self
221 .poll_repository
222 .find_by_id(poll_id)
223 .await?
224 .ok_or_else(|| "Poll not found".to_string())?;
225
226 if poll.created_by != user_id {
228 return Err("Only the poll creator can publish the poll".to_string());
229 }
230
231 poll.publish()?;
233
234 let updated_poll = self.poll_repository.update(&poll).await?;
236
237 Ok(PollResponseDto::from(updated_poll))
238 }
239
240 pub async fn close_poll(
242 &self,
243 poll_id: Uuid,
244 user_id: Uuid,
245 ) -> Result<PollResponseDto, String> {
246 let mut poll = self
248 .poll_repository
249 .find_by_id(poll_id)
250 .await?
251 .ok_or_else(|| "Poll not found".to_string())?;
252
253 if poll.created_by != user_id {
255 return Err("Only the poll creator can close the poll".to_string());
256 }
257
258 poll.close()?;
260
261 let updated_poll = self.poll_repository.update(&poll).await?;
263
264 Ok(PollResponseDto::from(updated_poll))
265 }
266
267 pub async fn cancel_poll(
269 &self,
270 poll_id: Uuid,
271 user_id: Uuid,
272 ) -> Result<PollResponseDto, String> {
273 let mut poll = self
275 .poll_repository
276 .find_by_id(poll_id)
277 .await?
278 .ok_or_else(|| "Poll not found".to_string())?;
279
280 if poll.created_by != user_id {
282 return Err("Only the poll creator can cancel the poll".to_string());
283 }
284
285 poll.cancel()?;
287
288 let updated_poll = self.poll_repository.update(&poll).await?;
290
291 Ok(PollResponseDto::from(updated_poll))
292 }
293
294 pub async fn delete_poll(&self, poll_id: Uuid, user_id: Uuid) -> Result<bool, String> {
296 let poll = self
298 .poll_repository
299 .find_by_id(poll_id)
300 .await?
301 .ok_or_else(|| "Poll not found".to_string())?;
302
303 if poll.created_by != user_id {
305 return Err("Only the poll creator can delete the poll".to_string());
306 }
307
308 if poll.status != PollStatus::Draft && poll.status != PollStatus::Cancelled {
310 return Err("Can only delete polls in draft or cancelled status".to_string());
311 }
312
313 self.poll_repository.delete(poll_id).await
314 }
315
316 pub async fn cast_vote(
333 &self,
334 dto: CastVoteDto,
335 owner_id: Option<Uuid>,
336 ) -> Result<String, String> {
337 let poll_id =
339 Uuid::parse_str(&dto.poll_id).map_err(|_| "Invalid poll ID format".to_string())?;
340
341 let mut poll = self
343 .poll_repository
344 .find_by_id(poll_id)
345 .await?
346 .ok_or_else(|| "Poll not found".to_string())?;
347
348 if poll.status != PollStatus::Active {
350 return Err("Poll is not active".to_string());
351 }
352
353 if Utc::now() > poll.ends_at {
355 return Err("Poll has expired".to_string());
356 }
357
358 if let Some(oid) = owner_id {
360 let active_unit_owners = self
361 .unit_owner_repository
362 .find_active_by_building(poll.building_id)
363 .await?;
364 let is_building_owner = active_unit_owners.iter().any(|(_, owner, _)| *owner == oid);
365 if !is_building_owner {
366 return Err("You are not authorized to vote on this poll".to_string());
367 }
368 }
369
370 if let Some(oid) = owner_id {
372 if !poll.is_anonymous {
373 let existing_vote = self
374 .poll_vote_repository
375 .find_by_poll_and_owner(poll_id, oid)
376 .await?;
377 if existing_vote.is_some() {
378 return Err("You have already voted on this poll".to_string());
379 }
380 }
381 }
382
383 let vote = match poll.poll_type {
385 PollType::YesNo | PollType::MultipleChoice => {
386 let selected_ids = dto
387 .selected_option_ids
388 .ok_or_else(|| "Selected option IDs required for this poll type".to_string())?
389 .iter()
390 .map(|id| {
391 Uuid::parse_str(id).map_err(|_| "Invalid option ID format".to_string())
392 })
393 .collect::<Result<Vec<Uuid>, String>>()?;
394
395 for opt_id in &selected_ids {
397 if !poll.options.iter().any(|o| &o.id == opt_id) {
398 return Err("Invalid option ID".to_string());
399 }
400 }
401
402 if !poll.allow_multiple_votes && selected_ids.len() > 1 {
404 return Err("This poll does not allow multiple votes".to_string());
405 }
406
407 PollVote::new(
408 poll_id,
409 owner_id,
410 poll.building_id,
411 selected_ids,
412 None,
413 None,
414 )?
415 }
416 PollType::Rating => {
417 let rating = dto
418 .rating_value
419 .ok_or_else(|| "Rating value required for rating poll".to_string())?;
420
421 PollVote::new(
422 poll_id,
423 owner_id,
424 poll.building_id,
425 vec![],
426 Some(rating),
427 None,
428 )?
429 }
430 PollType::OpenEnded => {
431 let text = dto
432 .open_text
433 .ok_or_else(|| "Open text required for open-ended poll".to_string())?;
434
435 PollVote::new(
436 poll_id,
437 owner_id,
438 poll.building_id,
439 vec![],
440 None,
441 Some(text),
442 )?
443 }
444 };
445
446 self.poll_vote_repository.create(&vote).await?;
448
449 poll.total_votes_cast += 1;
451
452 if matches!(poll.poll_type, PollType::YesNo | PollType::MultipleChoice) {
454 for opt_id in &vote.selected_option_ids {
455 if let Some(option) = poll.options.iter_mut().find(|o| &o.id == opt_id) {
456 option.vote_count += 1;
457 }
458 }
459 }
460
461 self.poll_repository.update(&poll).await?;
463
464 Ok("Vote cast successfully".to_string())
465 }
466
467 pub async fn get_poll_results(&self, poll_id: Uuid) -> Result<PollResultsDto, String> {
469 let poll = self
471 .poll_repository
472 .find_by_id(poll_id)
473 .await?
474 .ok_or_else(|| "Poll not found".to_string())?;
475
476 let winning_option = if matches!(poll.poll_type, PollType::YesNo | PollType::MultipleChoice)
478 {
479 poll.options
480 .iter()
481 .max_by_key(|opt| opt.vote_count)
482 .map(|opt| {
483 let vote_percentage = if poll.total_votes_cast > 0 {
484 (opt.vote_count as f64 / poll.total_votes_cast as f64) * 100.0
485 } else {
486 0.0
487 };
488 PollOptionDto {
489 id: opt.id.to_string(),
490 option_text: opt.option_text.clone(),
491 attachment_url: opt.attachment_url.clone(),
492 vote_count: opt.vote_count,
493 vote_percentage,
494 display_order: opt.display_order,
495 }
496 })
497 } else {
498 None
499 };
500
501 Ok(PollResultsDto {
502 poll_id: poll.id.to_string(),
503 total_votes_cast: poll.total_votes_cast,
504 total_eligible_voters: poll.total_eligible_voters,
505 participation_rate: poll.participation_rate(),
506 winning_option,
507 options: poll
508 .options
509 .iter()
510 .map(|opt| {
511 let vote_percentage = if poll.total_votes_cast > 0 {
512 (opt.vote_count as f64 / poll.total_votes_cast as f64) * 100.0
513 } else {
514 0.0
515 };
516 PollOptionDto {
517 id: opt.id.to_string(),
518 option_text: opt.option_text.clone(),
519 attachment_url: opt.attachment_url.clone(),
520 vote_count: opt.vote_count,
521 vote_percentage,
522 display_order: opt.display_order,
523 }
524 })
525 .collect(),
526 })
527 }
528
529 pub async fn get_building_statistics(
531 &self,
532 building_id: Uuid,
533 ) -> Result<crate::application::ports::PollStatistics, String> {
534 self.poll_repository
535 .get_building_statistics(building_id)
536 .await
537 }
538
539 pub async fn auto_close_expired_polls(&self) -> Result<usize, String> {
541 let expired_polls = self.poll_repository.find_expired_active().await?;
542 let mut closed_count = 0;
543
544 for mut poll in expired_polls {
545 if poll.close().is_ok() {
546 self.poll_repository.update(&poll).await?;
547 closed_count += 1;
548 }
549 }
550
551 Ok(closed_count)
552 }
553}
554
555#[cfg(test)]
556mod tests {
557 use super::*;
558 use crate::application::dto::CreatePollOptionDto;
559 use crate::application::ports::PollStatistics;
560 use async_trait::async_trait;
561 use std::collections::HashMap;
562 use std::sync::Mutex;
563
564 struct MockPollRepository {
566 polls: Mutex<HashMap<Uuid, Poll>>,
567 }
568
569 impl MockPollRepository {
570 fn new() -> Self {
571 Self {
572 polls: Mutex::new(HashMap::new()),
573 }
574 }
575 }
576
577 #[async_trait]
578 impl PollRepository for MockPollRepository {
579 async fn create(&self, poll: &Poll) -> Result<Poll, String> {
580 let mut polls = self.polls.lock().unwrap();
581 polls.insert(poll.id, poll.clone());
582 Ok(poll.clone())
583 }
584
585 async fn find_by_id(&self, id: Uuid) -> Result<Option<Poll>, String> {
586 let polls = self.polls.lock().unwrap();
587 Ok(polls.get(&id).cloned())
588 }
589
590 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Poll>, String> {
591 let polls = self.polls.lock().unwrap();
592 Ok(polls
593 .values()
594 .filter(|p| p.building_id == building_id)
595 .cloned()
596 .collect())
597 }
598
599 async fn find_by_created_by(&self, created_by: Uuid) -> Result<Vec<Poll>, String> {
600 let polls = self.polls.lock().unwrap();
601 Ok(polls
602 .values()
603 .filter(|p| p.created_by == created_by)
604 .cloned()
605 .collect())
606 }
607
608 async fn find_all_paginated(
609 &self,
610 _page_request: &PageRequest,
611 _filters: &PollFilters,
612 ) -> Result<(Vec<Poll>, i64), String> {
613 let polls = self.polls.lock().unwrap();
614 let all: Vec<Poll> = polls.values().cloned().collect();
615 let total = all.len() as i64;
616 Ok((all, total))
617 }
618
619 async fn find_active(&self, building_id: Uuid) -> Result<Vec<Poll>, String> {
620 let polls = self.polls.lock().unwrap();
621 Ok(polls
622 .values()
623 .filter(|p| p.building_id == building_id && p.status == PollStatus::Active)
624 .cloned()
625 .collect())
626 }
627
628 async fn find_by_status(
629 &self,
630 building_id: Uuid,
631 status: &str,
632 ) -> Result<Vec<Poll>, String> {
633 let polls = self.polls.lock().unwrap();
634 let poll_status = match status {
635 "draft" => PollStatus::Draft,
636 "active" => PollStatus::Active,
637 "closed" => PollStatus::Closed,
638 "cancelled" => PollStatus::Cancelled,
639 _ => return Err("Invalid status".to_string()),
640 };
641 Ok(polls
642 .values()
643 .filter(|p| p.building_id == building_id && p.status == poll_status)
644 .cloned()
645 .collect())
646 }
647
648 async fn find_expired_active(&self) -> Result<Vec<Poll>, String> {
649 let polls = self.polls.lock().unwrap();
650 Ok(polls
651 .values()
652 .filter(|p| p.status == PollStatus::Active && Utc::now() > p.ends_at)
653 .cloned()
654 .collect())
655 }
656
657 async fn update(&self, poll: &Poll) -> Result<Poll, String> {
658 let mut polls = self.polls.lock().unwrap();
659 polls.insert(poll.id, poll.clone());
660 Ok(poll.clone())
661 }
662
663 async fn delete(&self, id: Uuid) -> Result<bool, String> {
664 let mut polls = self.polls.lock().unwrap();
665 Ok(polls.remove(&id).is_some())
666 }
667
668 async fn get_building_statistics(
669 &self,
670 building_id: Uuid,
671 ) -> Result<PollStatistics, String> {
672 let polls = self.polls.lock().unwrap();
673 let building_polls: Vec<&Poll> = polls
674 .values()
675 .filter(|p| p.building_id == building_id)
676 .collect();
677
678 let total = building_polls.len() as i64;
679 let active = building_polls
680 .iter()
681 .filter(|p| p.status == PollStatus::Active)
682 .count() as i64;
683 let closed = building_polls
684 .iter()
685 .filter(|p| p.status == PollStatus::Closed)
686 .count() as i64;
687
688 let avg_participation = if total > 0 {
689 building_polls
690 .iter()
691 .map(|p| p.participation_rate())
692 .sum::<f64>()
693 / total as f64
694 } else {
695 0.0
696 };
697
698 Ok(PollStatistics {
699 total_polls: total,
700 active_polls: active,
701 closed_polls: closed,
702 average_participation_rate: avg_participation,
703 })
704 }
705 }
706
707 struct MockPollVoteRepository {
708 votes: Mutex<HashMap<Uuid, PollVote>>,
709 }
710
711 impl MockPollVoteRepository {
712 fn new() -> Self {
713 Self {
714 votes: Mutex::new(HashMap::new()),
715 }
716 }
717 }
718
719 #[async_trait]
720 impl PollVoteRepository for MockPollVoteRepository {
721 async fn create(&self, vote: &PollVote) -> Result<PollVote, String> {
722 let mut votes = self.votes.lock().unwrap();
723 votes.insert(vote.id, vote.clone());
724 Ok(vote.clone())
725 }
726
727 async fn find_by_id(&self, id: Uuid) -> Result<Option<PollVote>, String> {
728 let votes = self.votes.lock().unwrap();
729 Ok(votes.get(&id).cloned())
730 }
731
732 async fn find_by_poll(&self, poll_id: Uuid) -> Result<Vec<PollVote>, String> {
733 let votes = self.votes.lock().unwrap();
734 Ok(votes
735 .values()
736 .filter(|v| v.poll_id == poll_id)
737 .cloned()
738 .collect())
739 }
740
741 async fn find_by_poll_and_owner(
742 &self,
743 poll_id: Uuid,
744 owner_id: Uuid,
745 ) -> Result<Option<PollVote>, String> {
746 let votes = self.votes.lock().unwrap();
747 Ok(votes
748 .values()
749 .find(|v| v.poll_id == poll_id && v.owner_id == Some(owner_id))
750 .cloned())
751 }
752
753 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<PollVote>, String> {
754 let votes = self.votes.lock().unwrap();
755 Ok(votes
756 .values()
757 .filter(|v| v.owner_id == Some(owner_id))
758 .cloned()
759 .collect())
760 }
761
762 async fn delete(&self, id: Uuid) -> Result<bool, String> {
763 let mut votes = self.votes.lock().unwrap();
764 Ok(votes.remove(&id).is_some())
765 }
766 }
767
768 struct MockUnitOwnerRepository;
769
770 #[async_trait]
771 impl UnitOwnerRepository for MockUnitOwnerRepository {
772 async fn create(
773 &self,
774 _unit_owner: &crate::domain::entities::UnitOwner,
775 ) -> Result<crate::domain::entities::UnitOwner, String> {
776 unimplemented!()
777 }
778
779 async fn find_by_id(
780 &self,
781 _id: Uuid,
782 ) -> Result<Option<crate::domain::entities::UnitOwner>, String> {
783 unimplemented!()
784 }
785
786 async fn find_current_owners_by_unit(
787 &self,
788 _unit_id: Uuid,
789 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
790 unimplemented!()
791 }
792
793 async fn find_current_units_by_owner(
794 &self,
795 _owner_id: Uuid,
796 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
797 unimplemented!()
798 }
799
800 async fn find_all_owners_by_unit(
801 &self,
802 _unit_id: Uuid,
803 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
804 unimplemented!()
805 }
806
807 async fn find_all_units_by_owner(
808 &self,
809 _owner_id: Uuid,
810 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
811 unimplemented!()
812 }
813
814 async fn update(
815 &self,
816 _unit_owner: &crate::domain::entities::UnitOwner,
817 ) -> Result<crate::domain::entities::UnitOwner, String> {
818 unimplemented!()
819 }
820
821 async fn delete(&self, _id: Uuid) -> Result<(), String> {
822 unimplemented!()
823 }
824
825 async fn has_active_owners(&self, _unit_id: Uuid) -> Result<bool, String> {
826 unimplemented!()
827 }
828
829 async fn get_total_ownership_percentage(
830 &self,
831 _unit_id: Uuid,
832 ) -> Result<rust_decimal::Decimal, String> {
833 unimplemented!()
834 }
835
836 async fn find_active_by_unit_and_owner(
837 &self,
838 _unit_id: Uuid,
839 _owner_id: Uuid,
840 ) -> Result<Option<crate::domain::entities::UnitOwner>, String> {
841 unimplemented!()
842 }
843
844 async fn find_active_by_building(
845 &self,
846 _building_id: Uuid,
847 ) -> Result<Vec<(Uuid, Uuid, rust_decimal::Decimal)>, String> {
848 Ok((0..10)
851 .map(|_| {
852 (
853 Uuid::new_v4(),
854 Uuid::new_v4(),
855 rust_decimal_macros::dec!(0.1),
856 )
857 })
858 .collect())
859 }
860
861 async fn find_active_quota_shares_by_building(
864 &self,
865 _building_id: Uuid,
866 ) -> Result<Vec<(Uuid, Uuid, rust_decimal::Decimal)>, String> {
867 Ok((0..10)
870 .map(|_| {
871 (
872 Uuid::new_v4(),
873 Uuid::new_v4(),
874 rust_decimal_macros::dec!(0.1),
875 )
876 })
877 .collect())
878 }
879
880 async fn find_voting_holders_by_unit(
881 &self,
882 _unit_id: Uuid,
883 ) -> Result<Vec<crate::domain::entities::LotHolder>, String> {
884 Ok(vec![])
885 }
886
887 async fn is_voting_representative(&self, _unit_owner_id: Uuid) -> Result<bool, String> {
888 Ok(false)
889 }
890
891 async fn set_voting_representative(&self, _unit_owner_id: Uuid) -> Result<(), String> {
892 Ok(())
893 }
894 }
895
896 struct MockOwnerRepository;
897
898 #[async_trait]
899 impl OwnerRepository for MockOwnerRepository {
900 async fn create(
901 &self,
902 _owner: &crate::domain::entities::Owner,
903 ) -> Result<crate::domain::entities::Owner, String> {
904 unimplemented!()
905 }
906
907 async fn find_by_id(
908 &self,
909 _id: Uuid,
910 ) -> Result<Option<crate::domain::entities::Owner>, String> {
911 unimplemented!()
912 }
913
914 async fn find_by_user_id(
915 &self,
916 _user_id: Uuid,
917 ) -> Result<Option<crate::domain::entities::Owner>, String> {
918 unimplemented!()
919 }
920
921 async fn find_by_user_id_and_organization(
922 &self,
923 _user_id: Uuid,
924 _organization_id: Uuid,
925 ) -> Result<Option<crate::domain::entities::Owner>, String> {
926 unimplemented!()
927 }
928
929 async fn find_by_email(
930 &self,
931 _email: &str,
932 ) -> Result<Option<crate::domain::entities::Owner>, String> {
933 unimplemented!()
934 }
935
936 async fn find_all(&self) -> Result<Vec<crate::domain::entities::Owner>, String> {
937 unimplemented!()
938 }
939
940 async fn find_all_paginated(
941 &self,
942 _page_request: &crate::application::dto::PageRequest,
943 _filters: &crate::application::dto::OwnerFilters,
944 ) -> Result<(Vec<crate::domain::entities::Owner>, i64), String> {
945 unimplemented!()
946 }
947
948 async fn update(
949 &self,
950 _owner: &crate::domain::entities::Owner,
951 ) -> Result<crate::domain::entities::Owner, String> {
952 unimplemented!()
953 }
954
955 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
956 unimplemented!()
957 }
958 async fn set_user_link(
959 &self,
960 _owner_id: Uuid,
961 _user_id: Option<Uuid>,
962 ) -> Result<bool, String> {
963 unimplemented!()
964 }
965 }
966
967 fn setup_use_cases() -> PollUseCases {
968 PollUseCases::new(
969 Arc::new(MockPollRepository::new()),
970 Arc::new(MockPollVoteRepository::new()),
971 Arc::new(MockOwnerRepository),
972 Arc::new(MockUnitOwnerRepository),
973 )
974 }
975
976 #[tokio::test]
977 async fn test_create_poll_success() {
978 let use_cases = setup_use_cases();
979 let building_id = Uuid::new_v4();
980 let created_by = Uuid::new_v4();
981
982 let dto = CreatePollDto {
983 building_id: building_id.to_string(),
984 title: "Test Poll".to_string(),
985 description: Some("Test description".to_string()),
986 poll_type: "yes_no".to_string(),
987 options: vec![
988 CreatePollOptionDto {
989 id: None,
990 option_text: "Yes".to_string(),
991 attachment_url: None,
992 display_order: 0,
993 },
994 CreatePollOptionDto {
995 id: None,
996 option_text: "No".to_string(),
997 attachment_url: None,
998 display_order: 1,
999 },
1000 ],
1001 is_anonymous: Some(false),
1002 allow_multiple_votes: Some(false),
1003 require_all_owners: Some(false),
1004 ends_at: (Utc::now() + chrono::Duration::days(7))
1005 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1006 };
1007
1008 let result = use_cases.create_poll(dto, created_by).await;
1009 assert!(result.is_ok());
1010
1011 let poll_response = result.unwrap();
1012 assert_eq!(poll_response.title, "Test Poll");
1013 assert_eq!(poll_response.total_eligible_voters, 10); }
1015
1016 #[tokio::test]
1017 async fn test_create_poll_invalid_end_date() {
1018 let use_cases = setup_use_cases();
1019 let building_id = Uuid::new_v4();
1020 let created_by = Uuid::new_v4();
1021
1022 let dto = CreatePollDto {
1023 building_id: building_id.to_string(),
1024 title: "Test Poll".to_string(),
1025 description: None,
1026 poll_type: "yes_no".to_string(),
1027 options: vec![],
1028 is_anonymous: None,
1029 allow_multiple_votes: None,
1030 require_all_owners: None,
1031 ends_at: (Utc::now() - chrono::Duration::days(1))
1032 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1033 };
1034
1035 let result = use_cases.create_poll(dto, created_by).await;
1036 assert!(result.is_err());
1037 assert!(result.unwrap_err().contains("must be in the future"));
1038 }
1039
1040 async fn make_active_poll(
1051 use_cases: &PollUseCases,
1052 building_id: Uuid,
1053 created_by: Uuid,
1054 ) -> Uuid {
1055 let dto = CreatePollDto {
1056 building_id: building_id.to_string(),
1057 title: "Faut-il repeindre le hall ?".to_string(),
1058 description: None,
1059 poll_type: "yes_no".to_string(),
1060 options: vec![
1061 CreatePollOptionDto {
1062 id: None,
1063 option_text: "Oui".to_string(),
1064 attachment_url: None,
1065 display_order: 0,
1066 },
1067 CreatePollOptionDto {
1068 id: None,
1069 option_text: "Non".to_string(),
1070 attachment_url: None,
1071 display_order: 1,
1072 },
1073 ],
1074 is_anonymous: Some(true),
1075 allow_multiple_votes: Some(false),
1076 require_all_owners: None,
1077 ends_at: (Utc::now() + chrono::Duration::days(7))
1078 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
1079 };
1080 let created = use_cases.create_poll(dto, created_by).await.unwrap();
1081 let poll_id = Uuid::parse_str(&created.id).unwrap();
1082 use_cases.publish_poll(poll_id, created_by).await.unwrap();
1083 poll_id
1084 }
1085
1086 #[tokio::test]
1087 async fn edge_vote_anonyme_reste_possible_sans_owner_id() {
1088 let use_cases = setup_use_cases();
1092 let building_id = Uuid::new_v4();
1093 let created_by = Uuid::new_v4();
1094 let poll_id = make_active_poll(&use_cases, building_id, created_by).await;
1095 let poll = use_cases.get_poll(poll_id).await.unwrap();
1096 let option_id = poll.options.first().unwrap().id.clone();
1097
1098 let vote_dto = CastVoteDto {
1099 poll_id: poll_id.to_string(),
1100 selected_option_ids: Some(vec![option_id]),
1101 rating_value: None,
1102 open_text: None,
1103 };
1104
1105 let result = use_cases.cast_vote(vote_dto, None).await;
1106 assert!(
1107 result.is_ok(),
1108 "un vote anonyme (owner_id=None) doit rester possible : {:?}",
1109 result.err()
1110 );
1111 }
1112
1113 #[tokio::test]
1114 async fn security_owner_hors_immeuble_est_refuse() {
1115 let use_cases = setup_use_cases();
1116 let building_id = Uuid::new_v4();
1117 let created_by = Uuid::new_v4();
1118 let poll_id = make_active_poll(&use_cases, building_id, created_by).await;
1119 let poll = use_cases.get_poll(poll_id).await.unwrap();
1120 let option_id = poll.options.first().unwrap().id.clone();
1121
1122 let outsider_owner_id = Uuid::new_v4();
1125 let vote_dto = CastVoteDto {
1126 poll_id: poll_id.to_string(),
1127 selected_option_ids: Some(vec![option_id]),
1128 rating_value: None,
1129 open_text: None,
1130 };
1131
1132 let result = use_cases.cast_vote(vote_dto, Some(outsider_owner_id)).await;
1133 assert!(result.is_err());
1134 assert!(result.unwrap_err().contains("not authorized to vote"));
1135 }
1136}