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