koprogo_api/application/use_cases/
resolution_use_cases.rs

1use crate::application::ports::{MeetingRepository, ResolutionRepository, VoteRepository};
2use crate::domain::entities::{
3    MajorityType, Resolution, ResolutionStatus, ResolutionType, Vote, VoteChoice,
4};
5use std::sync::Arc;
6use uuid::Uuid;
7
8pub struct ResolutionUseCases {
9    resolution_repository: Arc<dyn ResolutionRepository>,
10    vote_repository: Arc<dyn VoteRepository>,
11    meeting_repository: Arc<dyn MeetingRepository>,
12}
13
14impl ResolutionUseCases {
15    pub fn new(
16        resolution_repository: Arc<dyn ResolutionRepository>,
17        vote_repository: Arc<dyn VoteRepository>,
18        meeting_repository: Arc<dyn MeetingRepository>,
19    ) -> Self {
20        Self {
21            resolution_repository,
22            vote_repository,
23            meeting_repository,
24        }
25    }
26
27    /// Create a new resolution for a meeting
28    /// Enforces quorum validation per Art. 3.87 §5 CC before allowing resolution creation.
29    /// Issue #310: Validates agenda_item_index if provided (Art. 3.87 CC - only agenda items can be voted on)
30    pub async fn create_resolution(
31        &self,
32        meeting_id: Uuid,
33        title: String,
34        description: String,
35        resolution_type: ResolutionType,
36        majority_required: MajorityType,
37        agenda_item_index: Option<usize>,
38    ) -> Result<Resolution, String> {
39        // Fetch the meeting and check quorum (Art. 3.87 §5 CC)
40        let meeting = self
41            .meeting_repository
42            .find_by_id(meeting_id)
43            .await?
44            .ok_or_else(|| format!("Meeting not found: {}", meeting_id))?;
45
46        // Enforce quorum validation before allowing resolution creation
47        meeting.check_quorum_for_voting()?;
48
49        // Issue #310: If agenda_item_index provided, validate it exists and is non-empty
50        if let Some(index) = agenda_item_index {
51            if index >= meeting.agenda.len() {
52                return Err(
53                    "Resolution must correspond to a valid agenda item (Art. 3.87 CC)".to_string(),
54                );
55            }
56            let agenda_item = &meeting.agenda[index];
57            if agenda_item.trim().is_empty() {
58                return Err("Agenda item cannot be empty (Art. 3.87 CC)".to_string());
59            }
60        }
61
62        let resolution = Resolution::new(
63            meeting_id,
64            title,
65            description,
66            resolution_type,
67            majority_required,
68            agenda_item_index,
69        )?;
70
71        self.resolution_repository.create(&resolution).await
72    }
73
74    /// Get a resolution by ID
75    pub async fn get_resolution(&self, id: Uuid) -> Result<Option<Resolution>, String> {
76        self.resolution_repository.find_by_id(id).await
77    }
78
79    /// Get all resolutions for a meeting
80    pub async fn get_meeting_resolutions(
81        &self,
82        meeting_id: Uuid,
83    ) -> Result<Vec<Resolution>, String> {
84        self.resolution_repository
85            .find_by_meeting_id(meeting_id)
86            .await
87    }
88
89    /// Get resolutions by status
90    pub async fn get_resolutions_by_status(
91        &self,
92        status: ResolutionStatus,
93    ) -> Result<Vec<Resolution>, String> {
94        self.resolution_repository.find_by_status(status).await
95    }
96
97    /// Update a resolution (only allowed if status is Pending)
98    pub async fn update_resolution(&self, resolution: &Resolution) -> Result<Resolution, String> {
99        if resolution.status != ResolutionStatus::Pending {
100            return Err("Cannot update a resolution that is not pending".to_string());
101        }
102
103        self.resolution_repository.update(resolution).await
104    }
105
106    /// Delete a resolution (only allowed if no votes have been cast)
107    pub async fn delete_resolution(&self, id: Uuid) -> Result<bool, String> {
108        // Check if any votes exist
109        let votes = self.vote_repository.find_by_resolution_id(id).await?;
110        if !votes.is_empty() {
111            return Err("Cannot delete a resolution with existing votes".to_string());
112        }
113
114        self.resolution_repository.delete(id).await
115    }
116
117    /// Cast a vote on a resolution
118    pub async fn cast_vote(
119        &self,
120        resolution_id: Uuid,
121        owner_id: Uuid,
122        unit_id: Uuid,
123        vote_choice: VoteChoice,
124        voting_power: f64,
125        proxy_owner_id: Option<Uuid>,
126    ) -> Result<Vote, String> {
127        // Check if resolution exists and is pending
128        let resolution = self
129            .resolution_repository
130            .find_by_id(resolution_id)
131            .await?
132            .ok_or_else(|| "Resolution not found".to_string())?;
133
134        if resolution.status != ResolutionStatus::Pending {
135            return Err("Cannot vote on a resolution that is not pending".to_string());
136        }
137
138        // Check if unit has already voted
139        if self
140            .vote_repository
141            .has_voted(resolution_id, unit_id)
142            .await?
143        {
144            return Err("This unit has already voted on this resolution".to_string());
145        }
146
147        // Art. 3.87 §7 CC — un mandataire ne peut détenir plus de 3 procurations
148        // Exception : si le total des voix de ses procurations < 10% du total général
149        if let Some(proxy_id) = proxy_owner_id {
150            self.validate_proxy_limit(resolution_id, proxy_id, voting_power)
151                .await?;
152        }
153
154        // Create and save the vote
155        let vote = Vote::new(
156            resolution_id,
157            owner_id,
158            unit_id,
159            vote_choice.clone(),
160            voting_power,
161            proxy_owner_id,
162        )?;
163
164        let created_vote = self.vote_repository.create(&vote).await?;
165
166        // Update vote counts on the resolution
167        self.update_resolution_vote_counts(resolution_id).await?;
168
169        Ok(created_vote)
170    }
171
172    /// Change a vote (if allowed by business rules)
173    pub async fn change_vote(&self, vote_id: Uuid, new_choice: VoteChoice) -> Result<Vote, String> {
174        let mut vote = self
175            .vote_repository
176            .find_by_id(vote_id)
177            .await?
178            .ok_or_else(|| "Vote not found".to_string())?;
179
180        // Check if resolution is still pending
181        let resolution = self
182            .resolution_repository
183            .find_by_id(vote.resolution_id)
184            .await?
185            .ok_or_else(|| "Resolution not found".to_string())?;
186
187        if resolution.status != ResolutionStatus::Pending {
188            return Err("Cannot change vote on a closed resolution".to_string());
189        }
190
191        // Update the vote
192        vote.change_vote(new_choice)?;
193        let updated_vote = self.vote_repository.update(&vote).await?;
194
195        // Recalculate vote counts
196        self.update_resolution_vote_counts(vote.resolution_id)
197            .await?;
198
199        Ok(updated_vote)
200    }
201
202    /// Get all votes for a resolution
203    pub async fn get_resolution_votes(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
204        self.vote_repository
205            .find_by_resolution_id(resolution_id)
206            .await
207    }
208
209    /// Get all votes by an owner
210    pub async fn get_owner_votes(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
211        self.vote_repository.find_by_owner_id(owner_id).await
212    }
213
214    /// Close voting on a resolution and calculate final result
215    pub async fn close_voting(
216        &self,
217        resolution_id: Uuid,
218        total_voting_power: f64,
219    ) -> Result<Resolution, String> {
220        let mut resolution = self
221            .resolution_repository
222            .find_by_id(resolution_id)
223            .await?
224            .ok_or_else(|| "Resolution not found".to_string())?;
225
226        if resolution.status != ResolutionStatus::Pending {
227            return Err("Resolution voting is already closed".to_string());
228        }
229
230        // Calculate final result
231        resolution.close_voting(total_voting_power)?;
232
233        // Update resolution with final status
234        self.resolution_repository
235            .close_voting(resolution_id, resolution.status.clone())
236            .await?;
237
238        // Fetch updated resolution
239        self.resolution_repository
240            .find_by_id(resolution_id)
241            .await?
242            .ok_or_else(|| "Resolution not found after closing".to_string())
243    }
244
245    /// Get vote summary for a meeting (all resolutions with their results)
246    pub async fn get_meeting_vote_summary(
247        &self,
248        meeting_id: Uuid,
249    ) -> Result<Vec<Resolution>, String> {
250        self.resolution_repository
251            .get_meeting_vote_summary(meeting_id)
252            .await
253    }
254
255    /// Helper: Update vote counts for a resolution based on actual votes
256    async fn update_resolution_vote_counts(&self, resolution_id: Uuid) -> Result<(), String> {
257        // Get vote counts
258        let (pour_count, contre_count, abstention_count) = self
259            .vote_repository
260            .count_by_resolution_and_choice(resolution_id)
261            .await?;
262
263        // Get voting power totals
264        let (pour_power, contre_power, abstention_power) = self
265            .vote_repository
266            .sum_voting_power_by_resolution(resolution_id)
267            .await?;
268
269        // Update resolution
270        self.resolution_repository
271            .update_vote_counts(
272                resolution_id,
273                pour_count,
274                contre_count,
275                abstention_count,
276                pour_power,
277                contre_power,
278                abstention_power,
279            )
280            .await
281    }
282
283    /// Check if a unit has voted on a resolution
284    pub async fn has_unit_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
285        self.vote_repository.has_voted(resolution_id, unit_id).await
286    }
287
288    /// Valide la limite de procurations par mandataire (Art. 3.87 §7 CC).
289    ///
290    /// Règle: un mandataire ne peut détenir plus de 3 procurations.
291    /// Exception: si le total des voix représentées < 10% du total général,
292    /// la limite de 3 ne s'applique pas.
293    ///
294    /// Le `new_voting_power` est le pouvoir de vote de la nouvelle procuration
295    /// envisagée (utilisé pour le calcul de l'exception 10%).
296    async fn validate_proxy_limit(
297        &self,
298        resolution_id: Uuid,
299        proxy_owner_id: Uuid,
300        new_voting_power: f64,
301    ) -> Result<(), String> {
302        let (existing_count, existing_power) = self
303            .vote_repository
304            .count_proxy_votes_for_mandataire(resolution_id, proxy_owner_id)
305            .await?;
306
307        // Total du pouvoir de vote représenté après ajout de la nouvelle procuration
308        let total_proxy_power = existing_power + new_voting_power;
309
310        // Exception 10% : si le total des procurations < 10% du total AG, pas de limite
311        // On récupère le total de la résolution
312        let resolution = self
313            .resolution_repository
314            .find_by_id(resolution_id)
315            .await?
316            .ok_or_else(|| "Resolution not found".to_string())?;
317
318        // total_voting_power_* contient les votes déjà exprimés
319        // Pour l'exception, on compare la puissance des procurations au total millièmes
320        // En pratique, le syndic passe le total_quotas du meeting — on utilise une heuristique
321        let total_all_votes = resolution.total_voting_power_pour
322            + resolution.total_voting_power_contre
323            + resolution.total_voting_power_abstention
324            + new_voting_power; // inclure ce qu'on est en train d'ajouter
325
326        // Exception: si total procurations < 10% du total général → pas de limite
327        if total_all_votes > 0.0 && (total_proxy_power / total_all_votes) < 0.10 {
328            return Ok(()); // Exception 10% s'applique
329        }
330
331        // Règle générale: max 3 procurations
332        if existing_count >= 3 {
333            return Err(format!(
334                "Le mandataire détient déjà {} procurations. Maximum autorisé : 3 (Art. 3.87 §7 CC). \
335                 Exception 10% non applicable (procurations représentent >{:.1}% des votes).",
336                existing_count,
337                if total_all_votes > 0.0 {
338                    (total_proxy_power / total_all_votes) * 100.0
339                } else {
340                    0.0
341                }
342            ));
343        }
344
345        Ok(())
346    }
347
348    /// Get vote statistics for a resolution
349    pub async fn get_vote_statistics(&self, resolution_id: Uuid) -> Result<VoteStatistics, String> {
350        let resolution = self
351            .resolution_repository
352            .find_by_id(resolution_id)
353            .await?
354            .ok_or_else(|| "Resolution not found".to_string())?;
355
356        Ok(VoteStatistics {
357            total_votes: resolution.total_votes(),
358            vote_count_pour: resolution.vote_count_pour,
359            vote_count_contre: resolution.vote_count_contre,
360            vote_count_abstention: resolution.vote_count_abstention,
361            total_voting_power_pour: resolution.total_voting_power_pour,
362            total_voting_power_contre: resolution.total_voting_power_contre,
363            total_voting_power_abstention: resolution.total_voting_power_abstention,
364            pour_percentage: resolution.pour_percentage(),
365            contre_percentage: resolution.contre_percentage(),
366            abstention_percentage: resolution.abstention_percentage(),
367            status: resolution.status,
368        })
369    }
370}
371
372/// Vote statistics for a resolution
373#[derive(Debug, Clone)]
374pub struct VoteStatistics {
375    pub total_votes: i32,
376    pub vote_count_pour: i32,
377    pub vote_count_contre: i32,
378    pub vote_count_abstention: i32,
379    pub total_voting_power_pour: f64,
380    pub total_voting_power_contre: f64,
381    pub total_voting_power_abstention: f64,
382    pub pour_percentage: f64,
383    pub contre_percentage: f64,
384    pub abstention_percentage: f64,
385    pub status: ResolutionStatus,
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391    use crate::application::dto::PageRequest;
392    use crate::application::ports::{MeetingRepository, ResolutionRepository, VoteRepository};
393    use crate::domain::entities::{Meeting, MeetingType};
394    use async_trait::async_trait;
395    use chrono::Utc;
396    use std::collections::HashMap;
397    use std::sync::Mutex;
398
399    // Mock repositories for testing
400    struct MockMeetingRepository {
401        meetings: Mutex<HashMap<Uuid, Meeting>>,
402    }
403
404    impl MockMeetingRepository {
405        fn new() -> Self {
406            Self {
407                meetings: Mutex::new(HashMap::new()),
408            }
409        }
410    }
411
412    #[async_trait]
413    impl MeetingRepository for MockMeetingRepository {
414        async fn create(&self, meeting: &Meeting) -> Result<Meeting, String> {
415            self.meetings
416                .lock()
417                .unwrap()
418                .insert(meeting.id, meeting.clone());
419            Ok(meeting.clone())
420        }
421
422        async fn find_by_id(&self, id: Uuid) -> Result<Option<Meeting>, String> {
423            Ok(self.meetings.lock().unwrap().get(&id).cloned())
424        }
425
426        async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Meeting>, String> {
427            Ok(self
428                .meetings
429                .lock()
430                .unwrap()
431                .values()
432                .filter(|m| m.building_id == building_id)
433                .cloned()
434                .collect())
435        }
436
437        async fn update(&self, meeting: &Meeting) -> Result<Meeting, String> {
438            self.meetings
439                .lock()
440                .unwrap()
441                .insert(meeting.id, meeting.clone());
442            Ok(meeting.clone())
443        }
444
445        async fn delete(&self, id: Uuid) -> Result<bool, String> {
446            Ok(self.meetings.lock().unwrap().remove(&id).is_some())
447        }
448
449        async fn find_all_paginated(
450            &self,
451            _page_request: &PageRequest,
452            _organization_id: Option<Uuid>,
453        ) -> Result<(Vec<Meeting>, i64), String> {
454            let meetings: Vec<_> = self.meetings.lock().unwrap().values().cloned().collect();
455            let total = meetings.len() as i64;
456            Ok((meetings, total))
457        }
458    }
459
460    struct MockResolutionRepository {
461        resolutions: Mutex<HashMap<Uuid, Resolution>>,
462    }
463
464    impl MockResolutionRepository {
465        fn new() -> Self {
466            Self {
467                resolutions: Mutex::new(HashMap::new()),
468            }
469        }
470    }
471
472    #[async_trait]
473    impl ResolutionRepository for MockResolutionRepository {
474        async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
475            self.resolutions
476                .lock()
477                .unwrap()
478                .insert(resolution.id, resolution.clone());
479            Ok(resolution.clone())
480        }
481
482        async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
483            Ok(self.resolutions.lock().unwrap().get(&id).cloned())
484        }
485
486        async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
487            Ok(self
488                .resolutions
489                .lock()
490                .unwrap()
491                .values()
492                .filter(|r| r.meeting_id == meeting_id)
493                .cloned()
494                .collect())
495        }
496
497        async fn find_by_status(
498            &self,
499            status: ResolutionStatus,
500        ) -> Result<Vec<Resolution>, String> {
501            Ok(self
502                .resolutions
503                .lock()
504                .unwrap()
505                .values()
506                .filter(|r| r.status == status)
507                .cloned()
508                .collect())
509        }
510
511        async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
512            self.resolutions
513                .lock()
514                .unwrap()
515                .insert(resolution.id, resolution.clone());
516            Ok(resolution.clone())
517        }
518
519        async fn delete(&self, id: Uuid) -> Result<bool, String> {
520            Ok(self.resolutions.lock().unwrap().remove(&id).is_some())
521        }
522
523        async fn update_vote_counts(
524            &self,
525            resolution_id: Uuid,
526            vote_count_pour: i32,
527            vote_count_contre: i32,
528            vote_count_abstention: i32,
529            total_voting_power_pour: f64,
530            total_voting_power_contre: f64,
531            total_voting_power_abstention: f64,
532        ) -> Result<(), String> {
533            if let Some(resolution) = self.resolutions.lock().unwrap().get_mut(&resolution_id) {
534                resolution.vote_count_pour = vote_count_pour;
535                resolution.vote_count_contre = vote_count_contre;
536                resolution.vote_count_abstention = vote_count_abstention;
537                resolution.total_voting_power_pour = total_voting_power_pour;
538                resolution.total_voting_power_contre = total_voting_power_contre;
539                resolution.total_voting_power_abstention = total_voting_power_abstention;
540            }
541            Ok(())
542        }
543
544        async fn close_voting(
545            &self,
546            resolution_id: Uuid,
547            final_status: ResolutionStatus,
548        ) -> Result<(), String> {
549            if let Some(resolution) = self.resolutions.lock().unwrap().get_mut(&resolution_id) {
550                resolution.status = final_status;
551                resolution.voted_at = Some(chrono::Utc::now());
552            }
553            Ok(())
554        }
555
556        async fn get_meeting_vote_summary(
557            &self,
558            meeting_id: Uuid,
559        ) -> Result<Vec<Resolution>, String> {
560            self.find_by_meeting_id(meeting_id).await
561        }
562    }
563
564    struct MockVoteRepository {
565        votes: Mutex<HashMap<Uuid, Vote>>,
566    }
567
568    impl MockVoteRepository {
569        fn new() -> Self {
570            Self {
571                votes: Mutex::new(HashMap::new()),
572            }
573        }
574    }
575
576    #[async_trait]
577    impl VoteRepository for MockVoteRepository {
578        async fn create(&self, vote: &Vote) -> Result<Vote, String> {
579            self.votes.lock().unwrap().insert(vote.id, vote.clone());
580            Ok(vote.clone())
581        }
582
583        async fn find_by_id(&self, id: Uuid) -> Result<Option<Vote>, String> {
584            Ok(self.votes.lock().unwrap().get(&id).cloned())
585        }
586
587        async fn find_by_resolution_id(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
588            Ok(self
589                .votes
590                .lock()
591                .unwrap()
592                .values()
593                .filter(|v| v.resolution_id == resolution_id)
594                .cloned()
595                .collect())
596        }
597
598        async fn find_by_owner_id(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
599            Ok(self
600                .votes
601                .lock()
602                .unwrap()
603                .values()
604                .filter(|v| v.owner_id == owner_id)
605                .cloned()
606                .collect())
607        }
608
609        async fn find_by_resolution_and_unit(
610            &self,
611            resolution_id: Uuid,
612            unit_id: Uuid,
613        ) -> Result<Option<Vote>, String> {
614            Ok(self
615                .votes
616                .lock()
617                .unwrap()
618                .values()
619                .find(|v| v.resolution_id == resolution_id && v.unit_id == unit_id)
620                .cloned())
621        }
622
623        async fn has_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
624            Ok(self
625                .find_by_resolution_and_unit(resolution_id, unit_id)
626                .await?
627                .is_some())
628        }
629
630        async fn update(&self, vote: &Vote) -> Result<Vote, String> {
631            self.votes.lock().unwrap().insert(vote.id, vote.clone());
632            Ok(vote.clone())
633        }
634
635        async fn delete(&self, id: Uuid) -> Result<bool, String> {
636            Ok(self.votes.lock().unwrap().remove(&id).is_some())
637        }
638
639        async fn count_by_resolution_and_choice(
640            &self,
641            resolution_id: Uuid,
642        ) -> Result<(i32, i32, i32), String> {
643            let votes = self.find_by_resolution_id(resolution_id).await?;
644            let pour = votes
645                .iter()
646                .filter(|v| v.vote_choice == VoteChoice::Pour)
647                .count() as i32;
648            let contre = votes
649                .iter()
650                .filter(|v| v.vote_choice == VoteChoice::Contre)
651                .count() as i32;
652            let abstention = votes
653                .iter()
654                .filter(|v| v.vote_choice == VoteChoice::Abstention)
655                .count() as i32;
656            Ok((pour, contre, abstention))
657        }
658
659        async fn sum_voting_power_by_resolution(
660            &self,
661            resolution_id: Uuid,
662        ) -> Result<(f64, f64, f64), String> {
663            let votes = self.find_by_resolution_id(resolution_id).await?;
664            let pour: f64 = votes
665                .iter()
666                .filter(|v| v.vote_choice == VoteChoice::Pour)
667                .map(|v| v.voting_power)
668                .sum();
669            let contre: f64 = votes
670                .iter()
671                .filter(|v| v.vote_choice == VoteChoice::Contre)
672                .map(|v| v.voting_power)
673                .sum();
674            let abstention: f64 = votes
675                .iter()
676                .filter(|v| v.vote_choice == VoteChoice::Abstention)
677                .map(|v| v.voting_power)
678                .sum();
679            Ok((pour, contre, abstention))
680        }
681
682        async fn count_proxy_votes_for_mandataire(
683            &self,
684            resolution_id: Uuid,
685            proxy_owner_id: Uuid,
686        ) -> Result<(i64, f64), String> {
687            let votes = self.find_by_resolution_id(resolution_id).await?;
688            let proxy_votes: Vec<_> = votes
689                .iter()
690                .filter(|v| v.proxy_owner_id == Some(proxy_owner_id))
691                .collect();
692            let count = proxy_votes.len() as i64;
693            let power: f64 = proxy_votes.iter().map(|v| v.voting_power).sum();
694            Ok((count, power))
695        }
696    }
697
698    #[tokio::test]
699    async fn test_create_resolution() {
700        let resolution_repo = Arc::new(MockResolutionRepository::new());
701        let vote_repo = Arc::new(MockVoteRepository::new());
702        let meeting_repo = Arc::new(MockMeetingRepository::new());
703        let use_cases =
704            ResolutionUseCases::new(resolution_repo.clone(), vote_repo, meeting_repo.clone());
705
706        let org_id = Uuid::new_v4();
707        let building_id = Uuid::new_v4();
708        let meeting_id = Uuid::new_v4();
709
710        // Create a meeting with quorum reached
711        let mut meeting = Meeting::new(
712            org_id,
713            building_id,
714            MeetingType::Ordinary,
715            "AGO 2024".to_string(),
716            None,
717            Utc::now() + chrono::Duration::days(30),
718            "Salle des fêtes".to_string(),
719        )
720        .unwrap();
721        meeting.id = meeting_id;
722        // Validate quorum (600/1000 = 60% > 50%)
723        meeting.validate_quorum(600.0, 1000.0).unwrap();
724        meeting_repo.create(&meeting).await.unwrap();
725
726        let result = use_cases
727            .create_resolution(
728                meeting_id,
729                "Test Resolution".to_string(),
730                "Description".to_string(),
731                ResolutionType::Ordinary,
732                MajorityType::Simple,
733                None,
734            )
735            .await;
736
737        assert!(result.is_ok());
738        let resolution = result.unwrap();
739        assert_eq!(resolution.title, "Test Resolution");
740        assert_eq!(resolution.status, ResolutionStatus::Pending);
741    }
742
743    #[tokio::test]
744    async fn test_create_resolution_fails_without_quorum() {
745        let resolution_repo = Arc::new(MockResolutionRepository::new());
746        let vote_repo = Arc::new(MockVoteRepository::new());
747        let meeting_repo = Arc::new(MockMeetingRepository::new());
748        let use_cases =
749            ResolutionUseCases::new(resolution_repo.clone(), vote_repo, meeting_repo.clone());
750
751        let org_id = Uuid::new_v4();
752        let building_id = Uuid::new_v4();
753        let meeting_id = Uuid::new_v4();
754
755        // Create a meeting with quorum NOT reached
756        let mut meeting = Meeting::new(
757            org_id,
758            building_id,
759            MeetingType::Ordinary,
760            "AGO 2024".to_string(),
761            None,
762            Utc::now() + chrono::Duration::days(30),
763            "Salle des fêtes".to_string(),
764        )
765        .unwrap();
766        meeting.id = meeting_id;
767        // Validate quorum (400/1000 = 40% < 50%) — quorum NOT reached
768        meeting.validate_quorum(400.0, 1000.0).unwrap();
769        meeting_repo.create(&meeting).await.unwrap();
770
771        let result = use_cases
772            .create_resolution(
773                meeting_id,
774                "Test Resolution".to_string(),
775                "Description".to_string(),
776                ResolutionType::Ordinary,
777                MajorityType::Simple,
778                None,
779            )
780            .await;
781
782        // Should fail because quorum not reached
783        assert!(result.is_err());
784        assert!(result.unwrap_err().contains("second convocation"));
785    }
786
787    #[tokio::test]
788    async fn test_cast_vote_updates_counts() {
789        let resolution_repo = Arc::new(MockResolutionRepository::new());
790        let vote_repo = Arc::new(MockVoteRepository::new());
791        let meeting_repo = Arc::new(MockMeetingRepository::new());
792        let use_cases = ResolutionUseCases::new(
793            resolution_repo.clone(),
794            vote_repo.clone(),
795            meeting_repo.clone(),
796        );
797
798        // Create a resolution
799        let org_id = Uuid::new_v4();
800        let building_id = Uuid::new_v4();
801        let meeting_id = Uuid::new_v4();
802
803        // Create a meeting with quorum reached
804        let mut meeting = Meeting::new(
805            org_id,
806            building_id,
807            MeetingType::Ordinary,
808            "AGO 2024".to_string(),
809            None,
810            Utc::now() + chrono::Duration::days(30),
811            "Salle des fêtes".to_string(),
812        )
813        .unwrap();
814        meeting.id = meeting_id;
815        meeting.validate_quorum(600.0, 1000.0).unwrap();
816        meeting_repo.create(&meeting).await.unwrap();
817
818        let resolution = use_cases
819            .create_resolution(
820                meeting_id,
821                "Test Resolution".to_string(),
822                "Description".to_string(),
823                ResolutionType::Ordinary,
824                MajorityType::Simple,
825                None,
826            )
827            .await
828            .unwrap();
829
830        // Cast a vote
831        let owner_id = Uuid::new_v4();
832        let unit_id = Uuid::new_v4();
833        let result = use_cases
834            .cast_vote(
835                resolution.id,
836                owner_id,
837                unit_id,
838                VoteChoice::Pour,
839                100.0,
840                None,
841            )
842            .await;
843
844        assert!(result.is_ok());
845
846        // Check that vote counts were updated
847        let updated_resolution = use_cases
848            .get_resolution(resolution.id)
849            .await
850            .unwrap()
851            .unwrap();
852        assert_eq!(updated_resolution.vote_count_pour, 1);
853        assert_eq!(updated_resolution.total_voting_power_pour, 100.0);
854    }
855
856    #[tokio::test]
857    async fn test_cannot_vote_twice() {
858        let resolution_repo = Arc::new(MockResolutionRepository::new());
859        let vote_repo = Arc::new(MockVoteRepository::new());
860        let meeting_repo = Arc::new(MockMeetingRepository::new());
861        let use_cases =
862            ResolutionUseCases::new(resolution_repo.clone(), vote_repo, meeting_repo.clone());
863
864        // Create resolution
865        let org_id = Uuid::new_v4();
866        let building_id = Uuid::new_v4();
867        let meeting_id = Uuid::new_v4();
868
869        // Create a meeting with quorum reached
870        let mut meeting = Meeting::new(
871            org_id,
872            building_id,
873            MeetingType::Ordinary,
874            "AGO 2024".to_string(),
875            None,
876            Utc::now() + chrono::Duration::days(30),
877            "Salle des fêtes".to_string(),
878        )
879        .unwrap();
880        meeting.id = meeting_id;
881        meeting.validate_quorum(600.0, 1000.0).unwrap();
882        meeting_repo.create(&meeting).await.unwrap();
883
884        let resolution = use_cases
885            .create_resolution(
886                meeting_id,
887                "Test".to_string(),
888                "Desc".to_string(),
889                ResolutionType::Ordinary,
890                MajorityType::Simple,
891                None,
892            )
893            .await
894            .unwrap();
895
896        let owner_id = Uuid::new_v4();
897        let unit_id = Uuid::new_v4();
898
899        // First vote succeeds
900        let result1 = use_cases
901            .cast_vote(
902                resolution.id,
903                owner_id,
904                unit_id,
905                VoteChoice::Pour,
906                100.0,
907                None,
908            )
909            .await;
910        assert!(result1.is_ok());
911
912        // Second vote from same unit fails
913        let result2 = use_cases
914            .cast_vote(
915                resolution.id,
916                owner_id,
917                unit_id,
918                VoteChoice::Contre,
919                100.0,
920                None,
921            )
922            .await;
923        assert!(result2.is_err());
924        assert!(result2.unwrap_err().contains("already voted"));
925    }
926
927    /// Art. 3.87 §7 CC — un mandataire ne peut pas détenir plus de 3 procurations
928    #[tokio::test]
929    async fn test_proxy_limit_max_3_enforced() {
930        let resolution_repo = Arc::new(MockResolutionRepository::new());
931        let vote_repo = Arc::new(MockVoteRepository::new());
932        let meeting_repo = Arc::new(MockMeetingRepository::new());
933        let use_cases = ResolutionUseCases::new(
934            resolution_repo.clone(),
935            vote_repo.clone(),
936            meeting_repo.clone(),
937        );
938
939        let org_id = Uuid::new_v4();
940        let building_id = Uuid::new_v4();
941        let meeting_id = Uuid::new_v4();
942
943        // Create a meeting with quorum reached
944        let mut meeting = Meeting::new(
945            org_id,
946            building_id,
947            MeetingType::Ordinary,
948            "AGO 2024".to_string(),
949            None,
950            Utc::now() + chrono::Duration::days(30),
951            "Salle des fêtes".to_string(),
952        )
953        .unwrap();
954        meeting.id = meeting_id;
955        meeting.validate_quorum(600.0, 1000.0).unwrap();
956        meeting_repo.create(&meeting).await.unwrap();
957
958        let resolution = use_cases
959            .create_resolution(
960                meeting_id,
961                "Test procurations".to_string(),
962                "Description".to_string(),
963                ResolutionType::Ordinary,
964                MajorityType::Simple,
965                None,
966            )
967            .await
968            .unwrap();
969
970        let mandataire_id = Uuid::new_v4();
971
972        // Cast 3 proxy votes for the same mandataire (should all succeed)
973        for i in 0..3 {
974            let owner_id = Uuid::new_v4();
975            let unit_id = Uuid::new_v4();
976            let result = use_cases
977                .cast_vote(
978                    resolution.id,
979                    owner_id,
980                    unit_id,
981                    VoteChoice::Pour,
982                    100.0, // 100 millièmes chacun = 300 total
983                    Some(mandataire_id),
984                )
985                .await;
986            assert!(
987                result.is_ok(),
988                "Proxy vote {} should succeed, got: {:?}",
989                i + 1,
990                result.err()
991            );
992        }
993
994        // 4th proxy vote for the same mandataire should fail (>3 proxies AND >10% of votes)
995        let owner_id_4 = Uuid::new_v4();
996        let unit_id_4 = Uuid::new_v4();
997        let result4 = use_cases
998            .cast_vote(
999                resolution.id,
1000                owner_id_4,
1001                unit_id_4,
1002                VoteChoice::Pour,
1003                100.0,
1004                Some(mandataire_id),
1005            )
1006            .await;
1007        assert!(result4.is_err(), "4th proxy vote should be rejected");
1008        assert!(
1009            result4.unwrap_err().contains("3"),
1010            "Error should mention the 3-proxy limit"
1011        );
1012    }
1013
1014    /// Art. 3.87 §7 CC — exception 10% : si total procurations < 10% → pas de limite
1015    #[tokio::test]
1016    async fn test_proxy_limit_10_percent_exception_allows_more() {
1017        let resolution_repo = Arc::new(MockResolutionRepository::new());
1018        let vote_repo = Arc::new(MockVoteRepository::new());
1019        let meeting_repo = Arc::new(MockMeetingRepository::new());
1020        let use_cases = ResolutionUseCases::new(
1021            resolution_repo.clone(),
1022            vote_repo.clone(),
1023            meeting_repo.clone(),
1024        );
1025
1026        let org_id = Uuid::new_v4();
1027        let building_id = Uuid::new_v4();
1028        let meeting_id = Uuid::new_v4();
1029
1030        // Create a meeting with quorum reached
1031        let mut meeting = Meeting::new(
1032            org_id,
1033            building_id,
1034            MeetingType::Ordinary,
1035            "AGO 2024".to_string(),
1036            None,
1037            Utc::now() + chrono::Duration::days(30),
1038            "Salle des fêtes".to_string(),
1039        )
1040        .unwrap();
1041        meeting.id = meeting_id;
1042        meeting.validate_quorum(600.0, 1000.0).unwrap();
1043        meeting_repo.create(&meeting).await.unwrap();
1044
1045        let resolution = use_cases
1046            .create_resolution(
1047                meeting_id,
1048                "Test exception 10%".to_string(),
1049                "Description".to_string(),
1050                ResolutionType::Ordinary,
1051                MajorityType::Simple,
1052                None,
1053            )
1054            .await
1055            .unwrap();
1056
1057        let mandataire_id = Uuid::new_v4();
1058
1059        // First, add many direct votes to make the total large (900 millièmes direct)
1060        for _ in 0..9 {
1061            let _ = use_cases
1062                .cast_vote(
1063                    resolution.id,
1064                    Uuid::new_v4(),
1065                    Uuid::new_v4(),
1066                    VoteChoice::Pour,
1067                    100.0, // 100 millièmes each, 9 × 100 = 900 total direct
1068                    None,
1069                )
1070                .await;
1071        }
1072
1073        // Now add 4 proxy votes of 5 millièmes each = 20 millièmes proxy
1074        // Total votes = 900 + 20 = 920 → 20/920 = 2.2% < 10% → exception applies
1075        for i in 0..4 {
1076            let result = use_cases
1077                .cast_vote(
1078                    resolution.id,
1079                    Uuid::new_v4(),
1080                    Uuid::new_v4(),
1081                    VoteChoice::Pour,
1082                    5.0, // 5 millièmes chacun
1083                    Some(mandataire_id),
1084                )
1085                .await;
1086            assert!(
1087                result.is_ok(),
1088                "Proxy vote {} (10% exception) should succeed, got: {:?}",
1089                i + 1,
1090                result.err()
1091            );
1092        }
1093    }
1094}