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(
318 &self,
319 dto: CastVoteDto,
320 owner_id: Option<Uuid>,
321 ) -> Result<String, String> {
322 let poll_id =
324 Uuid::parse_str(&dto.poll_id).map_err(|_| "Invalid poll ID format".to_string())?;
325
326 let mut poll = self
328 .poll_repository
329 .find_by_id(poll_id)
330 .await?
331 .ok_or_else(|| "Poll not found".to_string())?;
332
333 if poll.status != PollStatus::Active {
335 return Err("Poll is not active".to_string());
336 }
337
338 if Utc::now() > poll.ends_at {
340 return Err("Poll has expired".to_string());
341 }
342
343 if let Some(oid) = owner_id {
345 if !poll.is_anonymous {
346 let existing_vote = self
347 .poll_vote_repository
348 .find_by_poll_and_owner(poll_id, oid)
349 .await?;
350 if existing_vote.is_some() {
351 return Err("You have already voted on this poll".to_string());
352 }
353 }
354 }
355
356 let vote = match poll.poll_type {
358 PollType::YesNo | PollType::MultipleChoice => {
359 let selected_ids = dto
360 .selected_option_ids
361 .ok_or_else(|| "Selected option IDs required for this poll type".to_string())?
362 .iter()
363 .map(|id| {
364 Uuid::parse_str(id).map_err(|_| "Invalid option ID format".to_string())
365 })
366 .collect::<Result<Vec<Uuid>, String>>()?;
367
368 for opt_id in &selected_ids {
370 if !poll.options.iter().any(|o| &o.id == opt_id) {
371 return Err("Invalid option ID".to_string());
372 }
373 }
374
375 if !poll.allow_multiple_votes && selected_ids.len() > 1 {
377 return Err("This poll does not allow multiple votes".to_string());
378 }
379
380 PollVote::new(
381 poll_id,
382 owner_id,
383 poll.building_id,
384 selected_ids,
385 None,
386 None,
387 )?
388 }
389 PollType::Rating => {
390 let rating = dto
391 .rating_value
392 .ok_or_else(|| "Rating value required for rating poll".to_string())?;
393
394 PollVote::new(
395 poll_id,
396 owner_id,
397 poll.building_id,
398 vec![],
399 Some(rating),
400 None,
401 )?
402 }
403 PollType::OpenEnded => {
404 let text = dto
405 .open_text
406 .ok_or_else(|| "Open text required for open-ended poll".to_string())?;
407
408 PollVote::new(
409 poll_id,
410 owner_id,
411 poll.building_id,
412 vec![],
413 None,
414 Some(text),
415 )?
416 }
417 };
418
419 self.poll_vote_repository.create(&vote).await?;
421
422 poll.total_votes_cast += 1;
424
425 if matches!(poll.poll_type, PollType::YesNo | PollType::MultipleChoice) {
427 for opt_id in &vote.selected_option_ids {
428 if let Some(option) = poll.options.iter_mut().find(|o| &o.id == opt_id) {
429 option.vote_count += 1;
430 }
431 }
432 }
433
434 self.poll_repository.update(&poll).await?;
436
437 Ok("Vote cast successfully".to_string())
438 }
439
440 pub async fn get_poll_results(&self, poll_id: Uuid) -> Result<PollResultsDto, String> {
442 let poll = self
444 .poll_repository
445 .find_by_id(poll_id)
446 .await?
447 .ok_or_else(|| "Poll not found".to_string())?;
448
449 let winning_option = if matches!(poll.poll_type, PollType::YesNo | PollType::MultipleChoice)
451 {
452 poll.options
453 .iter()
454 .max_by_key(|opt| opt.vote_count)
455 .map(|opt| {
456 let vote_percentage = if poll.total_votes_cast > 0 {
457 (opt.vote_count as f64 / poll.total_votes_cast as f64) * 100.0
458 } else {
459 0.0
460 };
461 PollOptionDto {
462 id: opt.id.to_string(),
463 option_text: opt.option_text.clone(),
464 attachment_url: opt.attachment_url.clone(),
465 vote_count: opt.vote_count,
466 vote_percentage,
467 display_order: opt.display_order,
468 }
469 })
470 } else {
471 None
472 };
473
474 Ok(PollResultsDto {
475 poll_id: poll.id.to_string(),
476 total_votes_cast: poll.total_votes_cast,
477 total_eligible_voters: poll.total_eligible_voters,
478 participation_rate: poll.participation_rate(),
479 winning_option,
480 options: poll
481 .options
482 .iter()
483 .map(|opt| {
484 let vote_percentage = if poll.total_votes_cast > 0 {
485 (opt.vote_count as f64 / poll.total_votes_cast as f64) * 100.0
486 } else {
487 0.0
488 };
489 PollOptionDto {
490 id: opt.id.to_string(),
491 option_text: opt.option_text.clone(),
492 attachment_url: opt.attachment_url.clone(),
493 vote_count: opt.vote_count,
494 vote_percentage,
495 display_order: opt.display_order,
496 }
497 })
498 .collect(),
499 })
500 }
501
502 pub async fn get_building_statistics(
504 &self,
505 building_id: Uuid,
506 ) -> Result<crate::application::ports::PollStatistics, String> {
507 self.poll_repository
508 .get_building_statistics(building_id)
509 .await
510 }
511
512 pub async fn auto_close_expired_polls(&self) -> Result<usize, String> {
514 let expired_polls = self.poll_repository.find_expired_active().await?;
515 let mut closed_count = 0;
516
517 for mut poll in expired_polls {
518 if poll.close().is_ok() {
519 self.poll_repository.update(&poll).await?;
520 closed_count += 1;
521 }
522 }
523
524 Ok(closed_count)
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531 use crate::application::dto::CreatePollOptionDto;
532 use crate::application::ports::PollStatistics;
533 use async_trait::async_trait;
534 use std::collections::HashMap;
535 use std::sync::Mutex;
536
537 struct MockPollRepository {
539 polls: Mutex<HashMap<Uuid, Poll>>,
540 }
541
542 impl MockPollRepository {
543 fn new() -> Self {
544 Self {
545 polls: Mutex::new(HashMap::new()),
546 }
547 }
548 }
549
550 #[async_trait]
551 impl PollRepository for MockPollRepository {
552 async fn create(&self, poll: &Poll) -> Result<Poll, String> {
553 let mut polls = self.polls.lock().unwrap();
554 polls.insert(poll.id, poll.clone());
555 Ok(poll.clone())
556 }
557
558 async fn find_by_id(&self, id: Uuid) -> Result<Option<Poll>, String> {
559 let polls = self.polls.lock().unwrap();
560 Ok(polls.get(&id).cloned())
561 }
562
563 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Poll>, String> {
564 let polls = self.polls.lock().unwrap();
565 Ok(polls
566 .values()
567 .filter(|p| p.building_id == building_id)
568 .cloned()
569 .collect())
570 }
571
572 async fn find_by_created_by(&self, created_by: Uuid) -> Result<Vec<Poll>, String> {
573 let polls = self.polls.lock().unwrap();
574 Ok(polls
575 .values()
576 .filter(|p| p.created_by == created_by)
577 .cloned()
578 .collect())
579 }
580
581 async fn find_all_paginated(
582 &self,
583 _page_request: &PageRequest,
584 _filters: &PollFilters,
585 ) -> Result<(Vec<Poll>, i64), String> {
586 let polls = self.polls.lock().unwrap();
587 let all: Vec<Poll> = polls.values().cloned().collect();
588 let total = all.len() as i64;
589 Ok((all, total))
590 }
591
592 async fn find_active(&self, building_id: Uuid) -> Result<Vec<Poll>, String> {
593 let polls = self.polls.lock().unwrap();
594 Ok(polls
595 .values()
596 .filter(|p| p.building_id == building_id && p.status == PollStatus::Active)
597 .cloned()
598 .collect())
599 }
600
601 async fn find_by_status(
602 &self,
603 building_id: Uuid,
604 status: &str,
605 ) -> Result<Vec<Poll>, String> {
606 let polls = self.polls.lock().unwrap();
607 let poll_status = match status {
608 "draft" => PollStatus::Draft,
609 "active" => PollStatus::Active,
610 "closed" => PollStatus::Closed,
611 "cancelled" => PollStatus::Cancelled,
612 _ => return Err("Invalid status".to_string()),
613 };
614 Ok(polls
615 .values()
616 .filter(|p| p.building_id == building_id && p.status == poll_status)
617 .cloned()
618 .collect())
619 }
620
621 async fn find_expired_active(&self) -> Result<Vec<Poll>, String> {
622 let polls = self.polls.lock().unwrap();
623 Ok(polls
624 .values()
625 .filter(|p| p.status == PollStatus::Active && Utc::now() > p.ends_at)
626 .cloned()
627 .collect())
628 }
629
630 async fn update(&self, poll: &Poll) -> Result<Poll, String> {
631 let mut polls = self.polls.lock().unwrap();
632 polls.insert(poll.id, poll.clone());
633 Ok(poll.clone())
634 }
635
636 async fn delete(&self, id: Uuid) -> Result<bool, String> {
637 let mut polls = self.polls.lock().unwrap();
638 Ok(polls.remove(&id).is_some())
639 }
640
641 async fn get_building_statistics(
642 &self,
643 building_id: Uuid,
644 ) -> Result<PollStatistics, String> {
645 let polls = self.polls.lock().unwrap();
646 let building_polls: Vec<&Poll> = polls
647 .values()
648 .filter(|p| p.building_id == building_id)
649 .collect();
650
651 let total = building_polls.len() as i64;
652 let active = building_polls
653 .iter()
654 .filter(|p| p.status == PollStatus::Active)
655 .count() as i64;
656 let closed = building_polls
657 .iter()
658 .filter(|p| p.status == PollStatus::Closed)
659 .count() as i64;
660
661 let avg_participation = if total > 0 {
662 building_polls
663 .iter()
664 .map(|p| p.participation_rate())
665 .sum::<f64>()
666 / total as f64
667 } else {
668 0.0
669 };
670
671 Ok(PollStatistics {
672 total_polls: total,
673 active_polls: active,
674 closed_polls: closed,
675 average_participation_rate: avg_participation,
676 })
677 }
678 }
679
680 struct MockPollVoteRepository {
681 votes: Mutex<HashMap<Uuid, PollVote>>,
682 }
683
684 impl MockPollVoteRepository {
685 fn new() -> Self {
686 Self {
687 votes: Mutex::new(HashMap::new()),
688 }
689 }
690 }
691
692 #[async_trait]
693 impl PollVoteRepository for MockPollVoteRepository {
694 async fn create(&self, vote: &PollVote) -> Result<PollVote, String> {
695 let mut votes = self.votes.lock().unwrap();
696 votes.insert(vote.id, vote.clone());
697 Ok(vote.clone())
698 }
699
700 async fn find_by_id(&self, id: Uuid) -> Result<Option<PollVote>, String> {
701 let votes = self.votes.lock().unwrap();
702 Ok(votes.get(&id).cloned())
703 }
704
705 async fn find_by_poll(&self, poll_id: Uuid) -> Result<Vec<PollVote>, String> {
706 let votes = self.votes.lock().unwrap();
707 Ok(votes
708 .values()
709 .filter(|v| v.poll_id == poll_id)
710 .cloned()
711 .collect())
712 }
713
714 async fn find_by_poll_and_owner(
715 &self,
716 poll_id: Uuid,
717 owner_id: Uuid,
718 ) -> Result<Option<PollVote>, String> {
719 let votes = self.votes.lock().unwrap();
720 Ok(votes
721 .values()
722 .find(|v| v.poll_id == poll_id && v.owner_id == Some(owner_id))
723 .cloned())
724 }
725
726 async fn find_by_owner(&self, owner_id: Uuid) -> Result<Vec<PollVote>, String> {
727 let votes = self.votes.lock().unwrap();
728 Ok(votes
729 .values()
730 .filter(|v| v.owner_id == Some(owner_id))
731 .cloned()
732 .collect())
733 }
734
735 async fn delete(&self, id: Uuid) -> Result<bool, String> {
736 let mut votes = self.votes.lock().unwrap();
737 Ok(votes.remove(&id).is_some())
738 }
739 }
740
741 struct MockUnitOwnerRepository;
742
743 #[async_trait]
744 impl UnitOwnerRepository for MockUnitOwnerRepository {
745 async fn create(
746 &self,
747 _unit_owner: &crate::domain::entities::UnitOwner,
748 ) -> Result<crate::domain::entities::UnitOwner, String> {
749 unimplemented!()
750 }
751
752 async fn find_by_id(
753 &self,
754 _id: Uuid,
755 ) -> Result<Option<crate::domain::entities::UnitOwner>, String> {
756 unimplemented!()
757 }
758
759 async fn find_current_owners_by_unit(
760 &self,
761 _unit_id: Uuid,
762 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
763 unimplemented!()
764 }
765
766 async fn find_current_units_by_owner(
767 &self,
768 _owner_id: Uuid,
769 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
770 unimplemented!()
771 }
772
773 async fn find_all_owners_by_unit(
774 &self,
775 _unit_id: Uuid,
776 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
777 unimplemented!()
778 }
779
780 async fn find_all_units_by_owner(
781 &self,
782 _owner_id: Uuid,
783 ) -> Result<Vec<crate::domain::entities::UnitOwner>, String> {
784 unimplemented!()
785 }
786
787 async fn update(
788 &self,
789 _unit_owner: &crate::domain::entities::UnitOwner,
790 ) -> Result<crate::domain::entities::UnitOwner, String> {
791 unimplemented!()
792 }
793
794 async fn delete(&self, _id: Uuid) -> Result<(), String> {
795 unimplemented!()
796 }
797
798 async fn has_active_owners(&self, _unit_id: Uuid) -> Result<bool, String> {
799 unimplemented!()
800 }
801
802 async fn get_total_ownership_percentage(&self, _unit_id: Uuid) -> Result<f64, String> {
803 unimplemented!()
804 }
805
806 async fn find_active_by_unit_and_owner(
807 &self,
808 _unit_id: Uuid,
809 _owner_id: Uuid,
810 ) -> Result<Option<crate::domain::entities::UnitOwner>, String> {
811 unimplemented!()
812 }
813
814 async fn find_active_by_building(
815 &self,
816 _building_id: Uuid,
817 ) -> Result<Vec<(Uuid, Uuid, f64)>, String> {
818 Ok((0..10)
821 .map(|_| (Uuid::new_v4(), Uuid::new_v4(), 0.1))
822 .collect())
823 }
824 }
825
826 struct MockOwnerRepository;
827
828 #[async_trait]
829 impl OwnerRepository for MockOwnerRepository {
830 async fn create(
831 &self,
832 _owner: &crate::domain::entities::Owner,
833 ) -> Result<crate::domain::entities::Owner, String> {
834 unimplemented!()
835 }
836
837 async fn find_by_id(
838 &self,
839 _id: Uuid,
840 ) -> Result<Option<crate::domain::entities::Owner>, String> {
841 unimplemented!()
842 }
843
844 async fn find_by_email(
845 &self,
846 _email: &str,
847 ) -> Result<Option<crate::domain::entities::Owner>, String> {
848 unimplemented!()
849 }
850
851 async fn find_all(&self) -> Result<Vec<crate::domain::entities::Owner>, String> {
852 unimplemented!()
853 }
854
855 async fn find_all_paginated(
856 &self,
857 _page_request: &crate::application::dto::PageRequest,
858 _filters: &crate::application::dto::OwnerFilters,
859 ) -> Result<(Vec<crate::domain::entities::Owner>, i64), String> {
860 unimplemented!()
861 }
862
863 async fn update(
864 &self,
865 _owner: &crate::domain::entities::Owner,
866 ) -> Result<crate::domain::entities::Owner, String> {
867 unimplemented!()
868 }
869
870 async fn delete(&self, _id: Uuid) -> Result<bool, String> {
871 unimplemented!()
872 }
873 }
874
875 fn setup_use_cases() -> PollUseCases {
876 PollUseCases::new(
877 Arc::new(MockPollRepository::new()),
878 Arc::new(MockPollVoteRepository::new()),
879 Arc::new(MockOwnerRepository),
880 Arc::new(MockUnitOwnerRepository),
881 )
882 }
883
884 #[tokio::test]
885 async fn test_create_poll_success() {
886 let use_cases = setup_use_cases();
887 let building_id = Uuid::new_v4();
888 let created_by = Uuid::new_v4();
889
890 let dto = CreatePollDto {
891 building_id: building_id.to_string(),
892 title: "Test Poll".to_string(),
893 description: Some("Test description".to_string()),
894 poll_type: "yes_no".to_string(),
895 options: vec![
896 CreatePollOptionDto {
897 id: None,
898 option_text: "Yes".to_string(),
899 attachment_url: None,
900 display_order: 0,
901 },
902 CreatePollOptionDto {
903 id: None,
904 option_text: "No".to_string(),
905 attachment_url: None,
906 display_order: 1,
907 },
908 ],
909 is_anonymous: Some(false),
910 allow_multiple_votes: Some(false),
911 require_all_owners: Some(false),
912 ends_at: (Utc::now() + chrono::Duration::days(7))
913 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
914 };
915
916 let result = use_cases.create_poll(dto, created_by).await;
917 assert!(result.is_ok());
918
919 let poll_response = result.unwrap();
920 assert_eq!(poll_response.title, "Test Poll");
921 assert_eq!(poll_response.total_eligible_voters, 10); }
923
924 #[tokio::test]
925 async fn test_create_poll_invalid_end_date() {
926 let use_cases = setup_use_cases();
927 let building_id = Uuid::new_v4();
928 let created_by = Uuid::new_v4();
929
930 let dto = CreatePollDto {
931 building_id: building_id.to_string(),
932 title: "Test Poll".to_string(),
933 description: None,
934 poll_type: "yes_no".to_string(),
935 options: vec![],
936 is_anonymous: None,
937 allow_multiple_votes: None,
938 require_all_owners: None,
939 ends_at: (Utc::now() - chrono::Duration::days(1))
940 .to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
941 };
942
943 let result = use_cases.create_poll(dto, created_by).await;
944 assert!(result.is_err());
945 assert!(result.unwrap_err().contains("must be in the future"));
946 }
947}