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        // Check if user already voted (if not anonymous)
344        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        // Validate vote based on poll type
357        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                // Validate options exist in poll
369                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                // Validate multiple votes setting
376                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        // Save vote
420        self.poll_vote_repository.create(&vote).await?;
421
422        // Update poll vote count and option counts
423        poll.total_votes_cast += 1;
424
425        // Update option vote counts for YesNo/MultipleChoice
426        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        // Save updated poll
435        self.poll_repository.update(&poll).await?;
436
437        Ok("Vote cast successfully".to_string())
438    }
439
440    /// Get poll results
441    pub async fn get_poll_results(&self, poll_id: Uuid) -> Result<PollResultsDto, String> {
442        // Fetch poll
443        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        // Calculate winning option (for YesNo/MultipleChoice)
450        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    /// Get poll statistics for a building
503    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    /// Find and auto-close expired polls (for background job)
513    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    // Mock repositories
538    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            // Return 10 unique owners (unit_id, owner_id, ownership_percentage)
819            // This matches the old hardcoded total_eligible_voters = 10
820            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); // Mock returns 10 owners
922    }
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}