koprogo_api/application/use_cases/
poll_use_cases.rs

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    /// Create a new poll (draft status)
38    pub async fn create_poll(
39        &self,
40        dto: CreatePollDto,
41        created_by: Uuid,
42    ) -> Result<PollResponseDto, String> {
43        // Parse UUIDs
44        let building_id = Uuid::parse_str(&dto.building_id)
45            .map_err(|_| "Invalid building ID format".to_string())?;
46
47        // Parse end date
48        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        // Validate end date is in future
53        if ends_at <= Utc::now() {
54            return Err("Poll end date must be in the future".to_string());
55        }
56
57        // Convert DTO poll type to domain poll type
58        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        // Convert options
67        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        // Count eligible voters by querying active unit_owners for building
80        // Each unique owner in the building counts as 1 eligible voter
81        let active_unit_owners = self
82            .unit_owner_repository
83            .find_active_by_building(building_id)
84            .await?;
85
86        // Count unique owner IDs (each owner counts once regardless of how many units they own)
87        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        // Create poll entity
95        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        // Set optional fields
108        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        // Save to repository
112        let created_poll = self.poll_repository.create(&poll).await?;
113
114        Ok(PollResponseDto::from(created_poll))
115    }
116
117    /// Update an existing poll (only if in draft status)
118    pub async fn update_poll(
119        &self,
120        poll_id: Uuid,
121        dto: UpdatePollDto,
122        user_id: Uuid,
123    ) -> Result<PollResponseDto, String> {
124        // Fetch existing poll
125        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        // Verify user is the creator
132        if poll.created_by != user_id {
133            return Err("Only the poll creator can update the poll".to_string());
134        }
135
136        // Only allow updates to draft polls
137        if poll.status != PollStatus::Draft {
138            return Err("Cannot update poll that is no longer in draft status".to_string());
139        }
140
141        // Update fields
142        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        // Save updated poll
167        let updated_poll = self.poll_repository.update(&poll).await?;
168
169        Ok(PollResponseDto::from(updated_poll))
170    }
171
172    /// Get poll by ID
173    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    /// List polls with pagination and filters
184    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    /// Find active polls for a building
205    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    /// Publish a draft poll (change status to Active)
214    pub async fn publish_poll(
215        &self,
216        poll_id: Uuid,
217        user_id: Uuid,
218    ) -> Result<PollResponseDto, String> {
219        // Fetch poll
220        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        // Verify user is the creator
227        if poll.created_by != user_id {
228            return Err("Only the poll creator can publish the poll".to_string());
229        }
230
231        // Publish poll (activate it)
232        poll.publish()?;
233
234        // Save
235        let updated_poll = self.poll_repository.update(&poll).await?;
236
237        Ok(PollResponseDto::from(updated_poll))
238    }
239
240    /// Close a poll manually
241    pub async fn close_poll(
242        &self,
243        poll_id: Uuid,
244        user_id: Uuid,
245    ) -> Result<PollResponseDto, String> {
246        // Fetch poll
247        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        // Verify user is the creator
254        if poll.created_by != user_id {
255            return Err("Only the poll creator can close the poll".to_string());
256        }
257
258        // Close poll
259        poll.close()?;
260
261        // Save
262        let updated_poll = self.poll_repository.update(&poll).await?;
263
264        Ok(PollResponseDto::from(updated_poll))
265    }
266
267    /// Cancel a poll
268    pub async fn cancel_poll(
269        &self,
270        poll_id: Uuid,
271        user_id: Uuid,
272    ) -> Result<PollResponseDto, String> {
273        // Fetch poll
274        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        // Verify user is the creator
281        if poll.created_by != user_id {
282            return Err("Only the poll creator can cancel the poll".to_string());
283        }
284
285        // Cancel poll
286        poll.cancel()?;
287
288        // Save
289        let updated_poll = self.poll_repository.update(&poll).await?;
290
291        Ok(PollResponseDto::from(updated_poll))
292    }
293
294    /// Delete a poll (only if in draft or cancelled status)
295    pub async fn delete_poll(&self, poll_id: Uuid, user_id: Uuid) -> Result<bool, String> {
296        // Fetch poll
297        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        // Verify user is the creator
304        if poll.created_by != user_id {
305            return Err("Only the poll creator can delete the poll".to_string());
306        }
307
308        // Only allow deletion of draft or cancelled polls
309        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    /// Cast a vote on a poll
317    pub async fn cast_vote(
318        &self,
319        dto: CastVoteDto,
320        owner_id: Option<Uuid>,
321    ) -> Result<String, String> {
322        // Parse poll ID
323        let poll_id =
324            Uuid::parse_str(&dto.poll_id).map_err(|_| "Invalid poll ID format".to_string())?;
325
326        // Fetch poll
327        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        // Verify poll is active
334        if poll.status != PollStatus::Active {
335            return Err("Poll is not active".to_string());
336        }
337
338        // Verify poll hasn't expired
339        if Utc::now() > poll.ends_at {
340            return Err("Poll has expired".to_string());
341        }
342
343        // Verify owner belongs to the building (authorization check)
344        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        // Check if user already voted (if not anonymous)
356        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        // Validate vote based on poll type
369        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                // Validate options exist in poll
381                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                // Validate multiple votes setting
388                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        // Save vote
432        self.poll_vote_repository.create(&vote).await?;
433
434        // Update poll vote count and option counts
435        poll.total_votes_cast += 1;
436
437        // Update option vote counts for YesNo/MultipleChoice
438        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        // Save updated poll
447        self.poll_repository.update(&poll).await?;
448
449        Ok("Vote cast successfully".to_string())
450    }
451
452    /// Get poll results
453    pub async fn get_poll_results(&self, poll_id: Uuid) -> Result<PollResultsDto, String> {
454        // Fetch poll
455        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        // Calculate winning option (for YesNo/MultipleChoice)
462        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    /// Get poll statistics for a building
515    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    /// Find and auto-close expired polls (for background job)
525    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    // Mock repositories
550    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            // Return 10 unique owners (unit_id, owner_id, ownership_percentage)
831            // This matches the old hardcoded total_eligible_voters = 10
832            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); // Mock returns 10 owners
949    }
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}